Merge "Import translations. DO NOT MERGE ANYWHERE"
diff --git a/apct-tests/perftests/inputmethod/src/android/inputmethod/ImePerfTest.java b/apct-tests/perftests/inputmethod/src/android/inputmethod/ImePerfTest.java
index a1383e6..203bb54 100644
--- a/apct-tests/perftests/inputmethod/src/android/inputmethod/ImePerfTest.java
+++ b/apct-tests/perftests/inputmethod/src/android/inputmethod/ImePerfTest.java
@@ -411,6 +411,10 @@
private void addResultToState(ManualBenchmarkState state) {
mTraceMethods.forAllSlices((key, slices) -> {
+ if (slices.size() < 2) {
+ Log.w(TAG, "No enough samples for: " + key);
+ return;
+ }
for (TraceMarkSlice slice : slices) {
state.addExtraResult(key, (long) (slice.getDurationInSeconds() * NANOS_PER_S));
}
diff --git a/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java b/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java
index aec60f2..b2bd8d7 100644
--- a/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java
+++ b/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java
@@ -266,6 +266,27 @@
}
}
+ /** Tests switching to an uninitialized user with wait times between iterations. */
+ @Test(timeout = TIMEOUT_MAX_TEST_TIME_MS)
+ public void switchUser_realistic() throws Exception {
+ while (mRunner.keepRunning()) {
+ mRunner.pauseTiming();
+ final int startUser = ActivityManager.getCurrentUser();
+ final int userId = createUserNoFlags();
+ waitCoolDownPeriod();
+ Log.d(TAG, "Starting timer");
+ mRunner.resumeTiming();
+
+ switchUser(userId);
+
+ mRunner.pauseTiming();
+ Log.d(TAG, "Stopping timer");
+ switchUserNoCheck(startUser);
+ removeUser(userId);
+ mRunner.resumeTimingForNextIteration();
+ }
+ }
+
/** Tests switching to a previously-started, but no-longer-running, user. */
@Test(timeout = TIMEOUT_MAX_TEST_TIME_MS)
public void switchUser_stopped() throws RemoteException {
@@ -286,6 +307,30 @@
}
}
+ /** Tests switching to a previously-started, but no-longer-running, user with wait
+ * times between iterations */
+ @Test(timeout = TIMEOUT_MAX_TEST_TIME_MS)
+ public void switchUser_stopped_realistic() throws RemoteException {
+ final int startUser = ActivityManager.getCurrentUser();
+ final int testUser = initializeNewUserAndSwitchBack(/* stopNewUser */ true);
+ while (mRunner.keepRunning()) {
+ mRunner.pauseTiming();
+ waitCoolDownPeriod();
+ Log.d(TAG, "Starting timer");
+ mRunner.resumeTiming();
+
+ switchUser(testUser);
+
+ mRunner.pauseTiming();
+ Log.d(TAG, "Stopping timer");
+ switchUserNoCheck(startUser);
+ stopUserAfterWaitingForBroadcastIdle(testUser, true);
+ attestFalse("Failed to stop user " + testUser, mAm.isUserRunning(testUser));
+ mRunner.resumeTimingForNextIteration();
+ }
+ removeUser(testUser);
+ }
+
/** Tests switching to an already-created already-running non-owner background user. */
@Test(timeout = TIMEOUT_MAX_TEST_TIME_MS)
public void switchUser_running() throws RemoteException {
@@ -306,6 +351,29 @@
}
}
+ /** Tests switching to an already-created already-running non-owner background user, with wait
+ * times between iterations */
+ @Test(timeout = TIMEOUT_MAX_TEST_TIME_MS)
+ public void switchUser_running_realistic() throws RemoteException {
+ final int startUser = ActivityManager.getCurrentUser();
+ final int testUser = initializeNewUserAndSwitchBack(/* stopNewUser */ false);
+ while (mRunner.keepRunning()) {
+ mRunner.pauseTiming();
+ waitCoolDownPeriod();
+ Log.d(TAG, "Starting timer");
+ mRunner.resumeTiming();
+
+ switchUser(testUser);
+
+ mRunner.pauseTiming();
+ Log.d(TAG, "Stopping timer");
+ waitForBroadcastIdle();
+ switchUserNoCheck(startUser);
+ mRunner.resumeTimingForNextIteration();
+ }
+ removeUser(testUser);
+ }
+
/** Tests stopping a background user. */
@Test(timeout = TIMEOUT_MAX_TEST_TIME_MS)
public void stopUser() throws RemoteException {
@@ -902,4 +970,18 @@
private void waitForBroadcastIdle() {
ShellHelper.runShellCommand("am wait-for-broadcast-idle");
}
+
+ private void sleep(long ms) {
+ try {
+ Thread.sleep(ms);
+ } catch (InterruptedException e) {
+ // Ignore
+ }
+ }
+
+ private void waitCoolDownPeriod() {
+ final int tenSeconds = 1000 * 10;
+ waitForBroadcastIdle();
+ sleep(tenSeconds);
+ }
}
diff --git a/apex/jobscheduler/framework/java/android/app/AlarmManager.java b/apex/jobscheduler/framework/java/android/app/AlarmManager.java
index 53e81c7..439f54c 100644
--- a/apex/jobscheduler/framework/java/android/app/AlarmManager.java
+++ b/apex/jobscheduler/framework/java/android/app/AlarmManager.java
@@ -290,6 +290,17 @@
@EnabledSince(targetSdkVersion = Build.VERSION_CODES.TIRAMISU)
public static final long SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT = 226439802L;
+ /**
+ * Holding the permission {@link Manifest.permission#SCHEDULE_EXACT_ALARM} will no longer pin
+ * the standby-bucket of the app to
+ * {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_WORKING_SET} or better.
+ *
+ * @hide
+ */
+ @ChangeId
+ @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public static final long SCHEDULE_EXACT_ALARM_DOES_NOT_ELEVATE_BUCKET = 262645982L;
+
@UnsupportedAppUsage
private final IAlarmManager mService;
private final Context mContext;
diff --git a/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java b/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java
index 4242cf8..5d67c96 100644
--- a/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java
+++ b/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java
@@ -17,6 +17,7 @@
package android.app;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.app.job.IJobScheduler;
import android.app.job.IUserVisibleJobObserver;
@@ -25,10 +26,13 @@
import android.app.job.JobSnapshot;
import android.app.job.JobWorkItem;
import android.content.Context;
+import android.content.pm.ParceledListSlice;
import android.os.RemoteException;
+import android.util.ArrayMap;
import java.util.List;
-
+import java.util.Map;
+import java.util.Set;
/**
* Concrete implementation of the JobScheduler interface
@@ -41,16 +45,42 @@
public class JobSchedulerImpl extends JobScheduler {
IJobScheduler mBinder;
private final Context mContext;
+ private final String mNamespace;
public JobSchedulerImpl(@NonNull Context context, IJobScheduler binder) {
+ this(context, binder, null);
+ }
+
+ private JobSchedulerImpl(@NonNull Context context, IJobScheduler binder,
+ @Nullable String namespace) {
mContext = context;
mBinder = binder;
+ mNamespace = namespace;
+ }
+
+ private JobSchedulerImpl(JobSchedulerImpl jsi, @Nullable String namespace) {
+ this(jsi.mContext, jsi.mBinder, namespace);
+ }
+
+ @NonNull
+ @Override
+ public JobScheduler forNamespace(@NonNull String namespace) {
+ if (namespace == null) {
+ throw new IllegalArgumentException("namespace cannot be null");
+ }
+ return new JobSchedulerImpl(this, namespace);
+ }
+
+ @Nullable
+ @Override
+ public String getNamespace() {
+ return mNamespace;
}
@Override
public int schedule(JobInfo job) {
try {
- return mBinder.schedule(job);
+ return mBinder.schedule(mNamespace, job);
} catch (RemoteException e) {
return JobScheduler.RESULT_FAILURE;
}
@@ -59,7 +89,7 @@
@Override
public int enqueue(JobInfo job, JobWorkItem work) {
try {
- return mBinder.enqueue(job, work);
+ return mBinder.enqueue(mNamespace, job, work);
} catch (RemoteException e) {
return JobScheduler.RESULT_FAILURE;
}
@@ -68,7 +98,7 @@
@Override
public int scheduleAsPackage(JobInfo job, String packageName, int userId, String tag) {
try {
- return mBinder.scheduleAsPackage(job, packageName, userId, tag);
+ return mBinder.scheduleAsPackage(mNamespace, job, packageName, userId, tag);
} catch (RemoteException e) {
return JobScheduler.RESULT_FAILURE;
}
@@ -77,23 +107,44 @@
@Override
public void cancel(int jobId) {
try {
- mBinder.cancel(jobId);
+ mBinder.cancel(mNamespace, jobId);
} catch (RemoteException e) {}
-
}
@Override
public void cancelAll() {
try {
+ mBinder.cancelAllInNamespace(mNamespace);
+ } catch (RemoteException e) {}
+ }
+
+ @Override
+ public void cancelInAllNamespaces() {
+ try {
mBinder.cancelAll();
} catch (RemoteException e) {}
-
}
@Override
public List<JobInfo> getAllPendingJobs() {
try {
- return mBinder.getAllPendingJobs().getList();
+ return mBinder.getAllPendingJobsInNamespace(mNamespace).getList();
+ } catch (RemoteException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public Map<String, List<JobInfo>> getPendingJobsInAllNamespaces() {
+ try {
+ final Map<String, ParceledListSlice<JobInfo>> parceledList =
+ mBinder.getAllPendingJobs();
+ final ArrayMap<String, List<JobInfo>> jobMap = new ArrayMap<>();
+ final Set<String> keys = parceledList.keySet();
+ for (String key : keys) {
+ jobMap.put(key, parceledList.get(key).getList());
+ }
+ return jobMap;
} catch (RemoteException e) {
return null;
}
@@ -102,7 +153,7 @@
@Override
public JobInfo getPendingJob(int jobId) {
try {
- return mBinder.getPendingJob(jobId);
+ return mBinder.getPendingJob(mNamespace, jobId);
} catch (RemoteException e) {
return null;
}
@@ -111,7 +162,7 @@
@Override
public int getPendingJobReason(int jobId) {
try {
- return mBinder.getPendingJobReason(jobId);
+ return mBinder.getPendingJobReason(mNamespace, jobId);
} catch (RemoteException e) {
return PENDING_JOB_REASON_UNDEFINED;
}
diff --git a/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl b/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl
index c87a2af..a1f1954 100644
--- a/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl
+++ b/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl
@@ -21,20 +21,24 @@
import android.app.job.JobSnapshot;
import android.app.job.JobWorkItem;
import android.content.pm.ParceledListSlice;
+import java.util.Map;
/**
* IPC interface that supports the app-facing {@link #JobScheduler} api.
* {@hide}
*/
interface IJobScheduler {
- int schedule(in JobInfo job);
- int enqueue(in JobInfo job, in JobWorkItem work);
- int scheduleAsPackage(in JobInfo job, String packageName, int userId, String tag);
- void cancel(int jobId);
+ int schedule(String namespace, in JobInfo job);
+ int enqueue(String namespace, in JobInfo job, in JobWorkItem work);
+ int scheduleAsPackage(String namespace, in JobInfo job, String packageName, int userId, String tag);
+ void cancel(String namespace, int jobId);
void cancelAll();
- ParceledListSlice getAllPendingJobs();
- JobInfo getPendingJob(int jobId);
- int getPendingJobReason(int jobId);
+ void cancelAllInNamespace(String namespace);
+ // Returns Map<String, ParceledListSlice>, where the keys are the namespaces.
+ Map<String, ParceledListSlice<JobInfo>> getAllPendingJobs();
+ ParceledListSlice<JobInfo> getAllPendingJobsInNamespace(String namespace);
+ JobInfo getPendingJob(String namespace, int jobId);
+ int getPendingJobReason(String namespace, int jobId);
boolean canRunLongJobs(String packageName);
boolean hasRunLongJobsPermission(String packageName, int userId);
List<JobInfo> getStartedJobs();
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
index deb97a5..e0fffb4 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
@@ -924,7 +924,8 @@
@SuppressWarnings("UnsafeParcelApi")
private JobInfo(Parcel in) {
jobId = in.readInt();
- extras = in.readPersistableBundle();
+ final PersistableBundle persistableExtras = in.readPersistableBundle();
+ extras = persistableExtras != null ? persistableExtras : PersistableBundle.EMPTY;
transientExtras = in.readBundle();
if (in.readInt() != 0) {
clipData = ClipData.CREATOR.createFromParcel(in);
@@ -1251,6 +1252,9 @@
* in them all being treated the same. The priorities each have slightly different
* behaviors, as noted in their relevant javadoc.
*
+ * Starting in Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
+ * the priority will only affect sorting order within the job's namespace.
+ *
* <b>NOTE:</b> Setting all of your jobs to high priority will not be
* beneficial to your app and in fact may hurt its performance in the
* long run.
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
index a5a7f93..18ddffb 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
@@ -277,6 +277,8 @@
@UnsupportedAppUsage
private final int jobId;
+ @Nullable
+ private final String mJobNamespace;
private final PersistableBundle extras;
private final Bundle transientExtras;
private final ClipData clipData;
@@ -295,7 +297,7 @@
private String debugStopReason; // Human readable stop reason for debugging.
/** @hide */
- public JobParameters(IBinder callback, int jobId, PersistableBundle extras,
+ public JobParameters(IBinder callback, String namespace, int jobId, PersistableBundle extras,
Bundle transientExtras, ClipData clipData, int clipGrantFlags,
boolean overrideDeadlineExpired, boolean isExpedited,
boolean isUserInitiated, Uri[] triggeredContentUris,
@@ -312,6 +314,7 @@
this.mTriggeredContentUris = triggeredContentUris;
this.mTriggeredContentAuthorities = triggeredContentAuthorities;
this.network = network;
+ this.mJobNamespace = namespace;
}
/**
@@ -322,6 +325,19 @@
}
/**
+ * Get the namespace this job was placed in.
+ *
+ * @see JobScheduler#forNamespace(String)
+ * @return The namespace this job was scheduled in. Will be {@code null} if there was no
+ * explicit namespace set and this job is therefore in the default namespace.
+ * @hide
+ */
+ @Nullable
+ public String getJobNamespace() {
+ return mJobNamespace;
+ }
+
+ /**
* @return The reason {@link JobService#onStopJob(JobParameters)} was called on this job. Will
* be {@link #STOP_REASON_UNDEFINED} if {@link JobService#onStopJob(JobParameters)} has not
* yet been called.
@@ -540,6 +556,7 @@
private JobParameters(Parcel in) {
jobId = in.readInt();
+ mJobNamespace = in.readString();
extras = in.readPersistableBundle();
transientExtras = in.readBundle();
if (in.readInt() != 0) {
@@ -581,6 +598,7 @@
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(jobId);
+ dest.writeString(mJobNamespace);
dest.writePersistableBundle(extras);
dest.writeBundle(transientExtras);
if (clipData != null) {
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
index c9981da..33668c7 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
@@ -38,6 +38,7 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
+import java.util.Map;
/**
* This is an API for scheduling various types of jobs against the framework that will be executed
@@ -264,6 +265,31 @@
}
/**
+ * Get a JobScheduler instance that is dedicated to a specific namespace. Any API calls using
+ * this instance will interact with jobs in that namespace, unless the API documentation says
+ * otherwise. Attempting to update a job scheduled in another namespace will not be possible
+ * but will instead create or update the job inside the current namespace. A JobScheduler
+ * instance dedicated to a namespace must be used to schedule or update jobs in that namespace.
+ * @see #getNamespace()
+ * @hide
+ */
+ @NonNull
+ public JobScheduler forNamespace(@NonNull String namespace) {
+ throw new RuntimeException("Not implemented. Must override in a subclass.");
+ }
+
+ /**
+ * Get the namespace this JobScheduler instance is operating in. A {@code null} value means
+ * that the app has not specified a namespace for this instance, and it is therefore using the
+ * default namespace.
+ * @hide
+ */
+ @Nullable
+ public String getNamespace() {
+ throw new RuntimeException("Not implemented. Must override in a subclass.");
+ }
+
+ /**
* Schedule a job to be executed. Will replace any currently scheduled job with the same
* ID with the new information in the {@link JobInfo}. If a job with the given ID is currently
* running, it will be stopped.
@@ -311,7 +337,7 @@
* but there are situations where it may get this wrong and count the JobInfo as changing.
* (That said, you should be relatively safe with a simple set of consistent data in these
* fields.) You should never use {@link JobInfo.Builder#setClipData(ClipData, int)} with
- * work you are enqueue, since currently this will always be treated as a different JobInfo,
+ * work you are enqueuing, since currently this will always be treated as a different JobInfo,
* even if the ClipData contents are exactly the same.</p>
*
* <p class="caution"><strong>Note:</strong> Scheduling a job can have a high cost, even if it's
@@ -319,6 +345,16 @@
* version {@link android.os.Build.VERSION_CODES#Q}. As such, the system may throttle calls to
* this API if calls are made too frequently in a short amount of time.
*
+ * <p class="caution"><strong>Note:</strong> Prior to Android version
+ * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, JobWorkItems could not be persisted.
+ * Apps were not allowed to enqueue JobWorkItems with persisted jobs and the system would throw
+ * an {@link IllegalArgumentException} if they attempted to do so. Starting with
+ * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
+ * JobWorkItems can be persisted alongside the hosting job.
+ * However, Intents cannot be persisted. Set a {@link PersistableBundle} using
+ * {@link JobWorkItem.Builder#setExtras(PersistableBundle)} for any information that needs
+ * to be persisted.
+ *
* <p>Note: The JobService component needs to be enabled in order to successfully schedule a
* job.
*
@@ -364,6 +400,15 @@
public abstract void cancelAll();
/**
+ * Cancel <em>all</em> jobs that have been scheduled by the calling application, regardless of
+ * namespace.
+ * @hide
+ */
+ public void cancelInAllNamespaces() {
+ throw new RuntimeException("Not implemented. Must override in a subclass.");
+ }
+
+ /**
* Retrieve all jobs that have been scheduled by the calling application.
*
* @return a list of all of the app's scheduled jobs. This includes jobs that are
@@ -372,6 +417,21 @@
public abstract @NonNull List<JobInfo> getAllPendingJobs();
/**
+ * Retrieve all jobs that have been scheduled by the calling application within the current
+ * namespace.
+ *
+ * @return a list of all of the app's scheduled jobs scheduled with the current namespace.
+ * If a namespace hasn't been explicitly set with {@link #forNamespace(String)},
+ * then this will return jobs in the default namespace.
+ * This includes jobs that are currently started as well as those that are still waiting to run.
+ * @hide
+ */
+ @NonNull
+ public Map<String, List<JobInfo>> getPendingJobsInAllNamespaces() {
+ throw new RuntimeException("Not implemented. Must override in a subclass.");
+ }
+
+ /**
* Look up the description of a scheduled job.
*
* @return The {@link JobInfo} description of the given scheduled job, or {@code null}
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobWorkItem.java b/apex/jobscheduler/framework/java/android/app/job/JobWorkItem.java
index 32945e0..18167e2 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobWorkItem.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobWorkItem.java
@@ -19,20 +19,33 @@
import static android.app.job.JobInfo.NETWORK_BYTES_UNKNOWN;
import android.annotation.BytesLong;
+import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.SuppressLint;
import android.compat.Compatibility;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Intent;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
+import android.os.PersistableBundle;
/**
* A unit of work that can be enqueued for a job using
* {@link JobScheduler#enqueue JobScheduler.enqueue}. See
* {@link JobParameters#dequeueWork() JobParameters.dequeueWork} for more details.
+ *
+ * <p class="caution"><strong>Note:</strong> Prior to Android version
+ * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, JobWorkItems could not be persisted.
+ * Apps were not allowed to enqueue JobWorkItems with persisted jobs and the system would throw
+ * an {@link IllegalArgumentException} if they attempted to do so. Starting with
+ * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, JobWorkItems can be persisted alongside
+ * the hosting job. However, Intents cannot be persisted. Set a {@link PersistableBundle} using
+ * {@link Builder#setExtras(PersistableBundle)} for any information that needs to be persisted.
*/
final public class JobWorkItem implements Parcelable {
+ @NonNull
+ private final PersistableBundle mExtras;
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
final Intent mIntent;
private final long mNetworkDownloadBytes;
@@ -49,6 +62,10 @@
* Create a new piece of work, which can be submitted to
* {@link JobScheduler#enqueue JobScheduler.enqueue}.
*
+ * <p>
+ * Intents cannot be used for persisted JobWorkItems.
+ * Use {@link Builder#setExtras(PersistableBundle)} instead for persisted JobWorkItems.
+ *
* @param intent The general Intent describing this work.
*/
public JobWorkItem(Intent intent) {
@@ -62,6 +79,10 @@
* See {@link JobInfo.Builder#setEstimatedNetworkBytes(long, long)} for
* details about how to estimate network traffic.
*
+ * <p>
+ * Intents cannot be used for persisted JobWorkItems.
+ * Use {@link Builder#setExtras(PersistableBundle)} instead for persisted JobWorkItems.
+ *
* @param intent The general Intent describing this work.
* @param downloadBytes The estimated size of network traffic that will be
* downloaded by this job work item, in bytes.
@@ -79,6 +100,10 @@
* See {@link JobInfo.Builder#setEstimatedNetworkBytes(long, long)} for
* details about how to estimate network traffic.
*
+ * <p>
+ * Intents cannot be used for persisted JobWorkItems.
+ * Use {@link Builder#setExtras(PersistableBundle)} instead for persisted JobWorkItems.
+ *
* @param intent The general Intent describing this work.
* @param downloadBytes The estimated size of network traffic that will be
* downloaded by this job work item, in bytes.
@@ -89,6 +114,7 @@
*/
public JobWorkItem(@Nullable Intent intent, @BytesLong long downloadBytes,
@BytesLong long uploadBytes, @BytesLong long minimumChunkBytes) {
+ mExtras = PersistableBundle.EMPTY;
mIntent = intent;
mNetworkDownloadBytes = downloadBytes;
mNetworkUploadBytes = uploadBytes;
@@ -96,6 +122,25 @@
enforceValidity(Compatibility.isChangeEnabled(JobInfo.REJECT_NEGATIVE_NETWORK_ESTIMATES));
}
+ private JobWorkItem(@NonNull Builder builder) {
+ mDeliveryCount = builder.mDeliveryCount;
+ mExtras = builder.mExtras.deepCopy();
+ mIntent = builder.mIntent;
+ mNetworkDownloadBytes = builder.mNetworkDownloadBytes;
+ mNetworkUploadBytes = builder.mNetworkUploadBytes;
+ mMinimumChunkBytes = builder.mMinimumNetworkChunkBytes;
+ }
+
+ /**
+ * Return the extras associated with this work.
+ *
+ * @see Builder#setExtras(PersistableBundle)
+ */
+ @NonNull
+ public PersistableBundle getExtras() {
+ return mExtras;
+ }
+
/**
* Return the Intent associated with this work.
*/
@@ -176,6 +221,7 @@
/**
* @hide
*/
+ @Nullable
public Object getGrants() {
return mGrants;
}
@@ -186,6 +232,8 @@
sb.append(mWorkId);
sb.append(" intent=");
sb.append(mIntent);
+ sb.append(" extras=");
+ sb.append(mExtras);
if (mNetworkDownloadBytes != NETWORK_BYTES_UNKNOWN) {
sb.append(" downloadBytes=");
sb.append(mNetworkDownloadBytes);
@@ -207,6 +255,140 @@
}
/**
+ * Builder class for constructing {@link JobWorkItem} objects.
+ */
+ public static final class Builder {
+ private int mDeliveryCount;
+ private PersistableBundle mExtras = PersistableBundle.EMPTY;
+ private Intent mIntent;
+ private long mNetworkDownloadBytes = NETWORK_BYTES_UNKNOWN;
+ private long mNetworkUploadBytes = NETWORK_BYTES_UNKNOWN;
+ private long mMinimumNetworkChunkBytes = NETWORK_BYTES_UNKNOWN;
+
+ /**
+ * Initialize a new Builder to construct a {@link JobWorkItem} object.
+ */
+ public Builder() {
+ }
+
+ /**
+ * @see JobWorkItem#getDeliveryCount()
+ * @return This object for method chaining
+ * @hide
+ */
+ @NonNull
+ public Builder setDeliveryCount(int deliveryCount) {
+ mDeliveryCount = deliveryCount;
+ return this;
+ }
+
+ /**
+ * Set optional extras. This can be persisted, so we only allow primitive types.
+ * @param extras Bundle containing extras you want the scheduler to hold on to for you.
+ * @return This object for method chaining
+ * @see JobWorkItem#getExtras()
+ */
+ @NonNull
+ public Builder setExtras(@NonNull PersistableBundle extras) {
+ if (extras == null) {
+ throw new IllegalArgumentException("extras cannot be null");
+ }
+ mExtras = extras;
+ return this;
+ }
+
+ /**
+ * Set an intent with information relevant to this work item.
+ *
+ * <p>
+ * Intents cannot be used for persisted JobWorkItems.
+ * Use {@link #setExtras(PersistableBundle)} instead for persisted JobWorkItems.
+ *
+ * @return This object for method chaining
+ * @see JobWorkItem#getIntent()
+ */
+ @NonNull
+ public Builder setIntent(@NonNull Intent intent) {
+ mIntent = intent;
+ return this;
+ }
+
+ /**
+ * Set the estimated size of network traffic that will be performed for this work item,
+ * in bytes.
+ *
+ * See {@link JobInfo.Builder#setEstimatedNetworkBytes(long, long)} for
+ * details about how to estimate network traffic.
+ *
+ * @param downloadBytes The estimated size of network traffic that will be
+ * downloaded for this work item, in bytes.
+ * @param uploadBytes The estimated size of network traffic that will be
+ * uploaded for this work item, in bytes.
+ * @return This object for method chaining
+ * @see JobInfo.Builder#setEstimatedNetworkBytes(long, long)
+ * @see JobWorkItem#getEstimatedNetworkDownloadBytes()
+ * @see JobWorkItem#getEstimatedNetworkUploadBytes()
+ */
+ @NonNull
+ @SuppressLint("MissingGetterMatchingBuilder")
+ public Builder setEstimatedNetworkBytes(@BytesLong long downloadBytes,
+ @BytesLong long uploadBytes) {
+ if (downloadBytes != NETWORK_BYTES_UNKNOWN && downloadBytes < 0) {
+ throw new IllegalArgumentException(
+ "Invalid network download bytes: " + downloadBytes);
+ }
+ if (uploadBytes != NETWORK_BYTES_UNKNOWN && uploadBytes < 0) {
+ throw new IllegalArgumentException("Invalid network upload bytes: " + uploadBytes);
+ }
+ mNetworkDownloadBytes = downloadBytes;
+ mNetworkUploadBytes = uploadBytes;
+ return this;
+ }
+
+ /**
+ * Set the minimum size of non-resumable network traffic this work item requires, in bytes.
+ * When the upload or download can be easily paused and resumed, use this to set the
+ * smallest size that must be transmitted between start and stop events to be considered
+ * successful. If the transfer cannot be paused and resumed, then this should be the sum
+ * of the values provided to {@link #setEstimatedNetworkBytes(long, long)}.
+ *
+ * See {@link JobInfo.Builder#setMinimumNetworkChunkBytes(long)} for
+ * details about how to set the minimum chunk.
+ *
+ * @param chunkSizeBytes The smallest piece of data that cannot be easily paused and
+ * resumed, in bytes.
+ * @return This object for method chaining
+ * @see JobInfo.Builder#setMinimumNetworkChunkBytes(long)
+ * @see JobWorkItem#getMinimumNetworkChunkBytes()
+ * @see JobWorkItem#JobWorkItem(android.content.Intent, long, long, long)
+ */
+ @NonNull
+ public Builder setMinimumNetworkChunkBytes(@BytesLong long chunkSizeBytes) {
+ if (chunkSizeBytes != NETWORK_BYTES_UNKNOWN && chunkSizeBytes <= 0) {
+ throw new IllegalArgumentException("Minimum chunk size must be positive");
+ }
+ mMinimumNetworkChunkBytes = chunkSizeBytes;
+ return this;
+ }
+
+ /**
+ * @return The JobWorkItem object to hand to the JobScheduler. This object is immutable.
+ */
+ @NonNull
+ public JobWorkItem build() {
+ return build(Compatibility.isChangeEnabled(JobInfo.REJECT_NEGATIVE_NETWORK_ESTIMATES));
+ }
+
+ /** @hide */
+ @NonNull
+ public JobWorkItem build(boolean rejectNegativeNetworkEstimates) {
+ JobWorkItem jobWorkItem = new JobWorkItem(this);
+ jobWorkItem.enforceValidity(rejectNegativeNetworkEstimates);
+ return jobWorkItem;
+ }
+ }
+
+ /**
* @hide
*/
public void enforceValidity(boolean rejectNegativeNetworkEstimates) {
@@ -249,6 +431,7 @@
} else {
out.writeInt(0);
}
+ out.writePersistableBundle(mExtras);
out.writeLong(mNetworkDownloadBytes);
out.writeLong(mNetworkUploadBytes);
out.writeLong(mMinimumChunkBytes);
@@ -274,6 +457,8 @@
} else {
mIntent = null;
}
+ final PersistableBundle extras = in.readPersistableBundle();
+ mExtras = extras != null ? extras : PersistableBundle.EMPTY;
mNetworkDownloadBytes = in.readLong();
mNetworkUploadBytes = in.readLong();
mMinimumChunkBytes = in.readLong();
diff --git a/apex/jobscheduler/framework/java/android/app/job/UserVisibleJobSummary.java b/apex/jobscheduler/framework/java/android/app/job/UserVisibleJobSummary.java
index afcbe7d..311a9b2 100644
--- a/apex/jobscheduler/framework/java/android/app/job/UserVisibleJobSummary.java
+++ b/apex/jobscheduler/framework/java/android/app/job/UserVisibleJobSummary.java
@@ -17,9 +17,12 @@
package android.app.job;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.os.Parcel;
import android.os.Parcelable;
+import java.util.Objects;
+
/**
* Summary of a scheduled job that the user is meant to be aware of.
*
@@ -30,13 +33,16 @@
private final int mSourceUserId;
@NonNull
private final String mSourcePackageName;
+ @Nullable
+ private final String mNamespace;
private final int mJobId;
public UserVisibleJobSummary(int callingUid, int sourceUserId,
- @NonNull String sourcePackageName, int jobId) {
+ @NonNull String sourcePackageName, String namespace, int jobId) {
mCallingUid = callingUid;
mSourceUserId = sourceUserId;
mSourcePackageName = sourcePackageName;
+ mNamespace = namespace;
mJobId = jobId;
}
@@ -44,6 +50,7 @@
mCallingUid = in.readInt();
mSourceUserId = in.readInt();
mSourcePackageName = in.readString();
+ mNamespace = in.readString();
mJobId = in.readInt();
}
@@ -55,6 +62,10 @@
return mJobId;
}
+ public String getNamespace() {
+ return mNamespace;
+ }
+
public int getSourceUserId() {
return mSourceUserId;
}
@@ -71,6 +82,7 @@
return mCallingUid == that.mCallingUid
&& mSourceUserId == that.mSourceUserId
&& mSourcePackageName.equals(that.mSourcePackageName)
+ && Objects.equals(mNamespace, that.mNamespace)
&& mJobId == that.mJobId;
}
@@ -80,6 +92,9 @@
result = 31 * result + mCallingUid;
result = 31 * result + mSourceUserId;
result = 31 * result + mSourcePackageName.hashCode();
+ if (mNamespace != null) {
+ result = 31 * result + mNamespace.hashCode();
+ }
result = 31 * result + mJobId;
return result;
}
@@ -90,6 +105,7 @@
+ "callingUid=" + mCallingUid
+ ", sourceUserId=" + mSourceUserId
+ ", sourcePackageName='" + mSourcePackageName + "'"
+ + ", namespace=" + mNamespace
+ ", jobId=" + mJobId
+ "}";
}
@@ -104,6 +120,7 @@
dest.writeInt(mCallingUid);
dest.writeInt(mSourceUserId);
dest.writeString(mSourcePackageName);
+ dest.writeString(mNamespace);
dest.writeInt(mJobId);
}
diff --git a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java
index e2d302f..0fa5764 100644
--- a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java
+++ b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java
@@ -318,6 +318,10 @@
private Bundle mIdleIntentOptions;
private Intent mLightIdleIntent;
private Bundle mLightIdleIntentOptions;
+ private Intent mPowerSaveWhitelistChangedIntent;
+ private Bundle mPowerSaveWhitelistChangedOptions;
+ private Intent mPowerSaveTempWhitelistChangedIntent;
+ private Bundle mPowerSaveTempWhilelistChangedOptions;
private AnyMotionDetector mAnyMotionDetector;
private final AppStateTrackerImpl mAppStateTracker;
@GuardedBy("this")
@@ -2532,15 +2536,26 @@
mAppStateTracker.onSystemServicesReady();
+ final Bundle mostRecentDeliveryOptions = BroadcastOptions.makeBasic()
+ .setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT)
+ .toBundle();
+
mIdleIntent = new Intent(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED);
mIdleIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY
| Intent.FLAG_RECEIVER_FOREGROUND);
mLightIdleIntent = new Intent(PowerManager.ACTION_LIGHT_DEVICE_IDLE_MODE_CHANGED);
mLightIdleIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY
| Intent.FLAG_RECEIVER_FOREGROUND);
- final BroadcastOptions options = BroadcastOptions.makeBasic();
- options.setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT);
- mIdleIntentOptions = mLightIdleIntentOptions = options.toBundle();
+ mIdleIntentOptions = mLightIdleIntentOptions = mostRecentDeliveryOptions;
+
+ mPowerSaveWhitelistChangedIntent = new Intent(
+ PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED);
+ mPowerSaveWhitelistChangedIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
+ mPowerSaveTempWhitelistChangedIntent = new Intent(
+ PowerManager.ACTION_POWER_SAVE_TEMP_WHITELIST_CHANGED);
+ mPowerSaveTempWhitelistChangedIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
+ mPowerSaveWhitelistChangedOptions = mostRecentDeliveryOptions;
+ mPowerSaveTempWhilelistChangedOptions = mostRecentDeliveryOptions;
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_BATTERY_CHANGED);
@@ -4350,17 +4365,17 @@
}
private void reportPowerSaveWhitelistChangedLocked() {
- Intent intent = new Intent(PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED);
- intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
- getContext().sendBroadcastAsUser(intent, UserHandle.SYSTEM);
+ getContext().sendBroadcastAsUser(mPowerSaveWhitelistChangedIntent, UserHandle.SYSTEM,
+ null /* receiverPermission */,
+ mPowerSaveWhitelistChangedOptions);
}
private void reportTempWhitelistChangedLocked(final int uid, final boolean added) {
mHandler.obtainMessage(MSG_REPORT_TEMP_APP_WHITELIST_CHANGED, uid, added ? 1 : 0)
.sendToTarget();
- Intent intent = new Intent(PowerManager.ACTION_POWER_SAVE_TEMP_WHITELIST_CHANGED);
- intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
- getContext().sendBroadcastAsUser(intent, UserHandle.SYSTEM);
+ getContext().sendBroadcastAsUser(mPowerSaveTempWhitelistChangedIntent, UserHandle.SYSTEM,
+ null /* receiverPermission */,
+ mPowerSaveTempWhilelistChangedOptions);
}
private void passWhiteListsToForceAppStandbyTrackerLocked() {
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
index 29e730d..d6d51e0 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -2704,9 +2704,11 @@
}
@Override
- public boolean hasExactAlarmPermission(String packageName, int uid) {
- return hasScheduleExactAlarmInternal(packageName, uid)
- || hasUseExactAlarmInternal(packageName, uid);
+ public boolean shouldGetBucketElevation(String packageName, int uid) {
+ return hasUseExactAlarmInternal(packageName, uid) || (!CompatChanges.isChangeEnabled(
+ AlarmManager.SCHEDULE_EXACT_ALARM_DOES_NOT_ELEVATE_BUCKET, packageName,
+ UserHandle.getUserHandleForUid(uid)) && hasScheduleExactAlarmInternal(
+ packageName, uid));
}
@Override
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 397d2c4..b806ef8 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
@@ -615,7 +615,7 @@
private boolean isSimilarJobRunningLocked(JobStatus job) {
for (int i = mRunningJobs.size() - 1; i >= 0; --i) {
JobStatus js = mRunningJobs.valueAt(i);
- if (job.getUid() == js.getUid() && job.getJobId() == js.getJobId()) {
+ if (job.matches(js.getUid(), js.getNamespace(), js.getJobId())) {
return true;
}
}
@@ -1687,12 +1687,12 @@
@GuardedBy("mLock")
boolean executeTimeoutCommandLocked(PrintWriter pw, String pkgName, int userId,
- boolean hasJobId, int jobId) {
+ @Nullable String namespace, boolean hasJobId, int jobId) {
boolean foundSome = false;
for (int i = 0; i < mActiveServices.size(); i++) {
final JobServiceContext jc = mActiveServices.get(i);
final JobStatus js = jc.getRunningJobLocked();
- if (jc.timeoutIfExecutingLocked(pkgName, userId, hasJobId, jobId, "shell")) {
+ if (jc.timeoutIfExecutingLocked(pkgName, userId, namespace, hasJobId, jobId, "shell")) {
foundSome = true;
pw.print("Timing out: ");
js.printUniqueId(pw);
@@ -1709,11 +1709,13 @@
*/
@Nullable
@GuardedBy("mLock")
- Pair<Long, Long> getEstimatedNetworkBytesLocked(String pkgName, int uid, int jobId) {
+ Pair<Long, Long> getEstimatedNetworkBytesLocked(String pkgName, int uid,
+ String namespace, 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)) {
+ if (js != null && js.matches(uid, namespace, jobId)
+ && js.getSourcePackageName().equals(pkgName)) {
return jc.getEstimatedNetworkBytes();
}
}
@@ -1726,11 +1728,13 @@
*/
@Nullable
@GuardedBy("mLock")
- Pair<Long, Long> getTransferredNetworkBytesLocked(String pkgName, int uid, int jobId) {
+ Pair<Long, Long> getTransferredNetworkBytesLocked(String pkgName, int uid,
+ String namespace, 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)) {
+ if (js != null && js.matches(uid, namespace, jobId)
+ && js.getSourcePackageName().equals(pkgName)) {
return jc.getTransferredNetworkBytes();
}
}
@@ -1753,6 +1757,9 @@
pendingJobQueue.resetIterator();
while ((js = pendingJobQueue.next()) != null) {
s.append("(")
+ .append("{")
+ .append(js.getNamespace())
+ .append("} ")
.append(js.getJob().getId())
.append(", ")
.append(js.getUid())
@@ -1777,6 +1784,9 @@
if (job == null) {
s.append("nothing");
} else {
+ if (job.getNamespace() != null) {
+ s.append(job.getNamespace()).append(":");
+ }
s.append(job.getJobId()).append("/").append(job.getUid());
}
s.append(")");
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 c032513..8defa16 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -89,6 +89,7 @@
import android.util.Pair;
import android.util.Slog;
import android.util.SparseArray;
+import android.util.SparseArrayMap;
import android.util.SparseBooleanArray;
import android.util.SparseIntArray;
import android.util.SparseSetArray;
@@ -149,6 +150,7 @@
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Predicate;
@@ -377,8 +379,12 @@
@GuardedBy("mLock")
private final ArraySet<JobStatus> mChangedJobList = new ArraySet<>();
+ /**
+ * Cached pending job reasons. Mapping from UID -> namespace -> job ID -> reason.
+ */
@GuardedBy("mPendingJobReasonCache") // Use its own lock to avoid blocking JS processing
- private final SparseArray<SparseIntArray> mPendingJobReasonCache = new SparseArray<>();
+ private final SparseArrayMap<String, SparseIntArray> mPendingJobReasonCache =
+ new SparseArrayMap<>();
/**
* Named indices into standby bucket arrays, for clarity in referring to
@@ -1333,7 +1339,7 @@
private final Predicate<Integer> mIsUidActivePredicate = this::isUidActive;
public int scheduleAsPackage(JobInfo job, JobWorkItem work, int uId, String packageName,
- int userId, String tag) {
+ int userId, @Nullable String namespace, String tag) {
// Rate limit excessive schedule() calls.
final String servicePkg = job.getService().getPackageName();
if (job.isPersisted() && (packageName == null || packageName.equals(servicePkg))) {
@@ -1392,7 +1398,7 @@
}
synchronized (mLock) {
- final JobStatus toCancel = mJobs.getJobByUidAndJobId(uId, job.getId());
+ final JobStatus toCancel = mJobs.getJobByUidAndJobId(uId, namespace, job.getId());
if (work != null && toCancel != null) {
// Fast path: we are adding work to an existing job, and the JobInfo is not
@@ -1400,6 +1406,7 @@
if (toCancel.getJob().equals(job)) {
toCancel.enqueueWorkLocked(work);
+ mJobs.touchJob(toCancel);
// If any of work item is enqueued when the source is in the foreground,
// exempt the entire job.
@@ -1409,7 +1416,8 @@
}
}
- JobStatus jobStatus = JobStatus.createFromJobInfo(job, uId, packageName, userId, tag);
+ JobStatus jobStatus =
+ JobStatus.createFromJobInfo(job, uId, packageName, userId, namespace, tag);
// Return failure early if expedited job quota used up.
if (jobStatus.isRequestedExpeditedJob()) {
@@ -1504,26 +1512,47 @@
return JobScheduler.RESULT_SUCCESS;
}
- public List<JobInfo> getPendingJobs(int uid) {
+ private ArrayMap<String, List<JobInfo>> getPendingJobs(int uid) {
+ final ArrayMap<String, List<JobInfo>> outMap = new ArrayMap<>();
+ synchronized (mLock) {
+ ArraySet<JobStatus> jobs = mJobs.getJobsByUid(uid);
+ // Write out for loop to avoid addAll() creating an Iterator.
+ for (int i = jobs.size() - 1; i >= 0; i--) {
+ final JobStatus job = jobs.valueAt(i);
+ List<JobInfo> outList = outMap.get(job.getNamespace());
+ if (outList == null) {
+ outList = new ArrayList<JobInfo>(jobs.size());
+ outMap.put(job.getNamespace(), outList);
+ }
+
+ outList.add(job.getJob());
+ }
+ return outMap;
+ }
+ }
+
+ private List<JobInfo> getPendingJobsInNamespace(int uid, @Nullable String namespace) {
synchronized (mLock) {
ArraySet<JobStatus> jobs = mJobs.getJobsByUid(uid);
ArrayList<JobInfo> outList = new ArrayList<JobInfo>(jobs.size());
// Write out for loop to avoid addAll() creating an Iterator.
for (int i = jobs.size() - 1; i >= 0; i--) {
final JobStatus job = jobs.valueAt(i);
- outList.add(job.getJob());
+ if (Objects.equals(namespace, job.getNamespace())) {
+ outList.add(job.getJob());
+ }
}
return outList;
}
}
@JobScheduler.PendingJobReason
- private int getPendingJobReason(int uid, int jobId) {
+ private int getPendingJobReason(int uid, String namespace, int jobId) {
int reason;
// Some apps may attempt to query this frequently, so cache the reason under a separate lock
// so that the rest of JS processing isn't negatively impacted.
synchronized (mPendingJobReasonCache) {
- SparseIntArray jobIdToReason = mPendingJobReasonCache.get(uid);
+ SparseIntArray jobIdToReason = mPendingJobReasonCache.get(uid, namespace);
if (jobIdToReason != null) {
reason = jobIdToReason.get(jobId, JobScheduler.PENDING_JOB_REASON_UNDEFINED);
if (reason != JobScheduler.PENDING_JOB_REASON_UNDEFINED) {
@@ -1532,16 +1561,17 @@
}
}
synchronized (mLock) {
- reason = getPendingJobReasonLocked(uid, jobId);
+ reason = getPendingJobReasonLocked(uid, namespace, jobId);
if (DEBUG) {
- Slog.v(TAG, "getPendingJobReason(" + uid + "," + jobId + ")=" + reason);
+ Slog.v(TAG, "getPendingJobReason("
+ + uid + "," + namespace + "," + jobId + ")=" + reason);
}
}
synchronized (mPendingJobReasonCache) {
- SparseIntArray jobIdToReason = mPendingJobReasonCache.get(uid);
+ SparseIntArray jobIdToReason = mPendingJobReasonCache.get(uid, namespace);
if (jobIdToReason == null) {
jobIdToReason = new SparseIntArray();
- mPendingJobReasonCache.put(uid, jobIdToReason);
+ mPendingJobReasonCache.add(uid, namespace, jobIdToReason);
}
jobIdToReason.put(jobId, reason);
}
@@ -1550,10 +1580,10 @@
@JobScheduler.PendingJobReason
@GuardedBy("mLock")
- private int getPendingJobReasonLocked(int uid, int jobId) {
+ private int getPendingJobReasonLocked(int uid, String namespace, int jobId) {
// Very similar code to isReadyToBeExecutedLocked.
- JobStatus job = mJobs.getJobByUidAndJobId(uid, jobId);
+ JobStatus job = mJobs.getJobByUidAndJobId(uid, namespace, jobId);
if (job == null) {
// Job doesn't exist.
return JobScheduler.PENDING_JOB_REASON_INVALID_JOB_ID;
@@ -1645,12 +1675,12 @@
return JobScheduler.PENDING_JOB_REASON_UNDEFINED;
}
- public JobInfo getPendingJob(int uid, int jobId) {
+ private JobInfo getPendingJob(int uid, @Nullable String namespace, int jobId) {
synchronized (mLock) {
ArraySet<JobStatus> jobs = mJobs.getJobsByUid(uid);
for (int i = jobs.size() - 1; i >= 0; i--) {
JobStatus job = jobs.valueAt(i);
- if (job.getJobId() == jobId) {
+ if (job.getJobId() == jobId && Objects.equals(namespace, job.getNamespace())) {
return job.getJob();
}
}
@@ -1726,12 +1756,20 @@
* This will remove the job from the master list, and cancel the job if it was staged for
* execution or being executed.
*
- * @param uid Uid to check against for removal of a job.
+ * @param uid Uid to check against for removal of a job.
* @param includeSourceApp Whether to include jobs scheduled for this UID by another UID.
* If false, only jobs scheduled by this UID will be cancelled.
*/
public boolean cancelJobsForUid(int uid, boolean includeSourceApp,
@JobParameters.StopReason int reason, int internalReasonCode, String debugReason) {
+ return cancelJobsForUid(uid, includeSourceApp,
+ /* namespaceOnly */ false, /* namespace */ null,
+ reason, internalReasonCode, debugReason);
+ }
+
+ private boolean cancelJobsForUid(int uid, boolean includeSourceApp,
+ boolean namespaceOnly, @Nullable String namespace,
+ @JobParameters.StopReason int reason, int internalReasonCode, String debugReason) {
if (uid == Process.SYSTEM_UID) {
Slog.wtfStack(TAG, "Can't cancel all jobs for system uid");
return false;
@@ -1748,8 +1786,10 @@
}
for (int i = 0; i < jobsForUid.size(); i++) {
JobStatus toRemove = jobsForUid.valueAt(i);
- cancelJobImplLocked(toRemove, null, reason, internalReasonCode, debugReason);
- jobsCanceled = true;
+ if (!namespaceOnly || Objects.equals(namespace, toRemove.getNamespace())) {
+ cancelJobImplLocked(toRemove, null, reason, internalReasonCode, debugReason);
+ jobsCanceled = true;
+ }
}
}
return jobsCanceled;
@@ -1763,11 +1803,11 @@
* @param uid Uid of the calling client.
* @param jobId Id of the job, provided at schedule-time.
*/
- private boolean cancelJob(int uid, int jobId, int callingUid,
+ private boolean cancelJob(int uid, String namespace, int jobId, int callingUid,
@JobParameters.StopReason int reason) {
JobStatus toCancel;
synchronized (mLock) {
- toCancel = mJobs.getJobByUidAndJobId(uid, jobId);
+ toCancel = mJobs.getJobByUidAndJobId(uid, namespace, jobId);
if (toCancel != null) {
cancelJobImplLocked(toCancel, null, reason,
JobParameters.INTERNAL_STOP_REASON_CANCELED,
@@ -2197,7 +2237,8 @@
jobStatus.stopTrackingJobLocked(incomingJob);
synchronized (mPendingJobReasonCache) {
- SparseIntArray reasonCache = mPendingJobReasonCache.get(jobStatus.getUid());
+ SparseIntArray reasonCache =
+ mPendingJobReasonCache.get(jobStatus.getUid(), jobStatus.getNamespace());
if (reasonCache != null) {
reasonCache.delete(jobStatus.getJobId());
}
@@ -2228,7 +2269,8 @@
/** Remove the pending job reason for this job from the cache. */
void resetPendingJobReasonCache(@NonNull JobStatus jobStatus) {
synchronized (mPendingJobReasonCache) {
- final SparseIntArray reasons = mPendingJobReasonCache.get(jobStatus.getUid());
+ final SparseIntArray reasons =
+ mPendingJobReasonCache.get(jobStatus.getUid(), jobStatus.getNamespace());
if (reasons != null) {
reasons.delete(jobStatus.getJobId());
}
@@ -2490,7 +2532,8 @@
if (DEBUG) {
Slog.d(TAG, "Could not find job to remove. Was job removed while executing?");
}
- JobStatus newJs = mJobs.getJobByUidAndJobId(jobStatus.getUid(), jobStatus.getJobId());
+ JobStatus newJs = mJobs.getJobByUidAndJobId(
+ jobStatus.getUid(), jobStatus.getNamespace(), jobStatus.getJobId());
if (newJs != null) {
// This job was stopped because the app scheduled a new job with the same job ID.
// Check if the new job is ready to run.
@@ -2947,10 +2990,12 @@
synchronized (mPendingJobReasonCache) {
for (int i = 0; i < numRunnableJobs; ++i) {
final JobStatus job = runnableJobs.get(i);
- SparseIntArray reasons = mPendingJobReasonCache.get(job.getUid());
+ SparseIntArray reasons =
+ mPendingJobReasonCache.get(job.getUid(), job.getNamespace());
if (reasons == null) {
reasons = new SparseIntArray();
- mPendingJobReasonCache.put(job.getUid(), reasons);
+ mPendingJobReasonCache
+ .add(job.getUid(), job.getNamespace(), reasons);
}
// We're force batching these jobs, so consider it an optimization
// policy reason.
@@ -3731,6 +3776,14 @@
}
}
}
+ if (job.isPersisted()) {
+ // Intent.saveToXml() doesn't persist everything, so just reject all
+ // JobWorkItems with Intents to be safe/predictable.
+ if (jobWorkItem.getIntent() != null) {
+ throw new IllegalArgumentException(
+ "Cannot persist JobWorkItems with Intents");
+ }
+ }
}
return JobScheduler.RESULT_SUCCESS;
}
@@ -3749,7 +3802,7 @@
// IJobScheduler implementation
@Override
- public int schedule(JobInfo job) throws RemoteException {
+ public int schedule(String namespace, JobInfo job) throws RemoteException {
if (DEBUG) {
Slog.d(TAG, "Scheduling job: " + job.toString());
}
@@ -3770,10 +3823,14 @@
return result;
}
+ if (namespace != null) {
+ namespace = namespace.intern();
+ }
+
final long ident = Binder.clearCallingIdentity();
try {
return JobSchedulerService.this.scheduleAsPackage(job, null, uid, null, userId,
- null);
+ namespace, null);
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -3781,7 +3838,7 @@
// IJobScheduler implementation
@Override
- public int enqueue(JobInfo job, JobWorkItem work) throws RemoteException {
+ public int enqueue(String namespace, JobInfo job, JobWorkItem work) throws RemoteException {
if (DEBUG) {
Slog.d(TAG, "Enqueueing job: " + job.toString() + " work: " + work);
}
@@ -3789,9 +3846,6 @@
final int userId = UserHandle.getUserId(uid);
enforceValidJobRequest(uid, job);
- if (job.isPersisted()) {
- throw new IllegalArgumentException("Can't enqueue work for persisted jobs");
- }
if (work == null) {
throw new NullPointerException("work is null");
}
@@ -3801,18 +3855,22 @@
return result;
}
+ if (namespace != null) {
+ namespace = namespace.intern();
+ }
+
final long ident = Binder.clearCallingIdentity();
try {
return JobSchedulerService.this.scheduleAsPackage(job, work, uid, null, userId,
- null);
+ namespace, null);
} finally {
Binder.restoreCallingIdentity(ident);
}
}
@Override
- public int scheduleAsPackage(JobInfo job, String packageName, int userId, String tag)
- throws RemoteException {
+ public int scheduleAsPackage(String namespace, JobInfo job, String packageName, int userId,
+ String tag) throws RemoteException {
final int callerUid = Binder.getCallingUid();
if (DEBUG) {
Slog.d(TAG, "Caller uid " + callerUid + " scheduling job: " + job.toString()
@@ -3835,46 +3893,70 @@
return result;
}
+ if (namespace != null) {
+ namespace = namespace.intern();
+ }
+
final long ident = Binder.clearCallingIdentity();
try {
return JobSchedulerService.this.scheduleAsPackage(job, null, callerUid,
- packageName, userId, tag);
+ packageName, userId, namespace, tag);
} finally {
Binder.restoreCallingIdentity(ident);
}
}
@Override
- public ParceledListSlice<JobInfo> getAllPendingJobs() throws RemoteException {
+ public Map<String, ParceledListSlice<JobInfo>> getAllPendingJobs() throws RemoteException {
final int uid = Binder.getCallingUid();
final long ident = Binder.clearCallingIdentity();
try {
- return new ParceledListSlice<>(JobSchedulerService.this.getPendingJobs(uid));
+ final ArrayMap<String, List<JobInfo>> jobs =
+ JobSchedulerService.this.getPendingJobs(uid);
+ final ArrayMap<String, ParceledListSlice<JobInfo>> outMap = new ArrayMap<>();
+ for (int i = 0; i < jobs.size(); ++i) {
+ outMap.put(jobs.keyAt(i), new ParceledListSlice<>(jobs.valueAt(i)));
+ }
+ return outMap;
} finally {
Binder.restoreCallingIdentity(ident);
}
}
@Override
- public int getPendingJobReason(int jobId) throws RemoteException {
+ public ParceledListSlice<JobInfo> getAllPendingJobsInNamespace(String namespace)
+ throws RemoteException {
final int uid = Binder.getCallingUid();
final long ident = Binder.clearCallingIdentity();
try {
- return JobSchedulerService.this.getPendingJobReason(uid, jobId);
+ return new ParceledListSlice<>(
+ JobSchedulerService.this.getPendingJobsInNamespace(uid, namespace));
} finally {
Binder.restoreCallingIdentity(ident);
}
}
@Override
- public JobInfo getPendingJob(int jobId) throws RemoteException {
+ public JobInfo getPendingJob(String namespace, int jobId) throws RemoteException {
final int uid = Binder.getCallingUid();
final long ident = Binder.clearCallingIdentity();
try {
- return JobSchedulerService.this.getPendingJob(uid, jobId);
+ return JobSchedulerService.this.getPendingJob(uid, namespace, jobId);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ @Override
+ public int getPendingJobReason(String namespace, int jobId) throws RemoteException {
+ final int uid = Binder.getCallingUid();
+
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ return JobSchedulerService.this.getPendingJobReason(uid, namespace, jobId);
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -3897,12 +3979,29 @@
}
@Override
- public void cancel(int jobId) throws RemoteException {
+ public void cancelAllInNamespace(String namespace) throws RemoteException {
+ final int uid = Binder.getCallingUid();
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ JobSchedulerService.this.cancelJobsForUid(uid,
+ // Documentation says only jobs scheduled BY the app will be cancelled
+ /* includeSourceApp */ false,
+ /* namespaceOnly */ true, namespace,
+ JobParameters.STOP_REASON_CANCELLED_BY_APP,
+ JobParameters.INTERNAL_STOP_REASON_CANCELED,
+ "cancelAllInNamespace() called by app, callingUid=" + uid);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ @Override
+ public void cancel(String namespace, int jobId) throws RemoteException {
final int uid = Binder.getCallingUid();
final long ident = Binder.clearCallingIdentity();
try {
- JobSchedulerService.this.cancelJob(uid, jobId, uid,
+ JobSchedulerService.this.cancelJob(uid, namespace, jobId, uid,
JobParameters.STOP_REASON_CANCELLED_BY_APP);
} finally {
Binder.restoreCallingIdentity(ident);
@@ -4080,8 +4179,9 @@
}
// Shell command infrastructure: run the given job immediately
- int executeRunCommand(String pkgName, int userId, int jobId, boolean satisfied, boolean force) {
- Slog.d(TAG, "executeRunCommand(): " + pkgName + "/" + userId
+ int executeRunCommand(String pkgName, int userId, @Nullable String namespace,
+ int jobId, boolean satisfied, boolean force) {
+ Slog.d(TAG, "executeRunCommand(): " + pkgName + "/" + namespace + "/" + userId
+ " " + jobId + " s=" + satisfied + " f=" + force);
try {
@@ -4092,7 +4192,7 @@
}
synchronized (mLock) {
- final JobStatus js = mJobs.getJobByUidAndJobId(uid, jobId);
+ final JobStatus js = mJobs.getJobByUidAndJobId(uid, namespace, jobId);
if (js == null) {
return JobSchedulerShellCommand.CMD_ERR_NO_JOB;
}
@@ -4122,14 +4222,14 @@
// Shell command infrastructure: immediately timeout currently executing jobs
int executeTimeoutCommand(PrintWriter pw, String pkgName, int userId,
- boolean hasJobId, int jobId) {
+ @Nullable String namespace, boolean hasJobId, int jobId) {
if (DEBUG) {
Slog.v(TAG, "executeTimeoutCommand(): " + pkgName + "/" + userId + " " + jobId);
}
synchronized (mLock) {
final boolean foundSome = mConcurrencyManager.executeTimeoutCommandLocked(pw,
- pkgName, userId, hasJobId, jobId);
+ pkgName, userId, namespace, hasJobId, jobId);
if (!foundSome) {
pw.println("No matching executing jobs found.");
}
@@ -4138,7 +4238,7 @@
}
// Shell command infrastructure: cancel a scheduled job
- int executeCancelCommand(PrintWriter pw, String pkgName, int userId,
+ int executeCancelCommand(PrintWriter pw, String pkgName, int userId, @Nullable String namespace,
boolean hasJobId, int jobId) {
if (DEBUG) {
Slog.v(TAG, "executeCancelCommand(): " + pkgName + "/" + userId + " " + jobId);
@@ -4166,7 +4266,8 @@
}
} else {
pw.println("Canceling job " + pkgName + "/#" + jobId + " in user " + userId);
- if (!cancelJob(pkgUid, jobId, Process.SHELL_UID, JobParameters.STOP_REASON_USER)) {
+ if (!cancelJob(pkgUid, namespace, jobId,
+ Process.SHELL_UID, JobParameters.STOP_REASON_USER)) {
pw.println("No matching job found.");
}
}
@@ -4213,8 +4314,8 @@
}
}
- int getEstimatedNetworkBytes(PrintWriter pw, String pkgName, int userId, int jobId,
- int byteOption) {
+ int getEstimatedNetworkBytes(PrintWriter pw, String pkgName, int userId, String namespace,
+ int jobId, int byteOption) {
try {
final int uid = AppGlobals.getPackageManager().getPackageUid(pkgName, 0,
userId != UserHandle.USER_ALL ? userId : UserHandle.USER_SYSTEM);
@@ -4226,9 +4327,10 @@
}
synchronized (mLock) {
- final JobStatus js = mJobs.getJobByUidAndJobId(uid, jobId);
+ final JobStatus js = mJobs.getJobByUidAndJobId(uid, namespace, jobId);
if (DEBUG) {
- Slog.d(TAG, "get-estimated-network-bytes " + uid + "/" + jobId + ": " + js);
+ Slog.d(TAG, "get-estimated-network-bytes " + uid + "/"
+ + namespace + "/" + jobId + ": " + js);
}
if (js == null) {
pw.print("unknown("); UserHandle.formatUid(pw, uid);
@@ -4238,8 +4340,8 @@
final long downloadBytes;
final long uploadBytes;
- final Pair<Long, Long> bytes =
- mConcurrencyManager.getEstimatedNetworkBytesLocked(pkgName, uid, jobId);
+ final Pair<Long, Long> bytes = mConcurrencyManager.getEstimatedNetworkBytesLocked(
+ pkgName, uid, namespace, jobId);
if (bytes == null) {
downloadBytes = js.getEstimatedNetworkDownloadBytes();
uploadBytes = js.getEstimatedNetworkUploadBytes();
@@ -4260,8 +4362,8 @@
return 0;
}
- int getTransferredNetworkBytes(PrintWriter pw, String pkgName, int userId, int jobId,
- int byteOption) {
+ int getTransferredNetworkBytes(PrintWriter pw, String pkgName, int userId, String namespace,
+ int jobId, int byteOption) {
try {
final int uid = AppGlobals.getPackageManager().getPackageUid(pkgName, 0,
userId != UserHandle.USER_ALL ? userId : UserHandle.USER_SYSTEM);
@@ -4273,9 +4375,10 @@
}
synchronized (mLock) {
- final JobStatus js = mJobs.getJobByUidAndJobId(uid, jobId);
+ final JobStatus js = mJobs.getJobByUidAndJobId(uid, namespace, jobId);
if (DEBUG) {
- Slog.d(TAG, "get-transferred-network-bytes " + uid + "/" + jobId + ": " + js);
+ Slog.d(TAG, "get-transferred-network-bytes " + uid
+ + namespace + "/" + "/" + jobId + ": " + js);
}
if (js == null) {
pw.print("unknown("); UserHandle.formatUid(pw, uid);
@@ -4285,8 +4388,8 @@
final long downloadBytes;
final long uploadBytes;
- final Pair<Long, Long> bytes =
- mConcurrencyManager.getTransferredNetworkBytesLocked(pkgName, uid, jobId);
+ final Pair<Long, Long> bytes = mConcurrencyManager.getTransferredNetworkBytesLocked(
+ pkgName, uid, namespace, jobId);
if (bytes == null) {
downloadBytes = 0;
uploadBytes = 0;
@@ -4335,7 +4438,8 @@
}
// Shell command infrastructure
- int getJobState(PrintWriter pw, String pkgName, int userId, int jobId) {
+ int getJobState(PrintWriter pw, String pkgName, int userId, @Nullable String namespace,
+ int jobId) {
try {
final int uid = AppGlobals.getPackageManager().getPackageUid(pkgName, 0,
userId != UserHandle.USER_ALL ? userId : UserHandle.USER_SYSTEM);
@@ -4347,11 +4451,17 @@
}
synchronized (mLock) {
- final JobStatus js = mJobs.getJobByUidAndJobId(uid, jobId);
- if (DEBUG) Slog.d(TAG, "get-job-state " + uid + "/" + jobId + ": " + js);
+ final JobStatus js = mJobs.getJobByUidAndJobId(uid, namespace, jobId);
+ if (DEBUG) {
+ Slog.d(TAG,
+ "get-job-state " + namespace + "/" + uid + "/" + jobId + ": " + js);
+ }
if (js == null) {
- pw.print("unknown("); UserHandle.formatUid(pw, uid);
- pw.print("/jid"); pw.print(jobId); pw.println(")");
+ pw.print("unknown(");
+ UserHandle.formatUid(pw, uid);
+ pw.print("/jid");
+ pw.print(jobId);
+ pw.println(")");
return JobSchedulerShellCommand.CMD_ERR_NO_JOB;
}
@@ -4527,7 +4637,9 @@
}
jobPrinted = true;
- pw.print("JOB #"); job.printUniqueId(pw); pw.print(": ");
+ pw.print("JOB ");
+ job.printUniqueId(pw);
+ pw.print(": ");
pw.println(job.toShortStringExceptUniqueId());
pw.increaseIndent();
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 36ba8dd..2eeb25e 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java
@@ -16,6 +16,7 @@
package com.android.server.job;
+import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.AppGlobals;
import android.content.pm.IPackageManager;
@@ -107,7 +108,8 @@
}
}
- private boolean printError(int errCode, String pkgName, int userId, int jobId) {
+ private boolean printError(int errCode, String pkgName, int userId, @Nullable String namespace,
+ int jobId) {
PrintWriter pw;
switch (errCode) {
case CMD_ERR_NO_PACKAGE:
@@ -124,6 +126,10 @@
pw.print(jobId);
pw.print(" in package ");
pw.print(pkgName);
+ if (namespace != null) {
+ pw.print(" / namespace ");
+ pw.print(namespace);
+ }
pw.print(" / user ");
pw.println(userId);
return true;
@@ -134,6 +140,10 @@
pw.print(jobId);
pw.print(" in package ");
pw.print(pkgName);
+ if (namespace != null) {
+ pw.print(" / namespace ");
+ pw.print(namespace);
+ }
pw.print(" / user ");
pw.print(userId);
pw.println(" has functional constraints but --force not specified");
@@ -150,6 +160,7 @@
boolean force = false;
boolean satisfied = false;
int userId = UserHandle.USER_SYSTEM;
+ String namespace = null;
String opt;
while ((opt = getNextOption()) != null) {
@@ -169,6 +180,11 @@
userId = Integer.parseInt(getNextArgRequired());
break;
+ case "-n":
+ case "--namespace":
+ namespace = getNextArgRequired();
+ break;
+
default:
pw.println("Error: unknown option '" + opt + "'");
return -1;
@@ -185,8 +201,9 @@
final long ident = Binder.clearCallingIdentity();
try {
- int ret = mInternal.executeRunCommand(pkgName, userId, jobId, satisfied, force);
- if (printError(ret, pkgName, userId, jobId)) {
+ int ret = mInternal.executeRunCommand(pkgName, userId, namespace,
+ jobId, satisfied, force);
+ if (printError(ret, pkgName, userId, namespace, jobId)) {
return ret;
}
@@ -207,6 +224,7 @@
checkPermission("force timeout jobs");
int userId = UserHandle.USER_ALL;
+ String namespace = null;
String opt;
while ((opt = getNextOption()) != null) {
@@ -216,6 +234,11 @@
userId = UserHandle.parseUserArg(getNextArgRequired());
break;
+ case "-n":
+ case "--namespace":
+ namespace = getNextArgRequired();
+ break;
+
default:
pw.println("Error: unknown option '" + opt + "'");
return -1;
@@ -232,7 +255,8 @@
final long ident = Binder.clearCallingIdentity();
try {
- return mInternal.executeTimeoutCommand(pw, pkgName, userId, jobIdStr != null, jobId);
+ return mInternal.executeTimeoutCommand(pw, pkgName, userId, namespace,
+ jobIdStr != null, jobId);
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -242,6 +266,7 @@
checkPermission("cancel jobs");
int userId = UserHandle.USER_SYSTEM;
+ String namespace = null;
String opt;
while ((opt = getNextOption()) != null) {
@@ -251,6 +276,11 @@
userId = UserHandle.parseUserArg(getNextArgRequired());
break;
+ case "-n":
+ case "--namespace":
+ namespace = getNextArgRequired();
+ break;
+
default:
pw.println("Error: unknown option '" + opt + "'");
return -1;
@@ -268,7 +298,8 @@
final long ident = Binder.clearCallingIdentity();
try {
- return mInternal.executeCancelCommand(pw, pkgName, userId, jobIdStr != null, jobId);
+ return mInternal.executeCancelCommand(pw, pkgName, userId, namespace,
+ jobIdStr != null, jobId);
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -319,6 +350,7 @@
checkPermission("get estimated bytes");
int userId = UserHandle.USER_SYSTEM;
+ String namespace = null;
String opt;
while ((opt = getNextOption()) != null) {
@@ -328,6 +360,11 @@
userId = UserHandle.parseUserArg(getNextArgRequired());
break;
+ case "-n":
+ case "--namespace":
+ namespace = getNextArgRequired();
+ break;
+
default:
pw.println("Error: unknown option '" + opt + "'");
return -1;
@@ -344,8 +381,9 @@
final long ident = Binder.clearCallingIdentity();
try {
- int ret = mInternal.getEstimatedNetworkBytes(pw, pkgName, userId, jobId, byteOption);
- printError(ret, pkgName, userId, jobId);
+ int ret = mInternal.getEstimatedNetworkBytes(pw, pkgName, userId, namespace,
+ jobId, byteOption);
+ printError(ret, pkgName, userId, namespace, jobId);
return ret;
} finally {
Binder.restoreCallingIdentity(ident);
@@ -368,6 +406,7 @@
checkPermission("get transferred bytes");
int userId = UserHandle.USER_SYSTEM;
+ String namespace = null;
String opt;
while ((opt = getNextOption()) != null) {
@@ -377,6 +416,11 @@
userId = UserHandle.parseUserArg(getNextArgRequired());
break;
+ case "-n":
+ case "--namespace":
+ namespace = getNextArgRequired();
+ break;
+
default:
pw.println("Error: unknown option '" + opt + "'");
return -1;
@@ -393,8 +437,9 @@
final long ident = Binder.clearCallingIdentity();
try {
- int ret = mInternal.getTransferredNetworkBytes(pw, pkgName, userId, jobId, byteOption);
- printError(ret, pkgName, userId, jobId);
+ int ret = mInternal.getTransferredNetworkBytes(pw, pkgName, userId, namespace,
+ jobId, byteOption);
+ printError(ret, pkgName, userId, namespace, jobId);
return ret;
} finally {
Binder.restoreCallingIdentity(ident);
@@ -405,6 +450,7 @@
checkPermission("get job state");
int userId = UserHandle.USER_SYSTEM;
+ String namespace = null;
String opt;
while ((opt = getNextOption()) != null) {
@@ -414,6 +460,11 @@
userId = UserHandle.parseUserArg(getNextArgRequired());
break;
+ case "-n":
+ case "--namespace":
+ namespace = getNextArgRequired();
+ break;
+
default:
pw.println("Error: unknown option '" + opt + "'");
return -1;
@@ -430,8 +481,8 @@
final long ident = Binder.clearCallingIdentity();
try {
- int ret = mInternal.getJobState(pw, pkgName, userId, jobId);
- printError(ret, pkgName, userId, jobId);
+ int ret = mInternal.getJobState(pw, pkgName, userId, namespace, jobId);
+ printError(ret, pkgName, userId, namespace, jobId);
return ret;
} finally {
Binder.restoreCallingIdentity(ident);
@@ -521,7 +572,8 @@
pw.println("Job scheduler (jobscheduler) commands:");
pw.println(" help");
pw.println(" Print this help text.");
- pw.println(" run [-f | --force] [-s | --satisfied] [-u | --user USER_ID] PACKAGE JOB_ID");
+ pw.println(" run [-f | --force] [-s | --satisfied] [-u | --user USER_ID]"
+ + " [-n | --namespace NAMESPACE] PACKAGE JOB_ID");
pw.println(" Trigger immediate execution of a specific scheduled job. For historical");
pw.println(" reasons, some constraints, such as battery, are ignored when this");
pw.println(" command is called. If you don't want any constraints to be ignored,");
@@ -530,23 +582,30 @@
pw.println(" -f or --force: run the job even if technical constraints such as");
pw.println(" connectivity are not currently met. This is incompatible with -f ");
pw.println(" and so an error will be reported if both are given.");
+ pw.println(" -n or --namespace: specify the namespace this job sits in; the default");
+ pw.println(" is null (no namespace).");
pw.println(" -s or --satisfied: run the job only if all constraints are met.");
pw.println(" This is incompatible with -f and so an error will be reported");
pw.println(" if both are given.");
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(" timeout [-u | --user USER_ID] [PACKAGE] [JOB_ID]");
+ pw.println(" timeout [-u | --user USER_ID] [-n | --namespace NAMESPACE]"
+ + " [PACKAGE] [JOB_ID]");
pw.println(" Trigger immediate timeout of currently executing jobs, as if their.");
pw.println(" execution timeout had expired.");
pw.println(" Options:");
pw.println(" -u or --user: specify which user's job is to be run; the default is");
pw.println(" all users");
- pw.println(" cancel [-u | --user USER_ID] PACKAGE [JOB_ID]");
+ pw.println(" -n or --namespace: specify the namespace this job sits in; the default");
+ pw.println(" is null (no namespace).");
+ pw.println(" cancel [-u | --user USER_ID] [-n | --namespace NAMESPACE] PACKAGE [JOB_ID]");
pw.println(" Cancel a scheduled job. If a job ID is not supplied, all jobs scheduled");
pw.println(" by that package will be canceled. USE WITH CAUTION.");
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(" -n or --namespace: specify the namespace this job sits in; the default");
+ pw.println(" is null (no namespace).");
pw.println(" heartbeat [num]");
pw.println(" No longer used.");
pw.println(" monitor-battery [on|off]");
@@ -558,12 +617,14 @@
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(" get-estimated-download-bytes [-u | --user USER_ID]"
+ + " [-n | --namespace NAMESPACE] 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(" get-estimated-upload-bytes [-u | --user USER_ID]"
+ + " [-n | --namespace NAMESPACE] 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");
@@ -572,17 +633,20 @@
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(" get-transferred-download-bytes [-u | --user USER_ID]"
+ + " [-n | --namespace NAMESPACE] 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(" get-transferred-upload-bytes [-u | --user USER_ID]"
+ + " [-n | --namespace NAMESPACE] 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(" get-job-state [-u | --user USER_ID] [-n | --namespace NAMESPACE]"
+ + " 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");
pw.println(" active: job is actively running");
@@ -594,6 +658,8 @@
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(" -n or --namespace: specify the namespace this job sits in; the default");
+ pw.println(" is null (no namespace).");
pw.println(" trigger-dock-state [idle|active]");
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 15fc3c9..ce7da86 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
@@ -64,6 +64,8 @@
import com.android.server.tare.EconomyManagerInternal;
import com.android.server.tare.JobSchedulerEconomicPolicy;
+import java.util.Objects;
+
/**
* Handles client binding and lifecycle of a job. Jobs execute one at a time on an instance of this
* class.
@@ -304,7 +306,8 @@
job.changedAuthorities.toArray(triggeredAuthorities);
}
final JobInfo ji = job.getJob();
- mParams = new JobParameters(mRunningCallback, job.getJobId(), ji.getExtras(),
+ mParams = new JobParameters(mRunningCallback, job.getNamespace(), job.getJobId(),
+ ji.getExtras(),
ji.getTransientExtras(), ji.getClipData(), ji.getClipGrantFlags(),
isDeadlineExpired, job.shouldTreatAsExpeditedJob(),
job.shouldTreatAsUserInitiatedJob(), triggeredUris, triggeredAuthorities,
@@ -518,11 +521,12 @@
}
@GuardedBy("mLock")
- boolean timeoutIfExecutingLocked(String pkgName, int userId, boolean matchJobId, int jobId,
- String reason) {
+ boolean timeoutIfExecutingLocked(String pkgName, int userId, @Nullable String namespace,
+ boolean matchJobId, int jobId, String reason) {
final JobStatus executing = getRunningJobLocked();
if (executing != null && (userId == UserHandle.USER_ALL || userId == executing.getUserId())
&& (pkgName == null || pkgName.equals(executing.getSourcePackageName()))
+ && Objects.equals(namespace, executing.getNamespace())
&& (!matchJobId || jobId == executing.getJobId())) {
if (mVerb == VERB_EXECUTING) {
mParams.setStopReason(JobParameters.STOP_REASON_TIMEOUT,
@@ -611,6 +615,9 @@
"last work dequeued");
// This will finish the job.
doCallbackLocked(false, "last work dequeued");
+ } else {
+ // Delivery count has been updated, so persist JobWorkItem change.
+ mService.mJobs.touchJob(mRunningJob);
}
return work;
}
@@ -628,6 +635,7 @@
// Exception-throwing-can down the road to JobParameters.completeWork >:(
return true;
}
+ mService.mJobs.touchJob(mRunningJob);
return mRunningJob.completeWorkLocked(workId);
}
} finally {
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
index a1153e3..88270494 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
@@ -25,6 +25,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.job.JobInfo;
+import android.app.job.JobWorkItem;
import android.content.ComponentName;
import android.content.Context;
import android.net.NetworkRequest;
@@ -66,6 +67,7 @@
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
+import java.util.Objects;
import java.util.Set;
import java.util.StringJoiner;
import java.util.function.Consumer;
@@ -309,6 +311,15 @@
mJobSet.removeJobsOfUnlistedUsers(keepUserIds);
}
+ /** Note a change in the specified JobStatus that necessitates writing job state to disk. */
+ void touchJob(@NonNull JobStatus jobStatus) {
+ if (!jobStatus.isPersisted()) {
+ return;
+ }
+ mPendingJobWriteUids.put(jobStatus.getUid(), true);
+ maybeWriteStatusToDiskAsync();
+ }
+
@VisibleForTesting
public void clear() {
mJobSet.clear();
@@ -386,8 +397,8 @@
* @return the JobStatus that matches the provided uId and jobId, or null if none found.
*/
@Nullable
- public JobStatus getJobByUidAndJobId(int uid, int jobId) {
- return mJobSet.get(uid, jobId);
+ public JobStatus getJobByUidAndJobId(int uid, @Nullable String namespace, int jobId) {
+ return mJobSet.get(uid, namespace, jobId);
}
/**
@@ -429,6 +440,7 @@
private static final String XML_TAG_PERIODIC = "periodic";
private static final String XML_TAG_ONEOFF = "one-off";
private static final String XML_TAG_EXTRAS = "extras";
+ private static final String XML_TAG_JOB_WORK_ITEM = "job-work-item";
private void migrateJobFilesAsync() {
synchronized (mLock) {
@@ -723,6 +735,7 @@
writeConstraintsToXml(out, jobStatus);
writeExecutionCriteriaToXml(out, jobStatus);
writeBundleToXml(jobStatus.getJob().getExtras(), out);
+ writeJobWorkItemsToXml(out, jobStatus);
out.endTag(null, XML_TAG_JOB);
numJobs++;
@@ -764,6 +777,9 @@
if (jobStatus.getSourcePackageName() != null) {
out.attribute(null, "sourcePackageName", jobStatus.getSourcePackageName());
}
+ if (jobStatus.getNamespace() != null) {
+ out.attribute(null, "namespace", jobStatus.getNamespace());
+ }
if (jobStatus.getSourceTag() != null) {
out.attribute(null, "sourceTag", jobStatus.getSourceTag());
}
@@ -902,6 +918,53 @@
out.endTag(null, XML_TAG_ONEOFF);
}
}
+
+ private void writeJobWorkItemsToXml(@NonNull TypedXmlSerializer out,
+ @NonNull JobStatus jobStatus) throws IOException, XmlPullParserException {
+ // Write executing first since they're technically at the front of the queue.
+ writeJobWorkItemListToXml(out, jobStatus.executingWork);
+ writeJobWorkItemListToXml(out, jobStatus.pendingWork);
+ }
+
+ private void writeJobWorkItemListToXml(@NonNull TypedXmlSerializer out,
+ @Nullable List<JobWorkItem> jobWorkItems)
+ throws IOException, XmlPullParserException {
+ if (jobWorkItems == null) {
+ return;
+ }
+ // Write the items in list order to maintain the enqueue order.
+ final int size = jobWorkItems.size();
+ for (int i = 0; i < size; ++i) {
+ final JobWorkItem item = jobWorkItems.get(i);
+ if (item.getGrants() != null) {
+ // We currently don't allow persisting jobs when grants are involved.
+ // TODO(256618122): allow persisting JobWorkItems with grant flags
+ continue;
+ }
+ if (item.getIntent() != null) {
+ // Intent.saveToXml() doesn't persist everything, so we shouldn't attempt to
+ // persist these JobWorkItems at all.
+ Slog.wtf(TAG, "Encountered JobWorkItem with Intent in persisting list");
+ continue;
+ }
+ out.startTag(null, XML_TAG_JOB_WORK_ITEM);
+ out.attributeInt(null, "delivery-count", item.getDeliveryCount());
+ if (item.getEstimatedNetworkDownloadBytes() != JobInfo.NETWORK_BYTES_UNKNOWN) {
+ out.attributeLong(null, "estimated-download-bytes",
+ item.getEstimatedNetworkDownloadBytes());
+ }
+ if (item.getEstimatedNetworkUploadBytes() != JobInfo.NETWORK_BYTES_UNKNOWN) {
+ out.attributeLong(null, "estimated-upload-bytes",
+ item.getEstimatedNetworkUploadBytes());
+ }
+ if (item.getMinimumNetworkChunkBytes() != JobInfo.NETWORK_BYTES_UNKNOWN) {
+ out.attributeLong(null, "minimum-network-chunk-bytes",
+ item.getMinimumNetworkChunkBytes());
+ }
+ writeBundleToXml(item.getExtras(), out);
+ out.endTag(null, XML_TAG_JOB_WORK_ITEM);
+ }
+ }
};
/**
@@ -1135,6 +1198,7 @@
}
String sourcePackageName = parser.getAttributeValue(null, "sourcePackageName");
+ final String namespace = parser.getAttributeValue(null, "namespace");
final String sourceTag = parser.getAttributeValue(null, "sourceTag");
int eventType;
@@ -1257,7 +1321,13 @@
Slog.e(TAG, "Persisted extras contained invalid data", e);
return null;
}
- parser.nextTag(); // Consume </extras>
+ eventType = parser.nextTag(); // Consume </extras>
+
+ List<JobWorkItem> jobWorkItems = null;
+ if (eventType == XmlPullParser.START_TAG
+ && XML_TAG_JOB_WORK_ITEM.equals(parser.getName())) {
+ jobWorkItems = readJobWorkItemsFromXml(parser);
+ }
final JobInfo builtJob;
try {
@@ -1292,10 +1362,15 @@
sourceUserId, nowElapsed);
JobStatus js = new JobStatus(
builtJob, uid, sourcePackageName, sourceUserId,
- appBucket, sourceTag,
+ appBucket, namespace, sourceTag,
elapsedRuntimes.first, elapsedRuntimes.second,
lastSuccessfulRunTime, lastFailedRunTime,
(rtcIsGood) ? null : rtcRuntimes, internalFlags, /* dynamicConstraints */ 0);
+ if (jobWorkItems != null) {
+ for (int i = 0; i < jobWorkItems.size(); ++i) {
+ js.enqueueWorkLocked(jobWorkItems.get(i));
+ }
+ }
return js;
}
@@ -1468,6 +1543,64 @@
parser.getAttributeLong(null, "deadline", JobStatus.NO_LATEST_RUNTIME);
return Pair.create(earliestRunTimeRtc, latestRunTimeRtc);
}
+
+ @NonNull
+ private List<JobWorkItem> readJobWorkItemsFromXml(TypedXmlPullParser parser)
+ throws IOException, XmlPullParserException {
+ List<JobWorkItem> jobWorkItems = new ArrayList<>();
+
+ for (int eventType = parser.getEventType(); eventType != XmlPullParser.END_DOCUMENT;
+ eventType = parser.next()) {
+ final String tagName = parser.getName();
+ if (!XML_TAG_JOB_WORK_ITEM.equals(tagName)) {
+ // We're no longer operating with work items.
+ break;
+ }
+ try {
+ JobWorkItem jwi = readJobWorkItemFromXml(parser);
+ if (jwi != null) {
+ jobWorkItems.add(jwi);
+ }
+ } catch (Exception e) {
+ // If there's an issue with one JobWorkItem, drop only the one item and not the
+ // whole job.
+ Slog.e(TAG, "Problem with persisted JobWorkItem", e);
+ }
+ }
+
+ return jobWorkItems;
+ }
+
+ @Nullable
+ private JobWorkItem readJobWorkItemFromXml(TypedXmlPullParser parser)
+ throws IOException, XmlPullParserException {
+ JobWorkItem.Builder jwiBuilder = new JobWorkItem.Builder();
+
+ jwiBuilder
+ .setDeliveryCount(parser.getAttributeInt(null, "delivery-count"))
+ .setEstimatedNetworkBytes(
+ parser.getAttributeLong(null,
+ "estimated-download-bytes", JobInfo.NETWORK_BYTES_UNKNOWN),
+ parser.getAttributeLong(null,
+ "estimated-upload-bytes", JobInfo.NETWORK_BYTES_UNKNOWN))
+ .setMinimumNetworkChunkBytes(parser.getAttributeLong(null,
+ "minimum-network-chunk-bytes", JobInfo.NETWORK_BYTES_UNKNOWN));
+ parser.next();
+ try {
+ final PersistableBundle extras = PersistableBundle.restoreFromXml(parser);
+ jwiBuilder.setExtras(extras);
+ } catch (IllegalArgumentException e) {
+ Slog.e(TAG, "Persisted extras contained invalid data", e);
+ return null;
+ }
+
+ try {
+ return jwiBuilder.build();
+ } catch (Exception e) {
+ Slog.e(TAG, "Invalid JobWorkItem", e);
+ return null;
+ }
+ }
}
/** Set of all tracked jobs. */
@@ -1592,12 +1725,12 @@
return jobs != null && jobs.contains(job);
}
- public JobStatus get(int uid, int jobId) {
+ public JobStatus get(int uid, @Nullable String namespace, int jobId) {
ArraySet<JobStatus> jobs = mJobs.get(uid);
if (jobs != null) {
for (int i = jobs.size() - 1; i >= 0; i--) {
JobStatus job = jobs.valueAt(i);
- if (job.getJobId() == jobId) {
+ if (job.getJobId() == jobId && Objects.equals(namespace, job.getNamespace())) {
return job;
}
}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/PendingJobQueue.java b/apex/jobscheduler/service/java/com/android/server/job/PendingJobQueue.java
index 0eacfd6..36a26f0 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/PendingJobQueue.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/PendingJobQueue.java
@@ -28,6 +28,7 @@
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
+import java.util.Objects;
import java.util.PriorityQueue;
/**
@@ -280,12 +281,14 @@
return job1EJ ? -1 : 1;
}
- final int job1Priority = job1.getEffectivePriority();
- final int job2Priority = job2.getEffectivePriority();
- if (job1Priority != job2Priority) {
- // Use the priority set by an app for intra-app job ordering. Higher
- // priority should be before lower priority.
- return Integer.compare(job2Priority, job1Priority);
+ if (Objects.equals(job1.getNamespace(), job2.getNamespace())) {
+ final int job1Priority = job1.getEffectivePriority();
+ final int job2Priority = job2.getEffectivePriority();
+ if (job1Priority != job2Priority) {
+ // Use the priority set by an app for intra-app job ordering. Higher
+ // priority should be before lower priority.
+ return Integer.compare(job2Priority, job1Priority);
+ }
}
if (job1.lastEvaluatedBias != job2.lastEvaluatedBias) {
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 9b6186e..b0b6a01 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
@@ -30,6 +30,7 @@
import android.annotation.ElapsedRealtimeLong;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.app.AppGlobals;
import android.app.job.JobInfo;
import android.app.job.JobParameters;
@@ -70,6 +71,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.Objects;
import java.util.function.Predicate;
/**
@@ -225,6 +227,8 @@
final int sourceUserId;
final int sourceUid;
final String sourceTag;
+ @Nullable
+ private final String mNamespace;
final String tag;
@@ -515,6 +519,7 @@
* @param standbyBucket The standby bucket that the source package is currently assigned to,
* cached here for speed of handling during runnability evaluations (and updated when bucket
* assignments are changed)
+ * @param namespace The custom namespace the app put this job in.
* @param tag A string associated with the job for debugging/logging purposes.
* @param numFailures Count of how many times this job has requested a reschedule because
* its work was not yet finished.
@@ -529,13 +534,15 @@
* @param internalFlags Non-API property flags about this job
*/
private JobStatus(JobInfo job, int callingUid, String sourcePackageName,
- int sourceUserId, int standbyBucket, String tag, int numFailures, int numSystemStops,
+ int sourceUserId, int standbyBucket, @Nullable String namespace, String tag,
+ int numFailures, int numSystemStops,
long earliestRunTimeElapsedMillis, long latestRunTimeElapsedMillis,
long lastSuccessfulRunTime, long lastFailedRunTime, int internalFlags,
int dynamicConstraints) {
this.job = job;
this.callingUid = callingUid;
this.standbyBucket = standbyBucket;
+ mNamespace = namespace;
int tempSourceUid = -1;
if (sourceUserId != -1 && sourcePackageName != null) {
@@ -658,7 +665,7 @@
public JobStatus(JobStatus jobStatus) {
this(jobStatus.getJob(), jobStatus.getUid(),
jobStatus.getSourcePackageName(), jobStatus.getSourceUserId(),
- jobStatus.getStandbyBucket(),
+ jobStatus.getStandbyBucket(), jobStatus.getNamespace(),
jobStatus.getSourceTag(), jobStatus.getNumFailures(), jobStatus.getNumSystemStops(),
jobStatus.getEarliestRunTime(), jobStatus.getLatestRunTimeElapsed(),
jobStatus.getLastSuccessfulRunTime(), jobStatus.getLastFailedRunTime(),
@@ -669,6 +676,12 @@
Slog.i(TAG, "Cloning job with persisted run times", new RuntimeException("here"));
}
}
+ if (jobStatus.executingWork != null && jobStatus.executingWork.size() > 0) {
+ executingWork = new ArrayList<>(jobStatus.executingWork);
+ }
+ if (jobStatus.pendingWork != null && jobStatus.pendingWork.size() > 0) {
+ pendingWork = new ArrayList<>(jobStatus.pendingWork);
+ }
}
/**
@@ -680,13 +693,13 @@
* standby bucket is whatever the OS thinks it should be at this moment.
*/
public JobStatus(JobInfo job, int callingUid, String sourcePkgName, int sourceUserId,
- int standbyBucket, String sourceTag,
+ int standbyBucket, @Nullable String namespace, String sourceTag,
long earliestRunTimeElapsedMillis, long latestRunTimeElapsedMillis,
long lastSuccessfulRunTime, long lastFailedRunTime,
Pair<Long, Long> persistedExecutionTimesUTC,
int innerFlags, int dynamicConstraints) {
this(job, callingUid, sourcePkgName, sourceUserId,
- standbyBucket,
+ standbyBucket, namespace,
sourceTag, /* numFailures */ 0, /* numSystemStops */ 0,
earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis,
lastSuccessfulRunTime, lastFailedRunTime, innerFlags, dynamicConstraints);
@@ -710,7 +723,7 @@
long lastSuccessfulRunTime, long lastFailedRunTime) {
this(rescheduling.job, rescheduling.getUid(),
rescheduling.getSourcePackageName(), rescheduling.getSourceUserId(),
- rescheduling.getStandbyBucket(),
+ rescheduling.getStandbyBucket(), rescheduling.getNamespace(),
rescheduling.getSourceTag(), numFailures, numSystemStops,
newEarliestRuntimeElapsedMillis,
newLatestRuntimeElapsedMillis,
@@ -727,7 +740,7 @@
* caller.
*/
public static JobStatus createFromJobInfo(JobInfo job, int callingUid, String sourcePkg,
- int sourceUserId, String tag) {
+ int sourceUserId, @Nullable String namespace, String tag) {
final long elapsedNow = sElapsedRealtimeClock.millis();
final long earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis;
if (job.isPeriodic()) {
@@ -749,7 +762,7 @@
int standbyBucket = JobSchedulerService.standbyBucketForPackage(jobPackage,
sourceUserId, elapsedNow);
return new JobStatus(job, callingUid, sourcePkg, sourceUserId,
- standbyBucket, tag, /* numFailures */ 0, /* numSystemStops */ 0,
+ standbyBucket, namespace, tag, /* numFailures */ 0, /* numSystemStops */ 0,
earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis,
0 /* lastSuccessfulRunTime */, 0 /* lastFailedRunTime */,
/*innerFlags=*/ 0, /* dynamicConstraints */ 0);
@@ -897,6 +910,12 @@
}
public void printUniqueId(PrintWriter pw) {
+ if (mNamespace != null) {
+ pw.print(mNamespace);
+ pw.print(":");
+ } else {
+ pw.print("#");
+ }
UserHandle.formatUid(pw, callingUid);
pw.print("/");
pw.print(job.getId());
@@ -1036,6 +1055,10 @@
return true;
}
+ public String getNamespace() {
+ return mNamespace;
+ }
+
public String getSourceTag() {
return sourceTag;
}
@@ -1362,7 +1385,8 @@
public UserVisibleJobSummary getUserVisibleJobSummary() {
if (mUserVisibleJobSummary == null) {
mUserVisibleJobSummary = new UserVisibleJobSummary(
- callingUid, getSourceUserId(), getSourcePackageName(), getJobId());
+ callingUid, getSourceUserId(), getSourcePackageName(),
+ getNamespace(), getJobId());
}
return mUserVisibleJobSummary;
}
@@ -1989,8 +2013,12 @@
return (sat & mRequiredConstraintsOfInterest) == mRequiredConstraintsOfInterest;
}
- public boolean matches(int uid, int jobId) {
- return this.job.getId() == jobId && this.callingUid == uid;
+ /**
+ * Returns true if the given parameters match this job's unique identifier.
+ */
+ public boolean matches(int uid, @Nullable String namespace, int jobId) {
+ return this.job.getId() == jobId && this.callingUid == uid
+ && Objects.equals(mNamespace, namespace);
}
@Override
@@ -1998,7 +2026,13 @@
StringBuilder sb = new StringBuilder(128);
sb.append("JobStatus{");
sb.append(Integer.toHexString(System.identityHashCode(this)));
- sb.append(" #");
+ if (mNamespace != null) {
+ sb.append(" ");
+ sb.append(mNamespace);
+ sb.append(":");
+ } else {
+ sb.append(" #");
+ }
UserHandle.formatUid(sb, callingUid);
sb.append("/");
sb.append(job.getId());
@@ -2087,6 +2121,9 @@
public String toShortString() {
StringBuilder sb = new StringBuilder();
sb.append(Integer.toHexString(System.identityHashCode(this)));
+ if (mNamespace != null) {
+ sb.append(" {").append(mNamespace).append("}");
+ }
sb.append(" #");
UserHandle.formatUid(sb, callingUid);
sb.append("/");
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 d837ae0..13a2a94 100644
--- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
+++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
@@ -1492,7 +1492,8 @@
return STANDBY_BUCKET_WORKING_SET;
}
- if (mInjector.hasExactAlarmPermission(packageName, UserHandle.getUid(userId, appId))) {
+ if (mInjector.shouldGetExactAlarmBucketElevation(packageName,
+ UserHandle.getUid(userId, appId))) {
return STANDBY_BUCKET_WORKING_SET;
}
}
@@ -2455,11 +2456,11 @@
pw.print(mNoteResponseEventForAllBroadcastSessions);
pw.println();
- pw.print(" mBroadcastResponseExemptedRoles");
+ pw.print(" mBroadcastResponseExemptedRoles=");
pw.print(mBroadcastResponseExemptedRoles);
pw.println();
- pw.print(" mBroadcastResponseExemptedPermissions");
+ pw.print(" mBroadcastResponseExemptedPermissions=");
pw.print(mBroadcastResponseExemptedPermissions);
pw.println();
@@ -2625,8 +2626,8 @@
return packageName.equals(mWellbeingApp);
}
- boolean hasExactAlarmPermission(String packageName, int uid) {
- return mAlarmManagerInternal.hasExactAlarmPermission(packageName, uid);
+ boolean shouldGetExactAlarmBucketElevation(String packageName, int uid) {
+ return mAlarmManagerInternal.shouldGetBucketElevation(packageName, uid);
}
void updatePowerWhitelistCache() {
diff --git a/core/api/current.txt b/core/api/current.txt
index 0aed10c..2f916f1 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -935,6 +935,8 @@
field @Deprecated public static final int keyTextSize = 16843316; // 0x1010234
field @Deprecated public static final int keyWidth = 16843325; // 0x101023d
field public static final int keyboardLayout = 16843691; // 0x10103ab
+ field public static final int keyboardLayoutType;
+ field public static final int keyboardLocale;
field @Deprecated public static final int keyboardMode = 16843341; // 0x101024d
field public static final int keyboardNavigationCluster = 16844096; // 0x1010540
field public static final int keycode = 16842949; // 0x10100c5
@@ -5650,6 +5652,11 @@
field public static final int MODE_UNKNOWN = 0; // 0x0
}
+ public class GrammaticalInflectionManager {
+ method public int getApplicationGrammaticalGender();
+ method public void setRequestedApplicationGrammaticalGender(int);
+ }
+
public class Instrumentation {
ctor public Instrumentation();
method public android.os.TestLooperManager acquireLooperManager(android.os.Looper);
@@ -8588,12 +8595,22 @@
method public int getDeliveryCount();
method public long getEstimatedNetworkDownloadBytes();
method public long getEstimatedNetworkUploadBytes();
+ method @NonNull public android.os.PersistableBundle getExtras();
method public android.content.Intent getIntent();
method public long getMinimumNetworkChunkBytes();
method public void writeToParcel(android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.app.job.JobWorkItem> CREATOR;
}
+ public static final class JobWorkItem.Builder {
+ ctor public JobWorkItem.Builder();
+ method @NonNull public android.app.job.JobWorkItem build();
+ method @NonNull public android.app.job.JobWorkItem.Builder setEstimatedNetworkBytes(long, long);
+ method @NonNull public android.app.job.JobWorkItem.Builder setExtras(@NonNull android.os.PersistableBundle);
+ method @NonNull public android.app.job.JobWorkItem.Builder setIntent(@NonNull android.content.Intent);
+ method @NonNull public android.app.job.JobWorkItem.Builder setMinimumNetworkChunkBytes(long);
+ }
+
}
package android.app.people {
@@ -10005,6 +10022,7 @@
field public static final String FILE_INTEGRITY_SERVICE = "file_integrity";
field public static final String FINGERPRINT_SERVICE = "fingerprint";
field public static final String GAME_SERVICE = "game";
+ field public static final String GRAMMATICAL_INFLECTION_SERVICE = "grammatical_inflection";
field public static final String HARDWARE_PROPERTIES_SERVICE = "hardware_properties";
field public static final String HEALTHCONNECT_SERVICE = "healthconnect";
field public static final String INPUT_METHOD_SERVICE = "input_method";
@@ -10033,6 +10051,7 @@
field public static final String NFC_SERVICE = "nfc";
field public static final String NOTIFICATION_SERVICE = "notification";
field public static final String NSD_SERVICE = "servicediscovery";
+ field public static final String OVERLAY_SERVICE = "overlay";
field public static final String PEOPLE_SERVICE = "people";
field public static final String PERFORMANCE_HINT_SERVICE = "performance_hint";
field public static final String POWER_SERVICE = "power";
@@ -11201,6 +11220,49 @@
}
+package android.content.om {
+
+ public class FabricatedOverlay {
+ ctor public FabricatedOverlay(@NonNull String, @NonNull String);
+ method @NonNull public android.content.om.OverlayIdentifier getIdentifier();
+ method @NonNull public void setResourceValue(@NonNull String, @IntRange(from=android.util.TypedValue.TYPE_FIRST_INT, to=android.util.TypedValue.TYPE_LAST_INT) int, int, @Nullable String);
+ method @NonNull public void setResourceValue(@NonNull String, int, @NonNull String, @Nullable String);
+ method @NonNull public void setResourceValue(@NonNull String, @NonNull android.os.ParcelFileDescriptor, @Nullable String);
+ method public void setTargetOverlayable(@Nullable String);
+ }
+
+ public final class OverlayIdentifier implements android.os.Parcelable {
+ method public int describeContents();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.content.om.OverlayIdentifier> CREATOR;
+ }
+
+ public final class OverlayInfo implements android.os.Parcelable {
+ method public int describeContents();
+ method @NonNull public android.content.om.OverlayIdentifier getOverlayIdentifier();
+ method @Nullable public String getOverlayName();
+ method @Nullable public String getTargetOverlayableName();
+ method @NonNull public String getTargetPackageName();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.content.om.OverlayInfo> CREATOR;
+ }
+
+ public class OverlayManager {
+ method @NonNull @NonUiContext public java.util.List<android.content.om.OverlayInfo> getOverlayInfosForTarget(@NonNull String);
+ }
+
+ public final class OverlayManagerTransaction implements android.os.Parcelable {
+ ctor public OverlayManagerTransaction(@NonNull android.content.om.OverlayManager);
+ method @NonUiContext public void commit() throws java.io.IOException, android.content.pm.PackageManager.NameNotFoundException;
+ method public int describeContents();
+ method @NonNull public void registerFabricatedOverlay(@NonNull android.content.om.FabricatedOverlay);
+ method @NonNull public void unregisterFabricatedOverlay(@NonNull android.content.om.OverlayIdentifier);
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.content.om.OverlayManagerTransaction> CREATOR;
+ }
+
+}
+
package android.content.pm {
public class ActivityInfo extends android.content.pm.ComponentInfo implements android.os.Parcelable {
@@ -11217,6 +11279,7 @@
field public static final int CONFIG_DENSITY = 4096; // 0x1000
field public static final int CONFIG_FONT_SCALE = 1073741824; // 0x40000000
field public static final int CONFIG_FONT_WEIGHT_ADJUSTMENT = 268435456; // 0x10000000
+ field public static final int CONFIG_GRAMMATICAL_GENDER = 32768; // 0x8000
field public static final int CONFIG_KEYBOARD = 16; // 0x10
field public static final int CONFIG_KEYBOARD_HIDDEN = 32; // 0x20
field public static final int CONFIG_LAYOUT_DIRECTION = 8192; // 0x2000
@@ -11765,6 +11828,7 @@
method public void registerSessionCallback(@NonNull android.content.pm.PackageInstaller.SessionCallback, @NonNull android.os.Handler);
method @RequiresPermission(anyOf={android.Manifest.permission.DELETE_PACKAGES, android.Manifest.permission.REQUEST_DELETE_PACKAGES}) public void uninstall(@NonNull String, @NonNull android.content.IntentSender);
method @RequiresPermission(anyOf={android.Manifest.permission.DELETE_PACKAGES, android.Manifest.permission.REQUEST_DELETE_PACKAGES}) public void uninstall(@NonNull android.content.pm.VersionedPackage, @NonNull android.content.IntentSender);
+ method @RequiresPermission(anyOf={android.Manifest.permission.DELETE_PACKAGES, android.Manifest.permission.REQUEST_DELETE_PACKAGES}) public void uninstall(@NonNull android.content.pm.VersionedPackage, int, @NonNull android.content.IntentSender);
method @RequiresPermission(android.Manifest.permission.DELETE_PACKAGES) public void uninstallExistingPackage(@NonNull String, @Nullable android.content.IntentSender);
method public void unregisterSessionCallback(@NonNull android.content.pm.PackageInstaller.SessionCallback);
method public void updateSessionAppIcon(int, @Nullable android.graphics.Bitmap);
@@ -11893,6 +11957,8 @@
method public int getInstallReason();
method @Nullable public String getInstallerAttributionTag();
method @Nullable public String getInstallerPackageName();
+ method public int getInstallerUid();
+ method @NonNull public boolean getIsPreApprovalRequested();
method public int getMode();
method public int getOriginatingUid();
method @Nullable public android.net.Uri getOriginatingUri();
@@ -11943,6 +12009,7 @@
method public void setInstallLocation(int);
method public void setInstallReason(int);
method public void setInstallScenario(int);
+ method public void setInstallerPackageName(@Nullable String);
method public void setKeepApplicationEnabledSetting();
method public void setMultiPackage();
method public void setOriginatingUid(int);
@@ -12203,6 +12270,7 @@
field public static final String FEATURE_IDENTITY_CREDENTIAL_HARDWARE_DIRECT_ACCESS = "android.hardware.identity_credential_direct_access";
field public static final String FEATURE_INPUT_METHODS = "android.software.input_methods";
field public static final String FEATURE_IPSEC_TUNNELS = "android.software.ipsec_tunnels";
+ field public static final String FEATURE_IPSEC_TUNNEL_MIGRATION = "android.software.ipsec_tunnel_migration";
field public static final String FEATURE_IRIS = "android.hardware.biometrics.iris";
field public static final String FEATURE_KEYSTORE_APP_ATTEST_KEY = "android.hardware.keystore.app_attest_key";
field public static final String FEATURE_KEYSTORE_LIMITED_USE_KEY = "android.hardware.keystore.limited_use_key";
@@ -12831,6 +12899,7 @@
method public int diff(android.content.res.Configuration);
method public boolean equals(android.content.res.Configuration);
method @NonNull public static android.content.res.Configuration generateDelta(@NonNull android.content.res.Configuration, @NonNull android.content.res.Configuration);
+ method public int getGrammaticalGender();
method public int getLayoutDirection();
method @NonNull public android.os.LocaleList getLocales();
method public boolean isLayoutSizeAtLeast(int);
@@ -12860,6 +12929,10 @@
field @NonNull public static final android.os.Parcelable.Creator<android.content.res.Configuration> CREATOR;
field public static final int DENSITY_DPI_UNDEFINED = 0; // 0x0
field public static final int FONT_WEIGHT_ADJUSTMENT_UNDEFINED = 2147483647; // 0x7fffffff
+ field public static final int GRAMMATICAL_GENDER_FEMININE = 3; // 0x3
+ field public static final int GRAMMATICAL_GENDER_MASCULINE = 4; // 0x4
+ field public static final int GRAMMATICAL_GENDER_NEUTRAL = 2; // 0x2
+ field public static final int GRAMMATICAL_GENDER_NOT_SPECIFIED = 0; // 0x0
field public static final int HARDKEYBOARDHIDDEN_NO = 1; // 0x1
field public static final int HARDKEYBOARDHIDDEN_UNDEFINED = 0; // 0x0
field public static final int HARDKEYBOARDHIDDEN_YES = 2; // 0x2
@@ -13107,6 +13180,7 @@
method @NonNull public static android.content.res.loader.ResourcesProvider loadFromDirectory(@NonNull String, @Nullable android.content.res.loader.AssetsProvider) throws java.io.IOException;
method @NonNull public static android.content.res.loader.ResourcesProvider loadFromSplit(@NonNull android.content.Context, @NonNull String) throws java.io.IOException;
method @NonNull public static android.content.res.loader.ResourcesProvider loadFromTable(@NonNull android.os.ParcelFileDescriptor, @Nullable android.content.res.loader.AssetsProvider) throws java.io.IOException;
+ method @NonNull public static android.content.res.loader.ResourcesProvider loadOverlay(@NonNull android.content.om.OverlayInfo) throws java.io.IOException;
}
}
@@ -13192,13 +13266,14 @@
public final class GetCredentialRequest implements android.os.Parcelable {
method public int describeContents();
+ method @NonNull public android.os.Bundle getData();
method @NonNull public java.util.List<android.credentials.GetCredentialOption> getGetCredentialOptions();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.credentials.GetCredentialRequest> CREATOR;
}
public static final class GetCredentialRequest.Builder {
- ctor public GetCredentialRequest.Builder();
+ ctor public GetCredentialRequest.Builder(@NonNull android.os.Bundle);
method @NonNull public android.credentials.GetCredentialRequest.Builder addGetCredentialOption(@NonNull android.credentials.GetCredentialOption);
method @NonNull public android.credentials.GetCredentialRequest build();
method @NonNull public android.credentials.GetCredentialRequest.Builder setGetCredentialOptions(@NonNull java.util.List<android.credentials.GetCredentialOption>);
@@ -14648,6 +14723,7 @@
method public void drawLine(float, float, float, float, @NonNull android.graphics.Paint);
method public void drawLines(@NonNull @Size(multiple=4) float[], int, int, @NonNull android.graphics.Paint);
method public void drawLines(@NonNull @Size(multiple=4) float[], @NonNull android.graphics.Paint);
+ method public void drawMesh(@NonNull android.graphics.Mesh, android.graphics.BlendMode, @NonNull android.graphics.Paint);
method public void drawOval(@NonNull android.graphics.RectF, @NonNull android.graphics.Paint);
method public void drawOval(float, float, float, float, @NonNull android.graphics.Paint);
method public void drawPaint(@NonNull android.graphics.Paint);
@@ -14880,6 +14956,8 @@
enum_constant public static final android.graphics.ColorSpace.Named ACESCG;
enum_constant public static final android.graphics.ColorSpace.Named ADOBE_RGB;
enum_constant public static final android.graphics.ColorSpace.Named BT2020;
+ enum_constant public static final android.graphics.ColorSpace.Named BT2020_HLG;
+ enum_constant public static final android.graphics.ColorSpace.Named BT2020_PQ;
enum_constant public static final android.graphics.ColorSpace.Named BT709;
enum_constant public static final android.graphics.ColorSpace.Named CIE_LAB;
enum_constant public static final android.graphics.ColorSpace.Named CIE_XYZ;
@@ -15223,6 +15301,49 @@
enum_constant public static final android.graphics.Matrix.ScaleToFit START;
}
+ public class Mesh {
+ method @NonNull public static android.graphics.Mesh make(@NonNull android.graphics.MeshSpecification, int, @NonNull java.nio.Buffer, int, @NonNull android.graphics.Rect);
+ method @NonNull public static android.graphics.Mesh makeIndexed(@NonNull android.graphics.MeshSpecification, int, @NonNull java.nio.Buffer, int, @NonNull java.nio.ShortBuffer, @NonNull android.graphics.Rect);
+ method public void setColorUniform(@NonNull String, int);
+ method public void setColorUniform(@NonNull String, long);
+ method public void setColorUniform(@NonNull String, @NonNull android.graphics.Color);
+ method public void setFloatUniform(@NonNull String, float);
+ method public void setFloatUniform(@NonNull String, float, float);
+ method public void setFloatUniform(@NonNull String, float, float, float);
+ method public void setFloatUniform(@NonNull String, float, float, float, float);
+ method public void setFloatUniform(@NonNull String, @NonNull float[]);
+ method public void setIntUniform(@NonNull String, int);
+ method public void setIntUniform(@NonNull String, int, int);
+ method public void setIntUniform(@NonNull String, int, int, int);
+ method public void setIntUniform(@NonNull String, int, int, int, int);
+ method public void setIntUniform(@NonNull String, @NonNull int[]);
+ field public static final int TRIANGLES = 0; // 0x0
+ field public static final int TRIANGLE_STRIP = 1; // 0x1
+ }
+
+ public class MeshSpecification {
+ method @NonNull public static android.graphics.MeshSpecification make(@NonNull java.util.List<android.graphics.MeshSpecification.Attribute>, int, @NonNull java.util.List<android.graphics.MeshSpecification.Varying>, @NonNull String, @NonNull String);
+ method @NonNull public static android.graphics.MeshSpecification make(@NonNull java.util.List<android.graphics.MeshSpecification.Attribute>, int, @NonNull java.util.List<android.graphics.MeshSpecification.Varying>, @NonNull String, @NonNull String, @NonNull android.graphics.ColorSpace);
+ method @NonNull public static android.graphics.MeshSpecification make(@NonNull java.util.List<android.graphics.MeshSpecification.Attribute>, int, @NonNull java.util.List<android.graphics.MeshSpecification.Varying>, @NonNull String, @NonNull String, @NonNull android.graphics.ColorSpace, int);
+ field public static final int FLOAT = 0; // 0x0
+ field public static final int FLOAT2 = 1; // 0x1
+ field public static final int FLOAT3 = 2; // 0x2
+ field public static final int FLOAT4 = 3; // 0x3
+ field public static final int OPAQUE = 1; // 0x1
+ field public static final int PREMUL = 2; // 0x2
+ field public static final int UBYTE4 = 4; // 0x4
+ field public static final int UNKNOWN = 0; // 0x0
+ field public static final int UNPREMULT = 3; // 0x3
+ }
+
+ public static class MeshSpecification.Attribute {
+ ctor public MeshSpecification.Attribute(int, int, @NonNull String);
+ }
+
+ public static class MeshSpecification.Varying {
+ ctor public MeshSpecification.Varying(int, @NonNull String);
+ }
+
@Deprecated public class Movie {
method @Deprecated public static android.graphics.Movie decodeByteArray(byte[], int, int);
method @Deprecated public static android.graphics.Movie decodeFile(String);
@@ -15738,6 +15859,7 @@
}
public final class RecordingCanvas extends android.graphics.Canvas {
+ method public final void drawMesh(@NonNull android.graphics.Mesh, android.graphics.BlendMode, @NonNull android.graphics.Paint);
}
public final class Rect implements android.os.Parcelable {
@@ -17786,6 +17908,7 @@
method public void onCaptureSequenceAborted(@NonNull android.hardware.camera2.CameraCaptureSession, int);
method public void onCaptureSequenceCompleted(@NonNull android.hardware.camera2.CameraCaptureSession, int, long);
method public void onCaptureStarted(@NonNull android.hardware.camera2.CameraCaptureSession, @NonNull android.hardware.camera2.CaptureRequest, long, long);
+ method public void onReadoutStarted(@NonNull android.hardware.camera2.CameraCaptureSession, @NonNull android.hardware.camera2.CaptureRequest, long, long);
}
public abstract static class CameraCaptureSession.StateCallback {
@@ -18261,6 +18384,7 @@
field public static final int REQUEST_AVAILABLE_CAPABILITIES_SYSTEM_CAMERA = 14; // 0xe
field public static final int REQUEST_AVAILABLE_CAPABILITIES_ULTRA_HIGH_RESOLUTION_SENSOR = 16; // 0x10
field public static final int REQUEST_AVAILABLE_CAPABILITIES_YUV_REPROCESSING = 7; // 0x7
+ field public static final int SCALER_AVAILABLE_STREAM_USE_CASES_CROPPED_RAW = 6; // 0x6
field public static final int SCALER_AVAILABLE_STREAM_USE_CASES_DEFAULT = 0; // 0x0
field public static final int SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW = 1; // 0x1
field public static final int SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW_VIDEO_STILL = 4; // 0x4
@@ -18520,6 +18644,7 @@
field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Float> REPROCESS_EFFECTIVE_EXPOSURE_FACTOR;
field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Byte> REQUEST_PIPELINE_DEPTH;
field @NonNull public static final android.hardware.camera2.CaptureResult.Key<android.graphics.Rect> SCALER_CROP_REGION;
+ field @NonNull public static final android.hardware.camera2.CaptureResult.Key<android.graphics.Rect> SCALER_RAW_CROP_REGION;
field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> SCALER_ROTATE_AND_CROP;
field @NonNull public static final android.hardware.camera2.CaptureResult.Key<float[]> SENSOR_DYNAMIC_BLACK_LEVEL;
field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> SENSOR_DYNAMIC_WHITE_LEVEL;
@@ -18779,6 +18904,7 @@
method public int getSurfaceGroupId();
method @NonNull public java.util.List<android.view.Surface> getSurfaces();
method public int getTimestampBase();
+ method public boolean isReadoutTimestampUsed();
method public void removeSensorPixelModeUsed(int);
method public void removeSurface(@NonNull android.view.Surface);
method public void setDynamicRangeProfile(long);
@@ -18786,6 +18912,7 @@
method public void setPhysicalCameraId(@Nullable String);
method public void setStreamUseCase(long);
method public void setTimestampBase(int);
+ method public void useReadoutTimestamp(boolean);
method public void writeToParcel(android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.hardware.camera2.params.OutputConfiguration> CREATOR;
field public static final int MIRROR_MODE_AUTO = 0; // 0x0
@@ -19911,8 +20038,9 @@
method public int describeContents();
method @NonNull public android.location.GnssClock getClock();
method @NonNull public java.util.Collection<android.location.GnssAutomaticGainControl> getGnssAutomaticGainControls();
- method public boolean getIsFullTracking();
method @NonNull public java.util.Collection<android.location.GnssMeasurement> getMeasurements();
+ method public boolean hasFullTracking();
+ method public boolean isFullTracking();
method public void writeToParcel(android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.location.GnssMeasurementsEvent> CREATOR;
}
@@ -19921,9 +20049,10 @@
ctor public GnssMeasurementsEvent.Builder();
ctor public GnssMeasurementsEvent.Builder(@NonNull android.location.GnssMeasurementsEvent);
method @NonNull public android.location.GnssMeasurementsEvent build();
+ method @NonNull public android.location.GnssMeasurementsEvent.Builder clearFullTracking();
method @NonNull public android.location.GnssMeasurementsEvent.Builder setClock(@NonNull android.location.GnssClock);
+ method @NonNull public android.location.GnssMeasurementsEvent.Builder setFullTracking(boolean);
method @NonNull public android.location.GnssMeasurementsEvent.Builder setGnssAutomaticGainControls(@NonNull java.util.Collection<android.location.GnssAutomaticGainControl>);
- method @NonNull public android.location.GnssMeasurementsEvent.Builder setIsFullTracking(boolean);
method @NonNull public android.location.GnssMeasurementsEvent.Builder setMeasurements(@NonNull java.util.Collection<android.location.GnssMeasurement>);
}
@@ -20425,6 +20554,8 @@
field @NonNull public static final android.os.Parcelable.Creator<android.media.AudioDescriptor> CREATOR;
field public static final int STANDARD_EDID = 1; // 0x1
field public static final int STANDARD_NONE = 0; // 0x0
+ field public static final int STANDARD_SADB = 2; // 0x2
+ field public static final int STANDARD_VSADB = 3; // 0x3
}
public abstract class AudioDeviceCallback {
@@ -24138,6 +24269,7 @@
method public int getDisableReason();
method public int getFlags();
method @NonNull public String getRouteId();
+ method public int getSessionParticipantCount();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.media.RouteListingPreference.Item> CREATOR;
field public static final int DISABLE_REASON_AD = 3; // 0x3
@@ -24153,6 +24285,7 @@
method @NonNull public android.media.RouteListingPreference.Item build();
method @NonNull public android.media.RouteListingPreference.Item.Builder setDisableReason(int);
method @NonNull public android.media.RouteListingPreference.Item.Builder setFlags(int);
+ method @NonNull public android.media.RouteListingPreference.Item.Builder setSessionParticipantCount(@IntRange(from=0) int);
}
public final class RoutingSessionInfo implements android.os.Parcelable {
@@ -25544,6 +25677,7 @@
public abstract static class MediaProjection.Callback {
ctor public MediaProjection.Callback();
method public void onCapturedContentResize(int, int);
+ method public void onCapturedContentVisibilityChanged(boolean);
method public void onStop();
}
@@ -39361,6 +39495,8 @@
method public final boolean onUnbind(@NonNull android.content.Intent);
method public abstract void performControlAction(@NonNull String, @NonNull android.service.controls.actions.ControlAction, @NonNull java.util.function.Consumer<java.lang.Integer>);
method public static void requestAddControl(@NonNull android.content.Context, @NonNull android.content.ComponentName, @NonNull android.service.controls.Control);
+ field public static final String EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS = "android.service.controls.extra.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS";
+ field public static final String META_DATA_PANEL_ACTIVITY = "android.service.controls.META_DATA_PANEL_ACTIVITY";
field public static final String SERVICE_CONTROLS = "android.service.controls.ControlsProviderService";
field @NonNull public static final String TAG = "ControlsProviderService";
}
@@ -41038,6 +41174,32 @@
field public static final int RTT_MODE_VCO = 3; // 0x3
}
+ public final class CallAttributes implements android.os.Parcelable {
+ method public int describeContents();
+ method @NonNull public android.net.Uri getAddress();
+ method public int getCallCapabilities();
+ method public int getCallType();
+ method public int getDirection();
+ method @NonNull public CharSequence getDisplayName();
+ method @NonNull public android.telecom.PhoneAccountHandle getPhoneAccountHandle();
+ method public void writeToParcel(@Nullable android.os.Parcel, int);
+ field public static final int AUDIO_CALL = 1; // 0x1
+ field @NonNull public static final android.os.Parcelable.Creator<android.telecom.CallAttributes> CREATOR;
+ field public static final int DIRECTION_INCOMING = 1; // 0x1
+ field public static final int DIRECTION_OUTGOING = 2; // 0x2
+ field public static final int SUPPORTS_SET_INACTIVE = 2; // 0x2
+ field public static final int SUPPORTS_STREAM = 4; // 0x4
+ field public static final int SUPPORTS_TRANSFER = 8; // 0x8
+ field public static final int VIDEO_CALL = 2; // 0x2
+ }
+
+ public static final class CallAttributes.Builder {
+ ctor public CallAttributes.Builder(@NonNull android.telecom.PhoneAccountHandle, int, @NonNull CharSequence, @NonNull android.net.Uri);
+ method @NonNull public android.telecom.CallAttributes build();
+ method @NonNull public android.telecom.CallAttributes.Builder setCallCapabilities(int);
+ method @NonNull public android.telecom.CallAttributes.Builder setCallType(int);
+ }
+
public final class CallAudioState implements android.os.Parcelable {
ctor public CallAudioState(boolean, int, int);
method public static String audioRouteToString(int);
@@ -41052,10 +41214,21 @@
field public static final int ROUTE_BLUETOOTH = 2; // 0x2
field public static final int ROUTE_EARPIECE = 1; // 0x1
field public static final int ROUTE_SPEAKER = 8; // 0x8
+ field public static final int ROUTE_STREAMING = 16; // 0x10
field public static final int ROUTE_WIRED_HEADSET = 4; // 0x4
field public static final int ROUTE_WIRED_OR_EARPIECE = 5; // 0x5
}
+ public final class CallControl implements java.lang.AutoCloseable {
+ method public void close();
+ method public void disconnect(@NonNull android.telecom.DisconnectCause, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallException>);
+ method @NonNull public android.os.ParcelUuid getCallId();
+ method public void rejectCall(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallException>);
+ method public void setActive(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallException>);
+ method public void setInactive(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallException>);
+ method public void startCallStreaming(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallException>);
+ }
+
public final class CallEndpoint implements android.os.Parcelable {
ctor public CallEndpoint(@NonNull CharSequence, int, @NonNull android.os.ParcelUuid);
method public int describeContents();
@@ -41086,6 +41259,32 @@
field public static final int ERROR_UNSPECIFIED = 4; // 0x4
}
+ public interface CallEventCallback {
+ method public void onAnswer(int, @NonNull java.util.function.Consumer<java.lang.Boolean>);
+ method public void onCallAudioStateChanged(@NonNull android.telecom.CallAudioState);
+ method public void onCallStreamingFailed(int);
+ method public void onCallStreamingStarted(@NonNull java.util.function.Consumer<java.lang.Boolean>);
+ method public void onDisconnect(@NonNull java.util.function.Consumer<java.lang.Boolean>);
+ method public void onReject(@NonNull java.util.function.Consumer<java.lang.Boolean>);
+ method public void onSetActive(@NonNull java.util.function.Consumer<java.lang.Boolean>);
+ method public void onSetInactive(@NonNull java.util.function.Consumer<java.lang.Boolean>);
+ }
+
+ public final class CallException extends java.lang.RuntimeException implements android.os.Parcelable {
+ ctor public CallException(@Nullable String);
+ ctor public CallException(@Nullable String, int);
+ method public int describeContents();
+ method public int getCode();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field public static final int CODE_CALL_CANNOT_BE_SET_TO_ACTIVE = 4; // 0x4
+ field public static final int CODE_CALL_IS_NOT_BEING_TRACKED = 3; // 0x3
+ field public static final int CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME = 5; // 0x5
+ field public static final int CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL = 2; // 0x2
+ field public static final int CODE_ERROR_UNKNOWN = 1; // 0x1
+ field public static final int CODE_OPERATION_TIMED_OUT = 6; // 0x6
+ field @NonNull public static final android.os.Parcelable.Creator<android.telecom.CallException> CREATOR;
+ }
+
public abstract class CallRedirectionService extends android.app.Service {
ctor public CallRedirectionService();
method public final void cancelCall();
@@ -41130,6 +41329,18 @@
method public android.telecom.CallScreeningService.CallResponse.Builder setSkipNotification(boolean);
}
+ public abstract class CallStreamingService extends android.app.Service {
+ ctor public CallStreamingService();
+ method @Nullable public android.os.IBinder onBind(@NonNull android.content.Intent);
+ method public void onCallStreamingStarted(@NonNull android.telecom.StreamingCall);
+ method public void onCallStreamingStateChanged(int);
+ method public void onCallStreamingStopped();
+ field public static final String SERVICE_INTERFACE = "android.telecom.CallStreamingService";
+ field public static final int STREAMING_FAILED_ALREADY_STREAMING = 1; // 0x1
+ field public static final int STREAMING_FAILED_NO_SENDER = 2; // 0x2
+ field public static final int STREAMING_FAILED_SENDER_BINDING_ERROR = 3; // 0x3
+ }
+
public abstract class Conference extends android.telecom.Conferenceable {
ctor public Conference(android.telecom.PhoneAccountHandle);
method public final boolean addConnection(android.telecom.Connection);
@@ -41595,6 +41806,8 @@
field public static final int CAPABILITY_RTT = 4096; // 0x1000
field public static final int CAPABILITY_SELF_MANAGED = 2048; // 0x800
field public static final int CAPABILITY_SIM_SUBSCRIPTION = 4; // 0x4
+ field public static final int CAPABILITY_SUPPORTS_CALL_STREAMING = 524288; // 0x80000
+ field public static final int CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS = 262144; // 0x40000
field public static final int CAPABILITY_SUPPORTS_VIDEO_CALLING = 1024; // 0x400
field public static final int CAPABILITY_SUPPORTS_VOICE_CALLING_INDICATIONS = 65536; // 0x10000
field public static final int CAPABILITY_VIDEO_CALLING = 8; // 0x8
@@ -41783,10 +41996,27 @@
field @NonNull public static final android.os.Parcelable.Creator<android.telecom.StatusHints> CREATOR;
}
+ public final class StreamingCall implements android.os.Parcelable {
+ ctor public StreamingCall(@NonNull android.content.ComponentName, @NonNull String, @NonNull android.net.Uri, @NonNull android.os.Bundle);
+ method public int describeContents();
+ method @NonNull public android.net.Uri getAddress();
+ method @NonNull public android.content.ComponentName getComponentName();
+ method @NonNull public String getDisplayName();
+ method @NonNull public android.os.Bundle getExtras();
+ method public int getState();
+ method public void setStreamingState(int);
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.telecom.StreamingCall> CREATOR;
+ field public static final int STATE_DISCONNECTED = 3; // 0x3
+ field public static final int STATE_HOLDING = 2; // 0x2
+ field public static final int STATE_STREAMING = 1; // 0x1
+ }
+
public class TelecomManager {
method public void acceptHandover(android.net.Uri, int, android.telecom.PhoneAccountHandle);
method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.ANSWER_PHONE_CALLS, android.Manifest.permission.MODIFY_PHONE_STATE}) public void acceptRingingCall();
method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.ANSWER_PHONE_CALLS, android.Manifest.permission.MODIFY_PHONE_STATE}) public void acceptRingingCall(int);
+ method @RequiresPermission(android.Manifest.permission.MANAGE_OWN_CALLS) public void addCall(@NonNull android.telecom.CallAttributes, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.telecom.CallControl,android.telecom.CallException>, @NonNull android.telecom.CallEventCallback);
method public void addNewIncomingCall(android.telecom.PhoneAccountHandle, android.os.Bundle);
method public void addNewIncomingConference(@NonNull android.telecom.PhoneAccountHandle, @NonNull android.os.Bundle);
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void cancelMissedCallsNotification();
@@ -54259,7 +54489,7 @@
}
public abstract class HandwritingGesture {
- method @Nullable public String getFallbackText();
+ method @Nullable public final String getFallbackText();
field public static final int GESTURE_TYPE_DELETE = 4; // 0x4
field public static final int GESTURE_TYPE_DELETE_RANGE = 64; // 0x40
field public static final int GESTURE_TYPE_INSERT = 2; // 0x2
diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt
index adbbe61..2546294 100644
--- a/core/api/module-lib-current.txt
+++ b/core/api/module-lib-current.txt
@@ -302,6 +302,17 @@
}
+package android.net.wifi {
+
+ public final class WifiKeystore {
+ method @NonNull public static byte[] get(@NonNull String);
+ method @NonNull public static String[] list(@NonNull String);
+ method public static boolean put(@NonNull String, @NonNull byte[]);
+ method public static boolean remove(@NonNull String);
+ }
+
+}
+
package android.os {
public class ArtModuleServiceManager {
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 7105a4f..5f4c19b 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -1398,12 +1398,15 @@
method @NonNull public java.time.Instant getEndTime();
method public int getEventType();
method @NonNull public java.time.Instant getStartTime();
+ method @NonNull public android.os.PersistableBundle getVendorData();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.app.ambientcontext.AmbientContextEvent> CREATOR;
field public static final int EVENT_BACK_DOUBLE_TAP = 3; // 0x3
field public static final int EVENT_COUGH = 1; // 0x1
field public static final int EVENT_SNORE = 2; // 0x2
field public static final int EVENT_UNKNOWN = 0; // 0x0
+ field public static final int EVENT_VENDOR_WEARABLE_START = 100000; // 0x186a0
+ field public static final String KEY_VENDOR_WEARABLE_EVENT_NAME = "wearable_event_name";
field public static final int LEVEL_HIGH = 5; // 0x5
field public static final int LEVEL_LOW = 1; // 0x1
field public static final int LEVEL_MEDIUM = 3; // 0x3
@@ -1420,6 +1423,7 @@
method @NonNull public android.app.ambientcontext.AmbientContextEvent.Builder setEndTime(@NonNull java.time.Instant);
method @NonNull public android.app.ambientcontext.AmbientContextEvent.Builder setEventType(int);
method @NonNull public android.app.ambientcontext.AmbientContextEvent.Builder setStartTime(@NonNull java.time.Instant);
+ method @NonNull public android.app.ambientcontext.AmbientContextEvent.Builder setVendorData(@NonNull android.os.PersistableBundle);
}
public final class AmbientContextEventRequest implements android.os.Parcelable {
@@ -1492,11 +1496,13 @@
method @RequiresPermission(android.Manifest.permission.BACKUP) public android.content.Intent getDataManagementIntent(String);
method @Nullable @RequiresPermission(android.Manifest.permission.BACKUP) public CharSequence getDataManagementIntentLabel(@NonNull String);
method @Deprecated @Nullable @RequiresPermission(android.Manifest.permission.BACKUP) public String getDataManagementLabel(@NonNull String);
+ method @NonNull public android.app.backup.BackupRestoreEventLogger getDelayedRestoreLogger();
method @RequiresPermission(android.Manifest.permission.BACKUP) public String getDestinationString(String);
method @RequiresPermission(android.Manifest.permission.BACKUP) public boolean isAppEligibleForBackup(String);
method @RequiresPermission(android.Manifest.permission.BACKUP) public boolean isBackupEnabled();
method @RequiresPermission(android.Manifest.permission.BACKUP) public boolean isBackupServiceActive(android.os.UserHandle);
method @RequiresPermission(android.Manifest.permission.BACKUP) public String[] listAllTransports();
+ method @NonNull public void reportDelayedRestoreResult(@NonNull android.app.backup.BackupRestoreEventLogger);
method @RequiresPermission(android.Manifest.permission.BACKUP) public int requestBackup(String[], android.app.backup.BackupObserver);
method @RequiresPermission(android.Manifest.permission.BACKUP) public int requestBackup(String[], android.app.backup.BackupObserver, android.app.backup.BackupManagerMonitor, int);
method @Deprecated public int requestRestore(android.app.backup.RestoreObserver, android.app.backup.BackupManagerMonitor);
@@ -3327,6 +3333,7 @@
field public static final String CATEGORY_LEANBACK_SETTINGS = "android.intent.category.LEANBACK_SETTINGS";
field public static final String EXTRA_CALLING_PACKAGE = "android.intent.extra.CALLING_PACKAGE";
field public static final String EXTRA_FORCE_FACTORY_RESET = "android.intent.extra.FORCE_FACTORY_RESET";
+ field public static final String EXTRA_INSTALL_RESULT = "android.intent.extra.INSTALL_RESULT";
field public static final String EXTRA_INSTANT_APP_ACTION = "android.intent.extra.INSTANT_APP_ACTION";
field public static final String EXTRA_INSTANT_APP_BUNDLES = "android.intent.extra.INSTANT_APP_BUNDLES";
field public static final String EXTRA_INSTANT_APP_EXTRAS = "android.intent.extra.INSTANT_APP_EXTRAS";
@@ -3342,6 +3349,7 @@
field public static final String EXTRA_RESULT_NEEDED = "android.intent.extra.RESULT_NEEDED";
field public static final String EXTRA_ROLE_NAME = "android.intent.extra.ROLE_NAME";
field public static final String EXTRA_SHOWING_ATTRIBUTION = "android.intent.extra.SHOWING_ATTRIBUTION";
+ field public static final String EXTRA_UNINSTALL_ALL_USERS = "android.intent.extra.UNINSTALL_ALL_USERS";
field public static final String EXTRA_UNKNOWN_INSTANT_APP = "android.intent.extra.UNKNOWN_INSTANT_APP";
field public static final String EXTRA_USER_HANDLE = "android.intent.extra.user_handle";
field public static final String EXTRA_VERIFICATION_BUNDLE = "android.intent.extra.VERIFICATION_BUNDLE";
@@ -3425,15 +3433,10 @@
package android.content.om {
public final class OverlayInfo implements android.os.Parcelable {
- method public int describeContents();
method @Nullable public String getCategory();
method @NonNull public String getPackageName();
- method @Nullable public String getTargetOverlayableName();
- method @NonNull public String getTargetPackageName();
method public int getUserId();
method public boolean isEnabled();
- method public void writeToParcel(android.os.Parcel, int);
- field @NonNull public static final android.os.Parcelable.Creator<android.content.om.OverlayInfo> CREATOR;
}
public class OverlayManager {
@@ -3448,9 +3451,11 @@
package android.content.pm {
public class ApplicationInfo extends android.content.pm.PackageItemInfo implements android.os.Parcelable {
+ method @RequiresPermission(android.Manifest.permission.DELETE_PACKAGES) public boolean hasFragileUserData();
method public boolean isEncryptionAware();
method public boolean isInstantApp();
method public boolean isOem();
+ method @RequiresPermission(android.Manifest.permission.INSTALL_PACKAGES) public boolean isPrivilegedApp();
method public boolean isProduct();
method public boolean isVendor();
field public String credentialProtectedDataDir;
@@ -3568,16 +3573,32 @@
}
public class PackageInstaller {
+ method @NonNull public android.content.pm.PackageInstaller.InstallInfo getInstallInfo(@NonNull java.io.File, int) throws android.content.pm.PackageInstaller.PackageParsingException;
method @RequiresPermission(android.Manifest.permission.INSTALL_PACKAGES) public void setPermissionsResult(int, boolean);
+ field public static final String ACTION_CONFIRM_INSTALL = "android.content.pm.action.CONFIRM_INSTALL";
+ field public static final String ACTION_CONFIRM_PRE_APPROVAL = "android.content.pm.action.CONFIRM_PRE_APPROVAL";
field public static final int DATA_LOADER_TYPE_INCREMENTAL = 2; // 0x2
field public static final int DATA_LOADER_TYPE_NONE = 0; // 0x0
field public static final int DATA_LOADER_TYPE_STREAMING = 1; // 0x1
+ field public static final String EXTRA_CALLBACK = "android.content.pm.extra.CALLBACK";
field public static final String EXTRA_DATA_LOADER_TYPE = "android.content.pm.extra.DATA_LOADER_TYPE";
+ field public static final String EXTRA_LEGACY_STATUS = "android.content.pm.extra.LEGACY_STATUS";
+ field public static final String EXTRA_RESOLVED_BASE_PATH = "android.content.pm.extra.RESOLVED_BASE_PATH";
field public static final int LOCATION_DATA_APP = 0; // 0x0
field public static final int LOCATION_MEDIA_DATA = 2; // 0x2
field public static final int LOCATION_MEDIA_OBB = 1; // 0x1
}
+ public static class PackageInstaller.InstallInfo {
+ method public long calculateInstalledSize(@NonNull android.content.pm.PackageInstaller.SessionParams) throws java.io.IOException;
+ method public int getInstallLocation();
+ method @NonNull public String getPackageName();
+ }
+
+ public static class PackageInstaller.PackageParsingException extends java.lang.Exception {
+ method public int getErrorCode();
+ }
+
public static class PackageInstaller.Session implements java.io.Closeable {
method @RequiresPermission("com.android.permission.USE_INSTALLER_V2") public void addFile(int, @NonNull String, long, @NonNull byte[], @Nullable byte[]);
method @RequiresPermission(android.Manifest.permission.INSTALL_PACKAGES) public void commitTransferred(@NonNull android.content.IntentSender);
@@ -3624,6 +3645,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 canUserUninstall(@NonNull String, @NonNull android.os.UserHandle);
method @NonNull public abstract java.util.List<android.content.IntentFilter> getAllIntentFilters(@NonNull String);
method @NonNull @RequiresPermission("android.permission.GET_APP_METADATA") public android.os.PersistableBundle getAppMetadata(@NonNull String) throws android.content.pm.PackageManager.NameNotFoundException;
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;
@@ -3642,6 +3664,7 @@
method @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_INSTANT_APPS) public abstract java.util.List<android.content.pm.InstantAppInfo> getInstantApps();
method @Deprecated @NonNull public abstract java.util.List<android.content.pm.IntentFilterVerificationInfo> getIntentFilterVerifications(@NonNull String);
method @Deprecated @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) public abstract int getIntentVerificationStatusAsUser(@NonNull String, int);
+ method @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) public int getPackageUidAsUser(@NonNull String, @NonNull android.content.pm.PackageManager.PackageInfoFlags, int) throws android.content.pm.PackageManager.NameNotFoundException;
method @android.content.pm.PackageManager.PermissionFlags @RequiresPermission(anyOf={android.Manifest.permission.GRANT_RUNTIME_PERMISSIONS, android.Manifest.permission.REVOKE_RUNTIME_PERMISSIONS, android.Manifest.permission.GET_RUNTIME_PERMISSIONS}) public abstract int getPermissionFlags(@NonNull String, @NonNull String, @NonNull android.os.UserHandle);
method @NonNull @RequiresPermission(android.Manifest.permission.SUSPEND_APPS) public String[] getUnsuspendablePackages(@NonNull String[]);
method @RequiresPermission(android.Manifest.permission.GRANT_RUNTIME_PERMISSIONS) public abstract void grantRuntimePermission(@NonNull String, @NonNull String, @NonNull android.os.UserHandle);
@@ -3669,11 +3692,19 @@
method @RequiresPermission(value=android.Manifest.permission.CHANGE_COMPONENT_ENABLED_STATE, conditional=true) public void setSyntheticAppDetailsActivityEnabled(@NonNull String, boolean);
method public void setSystemAppState(@NonNull String, int);
method @RequiresPermission(android.Manifest.permission.INSTALL_PACKAGES) public abstract void setUpdateAvailable(@NonNull String, boolean);
+ method @NonNull public boolean shouldShowNewAppInstalledNotification();
method @Deprecated @RequiresPermission(android.Manifest.permission.SET_PREFERRED_APPLICATIONS) public abstract boolean updateIntentVerificationStatusAsUser(@NonNull String, int, int);
method @RequiresPermission(anyOf={android.Manifest.permission.GRANT_RUNTIME_PERMISSIONS, android.Manifest.permission.REVOKE_RUNTIME_PERMISSIONS}) public abstract void updatePermissionFlags(@NonNull String, @NonNull String, @android.content.pm.PackageManager.PermissionFlags int, @android.content.pm.PackageManager.PermissionFlags int, @NonNull android.os.UserHandle);
method @Deprecated @RequiresPermission(android.Manifest.permission.INTENT_FILTER_VERIFICATION_AGENT) public abstract void verifyIntentFilter(int, int, @NonNull java.util.List<java.lang.String>);
field public static final String ACTION_REQUEST_PERMISSIONS = "android.content.pm.action.REQUEST_PERMISSIONS";
field public static final String ACTION_REQUEST_PERMISSIONS_FOR_OTHER = "android.content.pm.action.REQUEST_PERMISSIONS_FOR_OTHER";
+ field public static final int DELETE_ALL_USERS = 2; // 0x2
+ field public static final int DELETE_FAILED_ABORTED = -5; // 0xfffffffb
+ field public static final int DELETE_FAILED_DEVICE_POLICY_MANAGER = -2; // 0xfffffffe
+ field public static final int DELETE_FAILED_INTERNAL_ERROR = -1; // 0xffffffff
+ field public static final int DELETE_FAILED_OWNER_BLOCKED = -4; // 0xfffffffc
+ field public static final int DELETE_KEEP_DATA = 1; // 0x1
+ field public static final int DELETE_SUCCEEDED = 1; // 0x1
field public static final String EXTRA_REQUEST_PERMISSIONS_LEGACY_ACCESS_PERMISSION_NAMES = "android.content.pm.extra.REQUEST_PERMISSIONS_LEGACY_ACCESS_PERMISSION_NAMES";
field public static final String EXTRA_REQUEST_PERMISSIONS_NAMES = "android.content.pm.extra.REQUEST_PERMISSIONS_NAMES";
field public static final String EXTRA_REQUEST_PERMISSIONS_RESULTS = "android.content.pm.extra.REQUEST_PERMISSIONS_RESULTS";
@@ -3781,6 +3812,11 @@
@IntDef(prefix={"FLAG_PERMISSION_"}, value={android.content.pm.PackageManager.FLAG_PERMISSION_USER_SET, android.content.pm.PackageManager.FLAG_PERMISSION_USER_FIXED, android.content.pm.PackageManager.FLAG_PERMISSION_POLICY_FIXED, android.content.pm.PackageManager.FLAG_PERMISSION_REVOKE_ON_UPGRADE, android.content.pm.PackageManager.FLAG_PERMISSION_SYSTEM_FIXED, android.content.pm.PackageManager.FLAG_PERMISSION_GRANTED_BY_DEFAULT, android.content.pm.PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED, android.content.pm.PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_DENIED, android.content.pm.PackageManager.FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT, android.content.pm.PackageManager.FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT, android.content.pm.PackageManager.FLAG_PERMISSION_RESTRICTION_INSTALLER_EXEMPT, android.content.pm.PackageManager.FLAG_PERMISSION_APPLY_RESTRICTION, android.content.pm.PackageManager.FLAG_PERMISSION_GRANTED_BY_ROLE, android.content.pm.PackageManager.FLAG_PERMISSION_REVOKED_COMPAT, android.content.pm.PackageManager.FLAG_PERMISSION_ONE_TIME, android.content.pm.PackageManager.FLAG_PERMISSION_AUTO_REVOKED}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface PackageManager.PermissionFlags {
}
+ public static final class PackageManager.UninstallCompleteCallback implements android.os.Parcelable {
+ method public void onUninstallComplete(@NonNull String, int, @Nullable String);
+ field @NonNull public static final android.os.Parcelable.Creator<android.content.pm.PackageManager.UninstallCompleteCallback> CREATOR;
+ }
+
public class PermissionGroupInfo extends android.content.pm.PackageItemInfo implements android.os.Parcelable {
field @StringRes public final int backgroundRequestDetailResourceId;
field @StringRes public final int backgroundRequestResourceId;
@@ -4240,6 +4276,7 @@
method @RequiresPermission(android.Manifest.permission.CONFIGURE_DISPLAY_BRIGHTNESS) public void setBrightnessConfiguration(android.hardware.display.BrightnessConfiguration);
method @RequiresPermission(android.Manifest.permission.CONFIGURE_DISPLAY_BRIGHTNESS) public void setBrightnessConfigurationForDisplay(@NonNull android.hardware.display.BrightnessConfiguration, @NonNull String);
method @Deprecated @RequiresPermission(android.Manifest.permission.CONTROL_DISPLAY_SATURATION) public void setSaturationLevel(float);
+ field public static final int VIRTUAL_DISPLAY_FLAG_STEAL_TOP_FOCUS_DISABLED = 65536; // 0x10000
field public static final int VIRTUAL_DISPLAY_FLAG_TRUSTED = 1024; // 0x400
}
@@ -4531,12 +4568,14 @@
public final class HdmiPortInfo implements android.os.Parcelable {
ctor public HdmiPortInfo(int, int, int, boolean, boolean, boolean);
+ ctor public HdmiPortInfo(int, int, int, boolean, boolean, boolean, boolean);
method public int describeContents();
method public int getAddress();
method public int getId();
method public int getType();
method public boolean isArcSupported();
method public boolean isCecSupported();
+ method public boolean isEarcSupported();
method public boolean isMhlSupported();
method public void writeToParcel(android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.hardware.hdmi.HdmiPortInfo> CREATOR;
@@ -4863,17 +4902,15 @@
public final class VirtualTouchscreenConfig extends android.hardware.input.VirtualInputDeviceConfig implements android.os.Parcelable {
method public int describeContents();
- method public int getHeightInPixels();
- method public int getWidthInPixels();
+ 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.VirtualTouchscreenConfig> CREATOR;
}
public static final class VirtualTouchscreenConfig.Builder extends android.hardware.input.VirtualInputDeviceConfig.Builder<android.hardware.input.VirtualTouchscreenConfig.Builder> {
- ctor public VirtualTouchscreenConfig.Builder();
+ ctor public VirtualTouchscreenConfig.Builder(@IntRange(from=1) int, @IntRange(from=1) int);
method @NonNull public android.hardware.input.VirtualTouchscreenConfig build();
- method @NonNull public android.hardware.input.VirtualTouchscreenConfig.Builder setHeightInPixels(int);
- method @NonNull public android.hardware.input.VirtualTouchscreenConfig.Builder setWidthInPixels(int);
}
}
@@ -5817,6 +5854,8 @@
field public static final int DATA_STATUS_DISABLED_CONTAMINANT = 4; // 0x4
field public static final int DATA_STATUS_DISABLED_DEBUG = 32; // 0x20
field public static final int DATA_STATUS_DISABLED_DOCK = 8; // 0x8
+ field public static final int DATA_STATUS_DISABLED_DOCK_DEVICE_MODE = 128; // 0x80
+ field public static final int DATA_STATUS_DISABLED_DOCK_HOST_MODE = 64; // 0x40
field public static final int DATA_STATUS_DISABLED_FORCE = 16; // 0x10
field public static final int DATA_STATUS_DISABLED_OVERHEAT = 2; // 0x2
field public static final int DATA_STATUS_ENABLED = 1; // 0x1
@@ -6565,6 +6604,7 @@
public class AudioManager {
method @Deprecated public int abandonAudioFocus(android.media.AudioManager.OnAudioFocusChangeListener, android.media.AudioAttributes);
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void addAssistantServicesUids(@NonNull int[]);
+ method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void addOnNonDefaultDevicesForStrategyChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.OnNonDefaultDevicesForStrategyChangedListener) throws java.lang.SecurityException;
method @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void addOnPreferredDeviceForStrategyChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.OnPreferredDeviceForStrategyChangedListener) throws java.lang.SecurityException;
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void addOnPreferredDevicesForCapturePresetChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.OnPreferredDevicesForCapturePresetChangedListener) throws java.lang.SecurityException;
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void addOnPreferredDevicesForStrategyChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.OnPreferredDevicesForStrategyChangedListener) throws java.lang.SecurityException;
@@ -6586,12 +6626,14 @@
method @IntRange(from=0) @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int getMaxVolumeIndexForAttributes(@NonNull android.media.AudioAttributes);
method @IntRange(from=0) @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int getMinVolumeIndexForAttributes(@NonNull android.media.AudioAttributes);
method @Nullable @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public android.media.AudioDeviceAttributes getMutingExpectedDevice();
+ method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public java.util.List<android.media.AudioDeviceAttributes> getNonDefaultDevicesForStrategy(@NonNull android.media.audiopolicy.AudioProductStrategy);
method @Nullable @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public android.media.AudioDeviceAttributes getPreferredDeviceForStrategy(@NonNull android.media.audiopolicy.AudioProductStrategy);
method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public java.util.List<android.media.AudioDeviceAttributes> getPreferredDevicesForCapturePreset(int);
method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public java.util.List<android.media.AudioDeviceAttributes> getPreferredDevicesForStrategy(@NonNull android.media.audiopolicy.AudioProductStrategy);
method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int[] getSupportedSystemUsages();
method @IntRange(from=0) @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int getVolumeIndexForAttributes(@NonNull android.media.AudioAttributes);
method public boolean isAudioServerRunning();
+ method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean isBluetoothVariableLatencyEnabled();
method public boolean isHdmiSystemAudioSupported();
method @RequiresPermission(android.Manifest.permission.CALL_AUDIO_INTERCEPTION) public boolean isPstnCallAudioInterceptable();
method @RequiresPermission(android.Manifest.permission.ACCESS_ULTRASOUND) public boolean isUltrasoundSupported();
@@ -6600,6 +6642,8 @@
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void registerMuteAwaitConnectionCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.MuteAwaitConnectionCallback);
method public void registerVolumeGroupCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.VolumeGroupCallback);
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void removeAssistantServicesUids(@NonNull int[]);
+ method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean removeDeviceAsNonDefaultForStrategy(@NonNull android.media.audiopolicy.AudioProductStrategy, @NonNull android.media.AudioDeviceAttributes);
+ method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void removeOnNonDefaultDevicesForStrategyChangedListener(@NonNull android.media.AudioManager.OnNonDefaultDevicesForStrategyChangedListener);
method @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void removeOnPreferredDeviceForStrategyChangedListener(@NonNull android.media.AudioManager.OnPreferredDeviceForStrategyChangedListener);
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void removeOnPreferredDevicesForCapturePresetChangedListener(@NonNull android.media.AudioManager.OnPreferredDevicesForCapturePresetChangedListener);
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void removeOnPreferredDevicesForStrategyChangedListener(@NonNull android.media.AudioManager.OnPreferredDevicesForStrategyChangedListener);
@@ -6610,6 +6654,8 @@
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void setActiveAssistantServiceUids(@NonNull int[]);
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean setAdditionalOutputDeviceDelay(@NonNull android.media.AudioDeviceInfo, @IntRange(from=0) long);
method public void setAudioServerStateCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.AudioServerStateCallback);
+ method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void setBluetoothVariableLatencyEnabled(boolean);
+ method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean setDeviceAsNonDefaultForStrategy(@NonNull android.media.audiopolicy.AudioProductStrategy, @NonNull android.media.AudioDeviceAttributes);
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void setDeviceVolumeBehavior(@NonNull android.media.AudioDeviceAttributes, int);
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void setFocusRequestResult(@NonNull android.media.AudioFocusInfo, int, @NonNull android.media.audiopolicy.AudioPolicy);
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean setPreferredDeviceForCapturePreset(int, @NonNull android.media.AudioDeviceAttributes);
@@ -6617,6 +6663,7 @@
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean setPreferredDevicesForStrategy(@NonNull android.media.audiopolicy.AudioProductStrategy, @NonNull java.util.List<android.media.AudioDeviceAttributes>);
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void setSupportedSystemUsages(@NonNull int[]);
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void setVolumeIndexForAttributes(@NonNull android.media.AudioAttributes, int, int);
+ method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean supportsBluetoothVariableLatency();
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void unregisterAudioPolicy(@NonNull android.media.audiopolicy.AudioPolicy);
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void unregisterAudioPolicyAsync(@NonNull android.media.audiopolicy.AudioPolicy);
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void unregisterMuteAwaitConnectionCallback(@NonNull android.media.AudioManager.MuteAwaitConnectionCallback);
@@ -6653,6 +6700,10 @@
field public static final int EVENT_TIMEOUT = 2; // 0x2
}
+ public static interface AudioManager.OnNonDefaultDevicesForStrategyChangedListener {
+ method public void onNonDefaultDevicesForStrategyChanged(@NonNull android.media.audiopolicy.AudioProductStrategy, @NonNull java.util.List<android.media.AudioDeviceAttributes>);
+ }
+
@Deprecated public static interface AudioManager.OnPreferredDeviceForStrategyChangedListener {
method @Deprecated public void onPreferredDeviceForStrategyChanged(@NonNull android.media.audiopolicy.AudioProductStrategy, @Nullable android.media.AudioDeviceAttributes);
}
@@ -6671,6 +6722,7 @@
}
public final class AudioPlaybackConfiguration implements android.os.Parcelable {
+ method public int getChannelMask();
method public int getClientPid();
method public int getClientUid();
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int getMutedBy();
@@ -6678,9 +6730,11 @@
method public android.media.PlayerProxy getPlayerProxy();
method public int getPlayerState();
method public int getPlayerType();
+ method @IntRange(from=0) public int getSampleRate();
method @IntRange(from=0) public int getSessionId();
method public boolean isActive();
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean isMuted();
+ method public boolean isSpatialized();
field @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public static final int MUTED_BY_APP_OPS = 8; // 0x8
field @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public static final int MUTED_BY_CLIENT_VOLUME = 16; // 0x10
field @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public static final int MUTED_BY_MASTER = 1; // 0x1
@@ -7734,6 +7788,7 @@
field public static final int STATUS_DATA_READY = 1; // 0x1
field public static final int STATUS_HIGH_WATER = 4; // 0x4
field public static final int STATUS_LOW_WATER = 2; // 0x2
+ field public static final int STATUS_NO_DATA = 16; // 0x10
field public static final int STATUS_OVERFLOW = 8; // 0x8
field public static final int SUBTYPE_AUDIO = 3; // 0x3
field public static final int SUBTYPE_DOWNLOAD = 5; // 0x5
@@ -10913,6 +10968,7 @@
field public static final String ACTION_TETHER_PROVISIONING_UI = "android.settings.TETHER_PROVISIONING_UI";
field public static final String ACTION_TETHER_SETTINGS = "android.settings.TETHER_SETTINGS";
field public static final String ACTION_TETHER_UNSUPPORTED_CARRIER_UI = "android.settings.TETHER_UNSUPPORTED_CARRIER_UI";
+ field public static final String ACTION_USER_SETTINGS = "android.settings.USER_SETTINGS";
}
public static final class Settings.Global extends android.provider.Settings.NameValueTable {
@@ -11824,6 +11880,7 @@
field @NonNull public static final android.os.Parcelable.Creator<android.service.notification.Adjustment> CREATOR;
field public static final String KEY_CONTEXTUAL_ACTIONS = "key_contextual_actions";
field public static final String KEY_IMPORTANCE = "key_importance";
+ field public static final String KEY_IMPORTANCE_PROPOSAL = "key_importance_proposal";
field public static final String KEY_NOT_CONVERSATION = "key_not_conversation";
field public static final String KEY_PEOPLE = "key_people";
field public static final String KEY_RANKING_SCORE = "key_ranking_score";
@@ -11864,6 +11921,10 @@
method public void onNotificationRemoved(@NonNull android.service.notification.StatusBarNotification, @NonNull android.service.notification.NotificationListenerService.RankingMap, @NonNull android.service.notification.NotificationStats, int);
}
+ public static class NotificationListenerService.Ranking {
+ method public int getProposedImportance();
+ }
+
public final class NotificationStats implements android.os.Parcelable {
ctor public NotificationStats();
ctor protected NotificationStats(android.os.Parcel);
@@ -12525,6 +12586,9 @@
method @Nullable public final android.os.IBinder onBind(@NonNull android.content.Intent);
method @BinderThread public abstract void onDataProvided(@NonNull android.os.PersistableBundle, @Nullable android.os.SharedMemory, @NonNull java.util.function.Consumer<java.lang.Integer>);
method @BinderThread public abstract void onDataStreamProvided(@NonNull android.os.ParcelFileDescriptor, @NonNull java.util.function.Consumer<java.lang.Integer>);
+ method @BinderThread public abstract void onQueryServiceStatus(@NonNull java.util.Set<java.lang.Integer>, @NonNull String, @NonNull java.util.function.Consumer<android.service.ambientcontext.AmbientContextDetectionServiceStatus>);
+ method @BinderThread public abstract void onStartDetection(@NonNull android.app.ambientcontext.AmbientContextEventRequest, @NonNull String, @NonNull java.util.function.Consumer<android.service.ambientcontext.AmbientContextDetectionServiceStatus>, @NonNull java.util.function.Consumer<android.service.ambientcontext.AmbientContextDetectionResult>);
+ method public abstract void onStopDetection(@NonNull String);
field public static final String SERVICE_INTERFACE = "android.service.wearable.WearableSensingService";
}
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 82cc3fd..233dee9 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -125,7 +125,7 @@
public class ActivityManager {
method @RequiresPermission(android.Manifest.permission.SET_ACTIVITY_WATCHER) public void addHomeVisibilityListener(@NonNull java.util.concurrent.Executor, @NonNull android.app.HomeVisibilityListener);
method public void alwaysShowUnsupportedCompileSdkWarning(android.content.ComponentName);
- method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}) public int[] getSecondaryDisplayIdsForStartingBackgroundUsers();
+ method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}) public int[] getDisplayIdsForStartingVisibleBackgroundUsers();
method public long getTotalRam();
method @RequiresPermission(allOf={android.Manifest.permission.PACKAGE_USAGE_STATS, android.Manifest.permission.INTERACT_ACROSS_USERS_FULL}, conditional=true) public int getUidProcessCapabilities(int);
method @RequiresPermission(allOf={android.Manifest.permission.PACKAGE_USAGE_STATS, android.Manifest.permission.INTERACT_ACROSS_USERS_FULL}, conditional=true) public int getUidProcessState(int);
@@ -136,7 +136,7 @@
method public static void resumeAppSwitches() throws android.os.RemoteException;
method @RequiresPermission(android.Manifest.permission.CHANGE_CONFIGURATION) public void scheduleApplicationInfoChanged(java.util.List<java.lang.String>, int);
method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}) public void setStopUserOnSwitch(int);
- method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}) public boolean startUserInBackgroundOnSecondaryDisplay(int, int);
+ method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}) public boolean startUserInBackgroundVisibleOnDisplay(int, int);
method @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) public boolean stopUser(int, boolean);
method @RequiresPermission(android.Manifest.permission.CHANGE_CONFIGURATION) public boolean updateMccMncConfiguration(@NonNull String, @NonNull String);
method @RequiresPermission(android.Manifest.permission.DUMP) public void waitForBroadcastIdle();
@@ -814,7 +814,7 @@
public class ApplicationInfo extends android.content.pm.PackageItemInfo implements android.os.Parcelable {
method public boolean hasRequestForegroundServiceExemption();
method public boolean isOnBackInvokedCallbackEnabled();
- method public boolean isPrivilegedApp();
+ method @RequiresPermission(android.Manifest.permission.INSTALL_PACKAGES) public boolean isPrivilegedApp();
method public boolean isSystemApp();
method public void setEnableOnBackInvokedCallback(boolean);
field public static final int PRIVATE_FLAG_PRIVILEGED = 8; // 0x8
@@ -831,7 +831,6 @@
public static class PackageInstaller.SessionParams implements android.os.Parcelable {
method public void setInstallFlagAllowTest();
- method public void setInstallerPackageName(@Nullable String);
}
public abstract class PackageManager {
@@ -1299,6 +1298,7 @@
method @RequiresPermission(android.Manifest.permission.REMAP_MODIFIER_KEYS) public void clearAllModifierKeyRemappings();
method @Nullable public String getCurrentKeyboardLayoutForInputDevice(@NonNull android.hardware.input.InputDeviceIdentifier);
method @NonNull public java.util.List<java.lang.String> getKeyboardLayoutDescriptorsForInputDevice(@NonNull android.view.InputDevice);
+ method @NonNull public String getKeyboardLayoutTypeForLayoutDescriptor(@NonNull String);
method @NonNull @RequiresPermission(android.Manifest.permission.REMAP_MODIFIER_KEYS) public java.util.Map<java.lang.Integer,java.lang.Integer> getModifierKeyRemapping();
method @RequiresPermission(android.Manifest.permission.REMAP_MODIFIER_KEYS) public void remapModifierKey(int, int);
method @RequiresPermission(android.Manifest.permission.SET_KEYBOARD_LAYOUT) public void removeKeyboardLayoutForInputDevice(@NonNull android.hardware.input.InputDeviceIdentifier, @NonNull String);
@@ -1942,7 +1942,7 @@
method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public boolean hasBaseUserRestriction(@NonNull String, @NonNull android.os.UserHandle);
method public static boolean isSplitSystemUser();
method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public boolean isUserTypeEnabled(@NonNull String);
- method public boolean isUsersOnSecondaryDisplaysSupported();
+ method public boolean isVisibleBackgroundUsersSupported();
method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public android.content.pm.UserInfo preCreateUser(@NonNull String) throws android.os.UserManager.UserOperationException;
}
@@ -3232,7 +3232,7 @@
package android.view.inputmethod {
public abstract class HandwritingGesture {
- method public int getGestureType();
+ method public final int getGestureType();
}
public final class InlineSuggestion implements android.os.Parcelable {
@@ -3256,6 +3256,7 @@
method public int getDisplayId();
method @NonNull @RequiresPermission(value=android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional=true) public java.util.List<android.view.inputmethod.InputMethodInfo> getInputMethodListAsUser(int);
method public boolean hasActiveInputConnection(@Nullable android.view.View);
+ method @RequiresPermission(android.Manifest.permission.TEST_INPUT_METHOD) public boolean hasPendingImeVisibilityRequests();
method @RequiresPermission(android.Manifest.permission.TEST_INPUT_METHOD) public boolean isInputMethodPickerShown();
method @RequiresPermission(android.Manifest.permission.TEST_INPUT_METHOD) public void setStylusWindowIdleTimeoutForTest(long);
field public static final long CLEAR_SHOW_FORCED_FLAG_WHEN_LEAVING = 214016041L; // 0xcc1a029L
diff --git a/core/java/android/animation/Animator.java b/core/java/android/animation/Animator.java
index 8142ee5..a81ef18 100644
--- a/core/java/android/animation/Animator.java
+++ b/core/java/android/animation/Animator.java
@@ -547,7 +547,6 @@
*/
void skipToEndValue(boolean inReverse) {}
-
/**
* Internal use only.
*
@@ -565,13 +564,13 @@
* repetition. lastPlayTime is similar and is used to calculate how many repeats have been
* done between the two times.
*/
- void animateValuesInRange(long currentPlayTime, long lastPlayTime) {}
+ void animateValuesInRange(long currentPlayTime, long lastPlayTime, boolean notify) {}
/**
* Internal use only. This animates any animation that has ended since lastPlayTime.
* If an animation hasn't been finished, no change will be made.
*/
- void animateSkipToEnds(long currentPlayTime, long lastPlayTime) {}
+ void animateSkipToEnds(long currentPlayTime, long lastPlayTime, boolean notify) {}
/**
* Internal use only. Adds all start times (after delay) to and end times to times.
diff --git a/core/java/android/animation/AnimatorSet.java b/core/java/android/animation/AnimatorSet.java
index 54aaafc..257adfe 100644
--- a/core/java/android/animation/AnimatorSet.java
+++ b/core/java/android/animation/AnimatorSet.java
@@ -188,6 +188,11 @@
*/
private long[] mChildStartAndStopTimes;
+ /**
+ * Tracks whether we've notified listeners of the onAnimationStart() event.
+ */
+ private boolean mStartListenersCalled;
+
// This is to work around a bug in b/34736819. This needs to be removed once app team
// fixes their side.
private AnimatorListenerAdapter mAnimationEndListener = new AnimatorListenerAdapter() {
@@ -736,14 +741,7 @@
startAnimation();
}
- if (mListeners != null) {
- ArrayList<AnimatorListener> tmpListeners =
- (ArrayList<AnimatorListener>) mListeners.clone();
- int numListeners = tmpListeners.size();
- for (int i = 0; i < numListeners; ++i) {
- tmpListeners.get(i).onAnimationStart(this, inReverse);
- }
- }
+ notifyStartListeners(inReverse);
if (isEmptySet) {
// In the case of empty AnimatorSet, or 0 duration scale, we will trigger the
// onAnimationEnd() right away.
@@ -751,6 +749,32 @@
}
}
+ private void notifyStartListeners(boolean inReverse) {
+ if (mListeners != null && !mStartListenersCalled) {
+ ArrayList<AnimatorListener> tmpListeners =
+ (ArrayList<AnimatorListener>) mListeners.clone();
+ int numListeners = tmpListeners.size();
+ for (int i = 0; i < numListeners; ++i) {
+ AnimatorListener listener = tmpListeners.get(i);
+ listener.onAnimationStart(this, inReverse);
+ }
+ }
+ mStartListenersCalled = true;
+ }
+
+ private void notifyEndListeners(boolean inReverse) {
+ if (mListeners != null && mStartListenersCalled) {
+ ArrayList<AnimatorListener> tmpListeners =
+ (ArrayList<AnimatorListener>) mListeners.clone();
+ int numListeners = tmpListeners.size();
+ for (int i = 0; i < numListeners; ++i) {
+ AnimatorListener listener = tmpListeners.get(i);
+ listener.onAnimationEnd(this, inReverse);
+ }
+ }
+ mStartListenersCalled = false;
+ }
+
// Returns true if set is empty or contains nothing but animator sets with no start delay.
private static boolean isEmptySet(AnimatorSet set) {
if (set.getStartDelay() > 0) {
@@ -823,7 +847,8 @@
private void animateBasedOnPlayTime(
long currentPlayTime,
long lastPlayTime,
- boolean inReverse
+ boolean inReverse,
+ boolean notify
) {
if (currentPlayTime < 0 || lastPlayTime < -1) {
throw new UnsupportedOperationException("Error: Play time should never be negative.");
@@ -854,8 +879,8 @@
while (index < endIndex) {
long playTime = startEndTimes[index];
if (lastPlayTime != playTime) {
- animateSkipToEnds(playTime, lastPlayTime);
- animateValuesInRange(playTime, lastPlayTime);
+ animateSkipToEnds(playTime, lastPlayTime, notify);
+ animateValuesInRange(playTime, lastPlayTime, notify);
lastPlayTime = playTime;
}
index++;
@@ -865,15 +890,15 @@
index--;
long playTime = startEndTimes[index];
if (lastPlayTime != playTime) {
- animateSkipToEnds(playTime, lastPlayTime);
- animateValuesInRange(playTime, lastPlayTime);
+ animateSkipToEnds(playTime, lastPlayTime, notify);
+ animateValuesInRange(playTime, lastPlayTime, notify);
lastPlayTime = playTime;
}
}
}
if (currentPlayTime != lastPlayTime) {
- animateSkipToEnds(currentPlayTime, lastPlayTime);
- animateValuesInRange(currentPlayTime, lastPlayTime);
+ animateSkipToEnds(currentPlayTime, lastPlayTime, notify);
+ animateValuesInRange(currentPlayTime, lastPlayTime, notify);
}
}
@@ -893,10 +918,13 @@
}
@Override
- void animateSkipToEnds(long currentPlayTime, long lastPlayTime) {
+ void animateSkipToEnds(long currentPlayTime, long lastPlayTime, boolean notify) {
initAnimation();
if (lastPlayTime > currentPlayTime) {
+ if (notify) {
+ notifyStartListeners(true);
+ }
for (int i = mEvents.size() - 1; i >= 0; i--) {
AnimationEvent event = mEvents.get(i);
Node node = event.mNode;
@@ -904,23 +932,31 @@
&& node.mStartTime != DURATION_INFINITE
) {
Animator animator = node.mAnimation;
- long start = node.mStartTime + animator.getStartDelay();
+ long start = node.mStartTime;
long end = node.mTotalDuration == DURATION_INFINITE
? Long.MAX_VALUE : node.mEndTime;
if (currentPlayTime <= start && start < lastPlayTime) {
animator.animateSkipToEnds(
- start - node.mStartTime,
- lastPlayTime - node.mStartTime
+ 0,
+ lastPlayTime - node.mStartTime,
+ notify
);
} else if (start <= currentPlayTime && currentPlayTime <= end) {
animator.animateSkipToEnds(
currentPlayTime - node.mStartTime,
- lastPlayTime - node.mStartTime
+ lastPlayTime - node.mStartTime,
+ notify
);
}
}
}
+ if (currentPlayTime <= 0 && notify) {
+ notifyEndListeners(true);
+ }
} else {
+ if (notify) {
+ notifyStartListeners(false);
+ }
int eventsSize = mEvents.size();
for (int i = 0; i < eventsSize; i++) {
AnimationEvent event = mEvents.get(i);
@@ -929,29 +965,48 @@
&& node.mStartTime != DURATION_INFINITE
) {
Animator animator = node.mAnimation;
- long start = node.mStartTime + animator.getStartDelay();
+ long start = node.mStartTime;
long end = node.mTotalDuration == DURATION_INFINITE
? Long.MAX_VALUE : node.mEndTime;
if (lastPlayTime < end && end <= currentPlayTime) {
animator.animateSkipToEnds(
end - node.mStartTime,
- lastPlayTime - node.mStartTime
+ lastPlayTime - node.mStartTime,
+ notify
);
} else if (start <= currentPlayTime && currentPlayTime <= end) {
animator.animateSkipToEnds(
currentPlayTime - node.mStartTime,
- lastPlayTime - node.mStartTime
+ lastPlayTime - node.mStartTime,
+ notify
);
}
}
}
+ if (currentPlayTime >= getTotalDuration() && notify) {
+ notifyEndListeners(false);
+ }
}
}
@Override
- void animateValuesInRange(long currentPlayTime, long lastPlayTime) {
+ void animateValuesInRange(long currentPlayTime, long lastPlayTime, boolean notify) {
initAnimation();
+ if (notify) {
+ if (lastPlayTime < 0 || (lastPlayTime == 0 && currentPlayTime > 0)) {
+ notifyStartListeners(false);
+ } else {
+ long duration = getTotalDuration();
+ if (duration >= 0
+ && (lastPlayTime > duration || (lastPlayTime == duration
+ && currentPlayTime < duration))
+ ) {
+ notifyStartListeners(true);
+ }
+ }
+ }
+
int eventsSize = mEvents.size();
for (int i = 0; i < eventsSize; i++) {
AnimationEvent event = mEvents.get(i);
@@ -960,13 +1015,17 @@
&& node.mStartTime != DURATION_INFINITE
) {
Animator animator = node.mAnimation;
- long start = node.mStartTime + animator.getStartDelay();
+ long start = node.mStartTime;
long end = node.mTotalDuration == DURATION_INFINITE
? Long.MAX_VALUE : node.mEndTime;
- if (start < currentPlayTime && currentPlayTime < end) {
+ if ((start < currentPlayTime && currentPlayTime < end)
+ || (start == currentPlayTime && lastPlayTime < start)
+ || (end == currentPlayTime && lastPlayTime > end)
+ ) {
animator.animateValuesInRange(
currentPlayTime - node.mStartTime,
- Math.max(-1, lastPlayTime - node.mStartTime)
+ Math.max(-1, lastPlayTime - node.mStartTime),
+ notify
);
}
}
@@ -1021,6 +1080,11 @@
* set to this time; it will simply set the time to this value and perform any appropriate
* actions based on that time. If the animation is already running, then setCurrentPlayTime()
* will set the current playing time to this value and continue playing from that point.
+ * On {@link Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and above, an AnimatorSet
+ * that hasn't been {@link #start()}ed, will issue
+ * {@link android.animation.Animator.AnimatorListener#onAnimationStart(Animator, boolean)}
+ * and {@link android.animation.Animator.AnimatorListener#onAnimationEnd(Animator, boolean)}
+ * events.
*
* @param playTime The time, in milliseconds, to which the animation is advanced or rewound.
* Unless the animation is reversing, the playtime is considered the time since
@@ -1042,12 +1106,12 @@
initAnimation();
+ long lastPlayTime = mSeekState.getPlayTime();
if (!isStarted() || isPaused()) {
if (mReversing && !isStarted()) {
throw new UnsupportedOperationException("Error: Something went wrong. mReversing"
+ " should not be set when AnimatorSet is not started.");
}
- long lastPlayTime = mSeekState.getPlayTime();
if (!mSeekState.isActive()) {
findLatestEventIdForTime(0);
initChildren();
@@ -1055,13 +1119,9 @@
skipToEndValue(!mReversing);
mSeekState.setPlayTime(0, mReversing);
}
- animateBasedOnPlayTime(playTime, lastPlayTime, mReversing);
- mSeekState.setPlayTime(playTime, mReversing);
- } else {
- // If the animation is running, just set the seek time and wait until the next frame
- // (i.e. doAnimationFrame(...)) to advance the animation.
- mSeekState.setPlayTime(playTime, mReversing);
}
+ animateBasedOnPlayTime(playTime, lastPlayTime, mReversing, true);
+ mSeekState.setPlayTime(playTime, mReversing);
}
/**
@@ -1101,7 +1161,7 @@
long previousTime = -1;
for (long time : times) {
- animateBasedOnPlayTime(time, previousTime, false);
+ animateBasedOnPlayTime(time, previousTime, false, false);
previousTime = time;
}
}
@@ -1397,15 +1457,7 @@
// No longer receive callbacks
removeAnimationCallback();
- // Call end listener
- if (mListeners != null) {
- ArrayList<AnimatorListener> tmpListeners =
- (ArrayList<AnimatorListener>) mListeners.clone();
- int numListeners = tmpListeners.size();
- for (int i = 0; i < numListeners; ++i) {
- tmpListeners.get(i).onAnimationEnd(this, mReversing);
- }
- }
+ notifyEndListeners(mReversing);
removeAnimationEndListener();
mSelfPulse = true;
mReversing = false;
diff --git a/core/java/android/animation/ValueAnimator.java b/core/java/android/animation/ValueAnimator.java
index d41c03d..7009725 100644
--- a/core/java/android/animation/ValueAnimator.java
+++ b/core/java/android/animation/ValueAnimator.java
@@ -1108,18 +1108,30 @@
}
}
- private void notifyStartListeners() {
+ private void notifyStartListeners(boolean isReversing) {
if (mListeners != null && !mStartListenersCalled) {
ArrayList<AnimatorListener> tmpListeners =
(ArrayList<AnimatorListener>) mListeners.clone();
int numListeners = tmpListeners.size();
for (int i = 0; i < numListeners; ++i) {
- tmpListeners.get(i).onAnimationStart(this, mReversing);
+ tmpListeners.get(i).onAnimationStart(this, isReversing);
}
}
mStartListenersCalled = true;
}
+ private void notifyEndListeners(boolean isReversing) {
+ if (mListeners != null && mStartListenersCalled) {
+ ArrayList<AnimatorListener> tmpListeners =
+ (ArrayList<AnimatorListener>) mListeners.clone();
+ int numListeners = tmpListeners.size();
+ for (int i = 0; i < numListeners; ++i) {
+ tmpListeners.get(i).onAnimationEnd(this, isReversing);
+ }
+ }
+ mStartListenersCalled = false;
+ }
+
/**
* Start the animation playing. This version of start() takes a boolean flag that indicates
* whether the animation should play in reverse. The flag is usually false, but may be set
@@ -1210,7 +1222,7 @@
if ((mStarted || mRunning) && mListeners != null) {
if (!mRunning) {
// If it's not yet running, then start listeners weren't called. Call them now.
- notifyStartListeners();
+ notifyStartListeners(mReversing);
}
int listenersSize = mListeners.size();
if (listenersSize > 0) {
@@ -1324,22 +1336,14 @@
boolean notify = (mStarted || mRunning) && mListeners != null;
if (notify && !mRunning) {
// If it's not yet running, then start listeners weren't called. Call them now.
- notifyStartListeners();
+ notifyStartListeners(mReversing);
}
mRunning = false;
mStarted = false;
- mStartListenersCalled = false;
mLastFrameTime = -1;
mFirstFrameTime = -1;
mStartTime = -1;
- if (notify && mListeners != null) {
- ArrayList<AnimatorListener> tmpListeners =
- (ArrayList<AnimatorListener>) mListeners.clone();
- int numListeners = tmpListeners.size();
- for (int i = 0; i < numListeners; ++i) {
- tmpListeners.get(i).onAnimationEnd(this, mReversing);
- }
- }
+ notifyEndListeners(mReversing);
// mReversing needs to be reset *after* notifying the listeners for the end callbacks.
mReversing = false;
if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
@@ -1366,9 +1370,8 @@
} else {
mOverallFraction = 0f;
}
- if (mListeners != null) {
- notifyStartListeners();
- }
+
+ notifyStartListeners(mReversing);
}
/**
@@ -1459,13 +1462,22 @@
* will be called.
*/
@Override
- void animateValuesInRange(long currentPlayTime, long lastPlayTime) {
- if (currentPlayTime < mStartDelay || lastPlayTime < -1) {
+ void animateValuesInRange(long currentPlayTime, long lastPlayTime, boolean notify) {
+ if (currentPlayTime < 0 || lastPlayTime < -1) {
throw new UnsupportedOperationException("Error: Play time should never be negative.");
}
initAnimation();
long duration = getTotalDuration();
+ if (notify) {
+ if (lastPlayTime < 0 || (lastPlayTime == 0 && currentPlayTime > 0)) {
+ notifyStartListeners(false);
+ } else if (lastPlayTime > duration
+ || (lastPlayTime == duration && currentPlayTime < duration)
+ ) {
+ notifyStartListeners(true);
+ }
+ }
if (duration >= 0) {
lastPlayTime = Math.min(duration, lastPlayTime);
}
@@ -1474,8 +1486,8 @@
// Check whether repeat callback is needed only when repeat count is non-zero
if (mRepeatCount > 0) {
- int iteration = (int) (currentPlayTime / mDuration);
- int lastIteration = (int) (lastPlayTime / mDuration);
+ int iteration = Math.max(0, (int) (currentPlayTime / mDuration));
+ int lastIteration = Math.max(0, (int) (lastPlayTime / mDuration));
// Clamp iteration to [0, mRepeatCount]
iteration = Math.min(iteration, mRepeatCount);
@@ -1491,24 +1503,33 @@
}
}
- if (mRepeatCount != INFINITE && currentPlayTime >= (mRepeatCount + 1) * mDuration) {
+ if (mRepeatCount != INFINITE && currentPlayTime > (mRepeatCount + 1) * mDuration) {
throw new IllegalStateException("Can't animate a value outside of the duration");
} else {
// Find the current fraction:
- float fraction = currentPlayTime / (float) mDuration;
+ float fraction = Math.max(0, currentPlayTime) / (float) mDuration;
fraction = getCurrentIterationFraction(fraction, false);
animateValue(fraction);
}
}
@Override
- void animateSkipToEnds(long currentPlayTime, long lastPlayTime) {
- if (currentPlayTime <= mStartDelay && lastPlayTime > mStartDelay) {
- skipToEndValue(true);
+ void animateSkipToEnds(long currentPlayTime, long lastPlayTime, boolean notify) {
+ boolean inReverse = currentPlayTime < lastPlayTime;
+ boolean doSkip;
+ if (currentPlayTime <= 0 && lastPlayTime > 0) {
+ doSkip = true;
} else {
long duration = getTotalDuration();
- if (duration >= 0 && currentPlayTime >= duration && lastPlayTime < duration) {
- skipToEndValue(false);
+ doSkip = duration >= 0 && currentPlayTime >= duration && lastPlayTime < duration;
+ }
+ if (doSkip) {
+ if (notify) {
+ notifyStartListeners(inReverse);
+ }
+ skipToEndValue(inReverse);
+ if (notify) {
+ notifyEndListeners(inReverse);
}
}
}
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index da88f4b..32d88b2 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -2761,6 +2761,7 @@
getOnBackInvokedDispatcher().unregisterOnBackInvokedCallback(mDefaultBackCallback);
mDefaultBackCallback = null;
}
+
if (mCallbacksController != null) {
mCallbacksController.clearCallbacks();
}
@@ -8338,6 +8339,7 @@
attachBaseContext(context);
mFragments.attachHost(null /*parent*/);
+ mActivityInfo = info;
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(mWindowControllerCallback);
@@ -8362,7 +8364,6 @@
mIntent = intent;
mReferrer = referrer;
mComponent = intent.getComponent();
- mActivityInfo = info;
mTitle = title;
mParent = parent;
mEmbeddedID = id;
diff --git a/core/java/android/app/ActivityClient.java b/core/java/android/app/ActivityClient.java
index ce99119..558dae5 100644
--- a/core/java/android/app/ActivityClient.java
+++ b/core/java/android/app/ActivityClient.java
@@ -59,6 +59,15 @@
}
}
+ /** Reports {@link android.app.servertransaction.RefreshCallbackItem} is executed. */
+ public void activityRefreshed(IBinder token) {
+ try {
+ getActivityClientController().activityRefreshed(token);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
/**
* Reports after {@link Activity#onTopResumedActivityChanged(boolean)} is called for losing the
* top most position.
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index cf07114..174c982 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -4380,7 +4380,7 @@
*
* <p>This method will allow the user to launch activities on that display, and it's typically
* used only on automotive builds when the vehicle has multiple displays (you can verify if it's
- * supported by calling {@link UserManager#isUsersOnSecondaryDisplaysSupported()}).
+ * supported by calling {@link UserManager#isVisibleBackgroundUsersSupported()}).
*
* <p><b>NOTE:</b> differently from {@link #switchUser(int)}, which stops the current foreground
* user before starting a new one, this method does not stop the previous user running in
@@ -4404,14 +4404,13 @@
@TestApi
@RequiresPermission(anyOf = {android.Manifest.permission.MANAGE_USERS,
android.Manifest.permission.INTERACT_ACROSS_USERS})
- public boolean startUserInBackgroundOnSecondaryDisplay(@UserIdInt int userId,
- int displayId) {
- if (!UserManager.isUsersOnSecondaryDisplaysEnabled()) {
+ public boolean startUserInBackgroundVisibleOnDisplay(@UserIdInt int userId, int displayId) {
+ if (!UserManager.isVisibleBackgroundUsersEnabled()) {
throw new UnsupportedOperationException(
"device does not support users on secondary displays");
}
try {
- return getService().startUserInBackgroundOnSecondaryDisplay(userId, displayId);
+ return getService().startUserInBackgroundVisibleOnDisplay(userId, displayId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -4427,9 +4426,9 @@
@Nullable
@RequiresPermission(anyOf = {android.Manifest.permission.MANAGE_USERS,
android.Manifest.permission.INTERACT_ACROSS_USERS})
- public int[] getSecondaryDisplayIdsForStartingBackgroundUsers() {
+ public int[] getDisplayIdsForStartingVisibleBackgroundUsers() {
try {
- return getService().getSecondaryDisplayIdsForStartingBackgroundUsers();
+ return getService().getDisplayIdsForStartingVisibleBackgroundUsers();
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -4589,7 +4588,7 @@
* Stops the given {@code userId}.
*
* <p><b>NOTE:</b> on systems that support
- * {@link UserManager#isUsersOnSecondaryDisplaysSupported() background users on secondary
+ * {@link UserManager#isVisibleBackgroundUsersSupported() background users on secondary
* displays}, this method will also unassign the user from the display it was started on.
*
* @hide
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 37749e6..afd8a52 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -62,6 +62,7 @@
import android.app.servertransaction.TransactionExecutor;
import android.app.servertransaction.TransactionExecutorHelper;
import android.bluetooth.BluetoothFrameworkInitializer;
+import android.companion.virtual.VirtualDeviceManager;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.AttributionSource;
import android.content.AutofillOptions;
@@ -361,6 +362,8 @@
private int mLastProcessState = PROCESS_STATE_UNKNOWN;
ArrayList<WeakReference<AssistStructure>> mLastAssistStructures = new ArrayList<>();
private int mLastSessionId;
+ // Holds the value of the last reported device ID value from the server for the top activity.
+ int mLastReportedDeviceId;
final ArrayMap<IBinder, CreateServiceData> mServicesData = new ArrayMap<>();
@UnsupportedAppUsage
final ArrayMap<IBinder, Service> mServices = new ArrayMap<>();
@@ -546,6 +549,9 @@
boolean hideForNow;
Configuration createdConfig;
Configuration overrideConfig;
+ // TODO(b/263402465): pass deviceId directly in LaunchActivityItem#execute
+ // The deviceId assigned by the server when this activity was first started.
+ int mDeviceId;
// Used for consolidating configs before sending on to Activity.
private Configuration tmpConfig = new Configuration();
// Callback used for updating activity override config and camera compat control state.
@@ -608,7 +614,7 @@
}
public ActivityClientRecord(IBinder token, Intent intent, int ident,
- ActivityInfo info, Configuration overrideConfig,
+ ActivityInfo info, Configuration overrideConfig, int deviceId,
String referrer, IVoiceInteractor voiceInteractor, Bundle state,
PersistableBundle persistentState, List<ResultInfo> pendingResults,
List<ReferrerIntent> pendingNewIntents, ActivityOptions activityOptions,
@@ -630,6 +636,7 @@
this.isForward = isForward;
this.profilerInfo = profilerInfo;
this.overrideConfig = overrideConfig;
+ this.mDeviceId = deviceId;
this.packageInfo = client.getPackageInfoNoCheck(activityInfo.applicationInfo);
mActivityOptions = activityOptions;
mLaunchedFromBubble = launchedFromBubble;
@@ -3816,6 +3823,7 @@
// Make sure we are running with the most recent config.
mConfigurationController.handleConfigurationChanged(null, null);
+ updateDeviceIdForNonUIContexts(r.mDeviceId);
if (localLOGV) Slog.v(
TAG, "Handling launch of " + r);
@@ -4219,7 +4227,8 @@
private void scheduleResume(ActivityClientRecord r) {
final ClientTransaction transaction = ClientTransaction.obtain(this.mAppThread, r.token);
- transaction.setLifecycleStateRequest(ResumeActivityItem.obtain(/* isForward */ false));
+ transaction.setLifecycleStateRequest(ResumeActivityItem.obtain(/* isForward */ false,
+ /* shouldSendCompatFakeFocus */ false));
executeTransaction(transaction);
}
@@ -4548,6 +4557,9 @@
context.setOuterContext(service);
service.attach(context, this, data.info.name, data.token, app,
ActivityManager.getService());
+ if (!service.isUiContext()) { // WindowProviderService is a UI Context.
+ service.updateDeviceId(mLastReportedDeviceId);
+ }
service.onCreate();
mServicesData.put(data.token, data);
mServices.put(data.token, service);
@@ -4884,7 +4896,7 @@
@Override
public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,
- boolean isForward, String reason) {
+ boolean isForward, boolean shouldSendCompatFakeFocus, String reason) {
// If we are getting ready to gc after going to the background, well
// we are back active so skip it.
unscheduleGcIdler();
@@ -4991,6 +5003,16 @@
if (r.activity.mVisibleFromClient) {
r.activity.makeVisible();
}
+
+ if (shouldSendCompatFakeFocus) {
+ // Attaching to a window is asynchronous with the activity being resumed,
+ // so it's possible we will need to send a fake focus event after attaching
+ if (impl != null) {
+ impl.dispatchCompatFakeFocus();
+ } else {
+ r.window.getDecorView().fakeFocusAfterAttachingToWindow();
+ }
+ }
}
r.nextIdle = mNewActivities;
@@ -5342,6 +5364,11 @@
}
}
+ @Override
+ public void reportRefresh(ActivityClientRecord r) {
+ ActivityClient.getInstance().activityRefreshed(r.token);
+ }
+
private void handleSetCoreSettings(Bundle coreSettings) {
synchronized (mCoreSettingsLock) {
mCoreSettings = coreSettings;
@@ -6061,9 +6088,48 @@
}
}
+ private void updateDeviceIdForNonUIContexts(int deviceId) {
+ // Invalid device id is treated as a no-op.
+ if (deviceId == VirtualDeviceManager.DEVICE_ID_INVALID) {
+ return;
+ }
+ if (deviceId == mLastReportedDeviceId) {
+ return;
+ }
+ mLastReportedDeviceId = deviceId;
+ ArrayList<Context> nonUIContexts = new ArrayList<>();
+ // Update Application and Service contexts with implicit device association.
+ // UI Contexts are able to derived their device Id association from the display.
+ synchronized (mResourcesManager) {
+ final int numApps = mAllApplications.size();
+ for (int i = 0; i < numApps; i++) {
+ nonUIContexts.add(mAllApplications.get(i));
+ }
+ final int numServices = mServices.size();
+ for (int i = 0; i < numServices; i++) {
+ final Service service = mServices.valueAt(i);
+ // WindowProviderService is a UI Context.
+ if (!service.isUiContext()) {
+ nonUIContexts.add(service);
+ }
+ }
+ }
+ for (Context context : nonUIContexts) {
+ try {
+ context.updateDeviceId(deviceId);
+ } catch (IllegalArgumentException e) {
+ // It can happen that the system already closed/removed a virtual device
+ // and the passed deviceId is no longer valid.
+ // TODO(b/263355088): check for validity of deviceId before updating
+ // instead of catching this exception once VDM add an API to validate ids.
+ }
+ }
+ }
+
@Override
- public void handleConfigurationChanged(Configuration config) {
+ public void handleConfigurationChanged(Configuration config, int deviceId) {
mConfigurationController.handleConfigurationChanged(config);
+ updateDeviceIdForNonUIContexts(deviceId);
// These are only done to maintain @UnsupportedAppUsage and should be removed someday.
mCurDefaultDisplayDpi = mConfigurationController.getCurDefaultDisplayDpi();
diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java
index 309b253..a7a4b35 100644
--- a/core/java/android/app/ApplicationPackageManager.java
+++ b/core/java/android/app/ApplicationPackageManager.java
@@ -3875,4 +3875,19 @@
throw e.rethrowAsRuntimeException();
}
}
+
+ @Override
+ public boolean canUserUninstall(String packageName, UserHandle user) {
+ try {
+ return mPM.getBlockUninstallForUser(packageName, user.getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+ }
+
+ @Override
+ public boolean shouldShowNewAppInstalledNotification() {
+ return Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.SHOW_NEW_APP_INSTALLED_NOTIFICATION_ENABLED, 0) == 1;
+ }
}
diff --git a/core/java/android/app/BroadcastOptions.java b/core/java/android/app/BroadcastOptions.java
index 38855c0..e202760 100644
--- a/core/java/android/app/BroadcastOptions.java
+++ b/core/java/android/app/BroadcastOptions.java
@@ -268,7 +268,11 @@
return opts;
}
- /** {@hide} */
+ /**
+ * {@hide}
+ * @deprecated use {@link #setDeliveryGroupMatchingFilter(IntentFilter)} instead.
+ */
+ @Deprecated
public static @NonNull BroadcastOptions makeRemovingMatchingFilter(
@NonNull IntentFilter removeMatchingFilter) {
BroadcastOptions opts = new BroadcastOptions();
@@ -703,7 +707,9 @@
* to remove any pending {@link Intent#ACTION_SCREEN_OFF} broadcasts.
*
* @hide
+ * @deprecated use {@link #setDeliveryGroupMatchingFilter(IntentFilter)} instead.
*/
+ @Deprecated
public void setRemoveMatchingFilter(@NonNull IntentFilter removeMatchingFilter) {
mRemoveMatchingFilter = Objects.requireNonNull(removeMatchingFilter);
}
diff --git a/core/java/android/app/ClientTransactionHandler.java b/core/java/android/app/ClientTransactionHandler.java
index f322ca9..419ffac 100644
--- a/core/java/android/app/ClientTransactionHandler.java
+++ b/core/java/android/app/ClientTransactionHandler.java
@@ -108,7 +108,8 @@
* @param reason Reason for performing this operation.
*/
public abstract void handleResumeActivity(@NonNull ActivityClientRecord r,
- boolean finalStateRequest, boolean isForward, String reason);
+ boolean finalStateRequest, boolean isForward, boolean shouldSendCompatFakeFocus,
+ String reason);
/**
* Notify the activity about top resumed state change.
@@ -139,6 +140,9 @@
/** Restart the activity after it was stopped. */
public abstract void performRestartActivity(@NonNull ActivityClientRecord r, boolean start);
+ /** Report that activity was refreshed to server. */
+ public abstract void reportRefresh(@NonNull ActivityClientRecord r);
+
/** Set pending activity configuration in case it will be updated by other transaction item. */
public abstract void updatePendingActivityConfiguration(@NonNull IBinder token,
Configuration overrideConfig);
@@ -181,8 +185,8 @@
/** Get package info. */
public abstract LoadedApk getPackageInfoNoCheck(ApplicationInfo ai);
- /** Deliver app configuration change notification. */
- public abstract void handleConfigurationChanged(Configuration config);
+ /** Deliver app configuration change notification and device association. */
+ public abstract void handleConfigurationChanged(Configuration config, int deviceId);
/**
* Get {@link android.app.ActivityThread.ActivityClientRecord} instance that corresponds to the
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index a832b9a..1120257 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -2710,7 +2710,7 @@
context.setResources(createResources(mToken, mPackageInfo, mSplitName, displayId,
overrideConfig, display.getDisplayAdjustments().getCompatibilityInfo(),
mResources.getLoaders()));
- context.mDisplay = display;
+ context.setDisplay(display);
// Inherit context type if the container is from System or System UI context to bypass
// UI context check.
context.mContextType = mContextType == CONTEXT_TYPE_SYSTEM_OR_SYSTEM_UI
@@ -2726,6 +2726,13 @@
return context;
}
+ private void setDisplay(Display display) {
+ mDisplay = display;
+ if (display != null) {
+ updateDeviceIdIfChanged(display.getDisplayId());
+ }
+ }
+
@Override
public @NonNull Context createDeviceContext(int deviceId) {
if (!isValidDeviceId(deviceId)) {
@@ -2863,8 +2870,8 @@
baseContext.setResources(windowContextResources);
// Associate the display with window context resources so that configuration update from
// the server side will also apply to the display's metrics.
- baseContext.mDisplay = ResourcesManager.getInstance().getAdjustedDisplay(displayId,
- windowContextResources);
+ baseContext.setDisplay(ResourcesManager.getInstance().getAdjustedDisplay(
+ displayId, windowContextResources));
return baseContext;
}
@@ -3008,11 +3015,24 @@
@Override
public void updateDisplay(int displayId) {
- mDisplay = mResourcesManager.getAdjustedDisplay(displayId, mResources);
+ setDisplay(mResourcesManager.getAdjustedDisplay(displayId, mResources));
if (mContextType == CONTEXT_TYPE_NON_UI) {
mContextType = CONTEXT_TYPE_DISPLAY_CONTEXT;
}
- // TODO(b/253201821): Update deviceId when display is updated.
+ }
+
+ private void updateDeviceIdIfChanged(int displayId) {
+ if (mIsExplicitDeviceId) {
+ return;
+ }
+ VirtualDeviceManager vdm = getSystemService(VirtualDeviceManager.class);
+ if (vdm != null) {
+ int deviceId = vdm.getDeviceIdForDisplayId(displayId);
+ if (deviceId != mDeviceId) {
+ mDeviceId = deviceId;
+ notifyOnDeviceChangedListeners(mDeviceId);
+ }
+ }
}
@Override
@@ -3307,8 +3327,8 @@
classLoader,
packageInfo.getApplication() == null ? null
: packageInfo.getApplication().getResources().getLoaders()));
- context.mDisplay = resourcesManager.getAdjustedDisplay(displayId,
- context.getResources());
+ context.setDisplay(resourcesManager.getAdjustedDisplay(
+ displayId, context.getResources()));
return context;
}
diff --git a/core/java/android/app/GrammaticalInflectionManager.java b/core/java/android/app/GrammaticalInflectionManager.java
new file mode 100644
index 0000000..1905b6a
--- /dev/null
+++ b/core/java/android/app/GrammaticalInflectionManager.java
@@ -0,0 +1,93 @@
+/*
+ * 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 android.annotation.SystemService;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.os.RemoteException;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * This class allow applications to control granular grammatical inflection settings (such as
+ * per-app grammatical gender).
+ */
+@SystemService(Context.GRAMMATICAL_INFLECTION_SERVICE)
+public class GrammaticalInflectionManager {
+ private static final Set<Integer> VALID_GENDER_VALUES = new HashSet<>(Arrays.asList(
+ Configuration.GRAMMATICAL_GENDER_NOT_SPECIFIED,
+ Configuration.GRAMMATICAL_GENDER_NEUTRAL,
+ Configuration.GRAMMATICAL_GENDER_FEMININE,
+ Configuration.GRAMMATICAL_GENDER_MASCULINE));
+
+ private final Context mContext;
+ private final IGrammaticalInflectionManager mService;
+
+ /** @hide Instantiated by ContextImpl */
+ public GrammaticalInflectionManager(Context context, IGrammaticalInflectionManager service) {
+ mContext = context;
+ mService = service;
+ }
+
+ /**
+ * Returns the current grammatical gender for the calling app. A new value can be requested via
+ * {@link #setRequestedApplicationGrammaticalGender(int)} and will be updated with a new
+ * configuration change. The method always returns the value received with the last received
+ * configuration change.
+ *
+ * @return the value of grammatical gender
+ * @see Configuration#getGrammaticalGender
+ */
+ @Configuration.GrammaticalGender
+ public int getApplicationGrammaticalGender() {
+ return mContext.getApplicationContext()
+ .getResources()
+ .getConfiguration()
+ .getGrammaticalGender();
+ }
+
+ /**
+ * Sets the current grammatical gender for the calling app (keyed by package name and user ID
+ * retrieved from the calling pid).
+ *
+ * <p><b>Note:</b> Changes to app grammatical gender will result in a configuration change (and
+ * potentially an Activity re-creation) being applied to the specified application. For more
+ * information, see the <a
+ * href="https://developer.android.com/guide/topics/resources/runtime-changes">section on
+ * handling configuration changes</a>. The set grammatical gender are persisted across
+ * application restarts; they are backed up if the user has enabled Backup & Restore.`
+ *
+ * @param grammaticalGender the terms of address the user preferred in an application.
+ * @see Configuration#getGrammaticalGender
+ */
+ public void setRequestedApplicationGrammaticalGender(
+ @Configuration.GrammaticalGender int grammaticalGender) {
+ if (!VALID_GENDER_VALUES.contains(grammaticalGender)) {
+ throw new IllegalArgumentException("Unknown grammatical gender");
+ }
+
+ try {
+ mService.setRequestedApplicationGrammaticalGender(
+ mContext.getPackageName(), mContext.getUserId(), grammaticalGender);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+}
diff --git a/core/java/android/app/IActivityClientController.aidl b/core/java/android/app/IActivityClientController.aidl
index 286b84c..ecea46a 100644
--- a/core/java/android/app/IActivityClientController.aidl
+++ b/core/java/android/app/IActivityClientController.aidl
@@ -39,6 +39,7 @@
interface IActivityClientController {
oneway void activityIdle(in IBinder token, in Configuration config, in boolean stopProfiling);
oneway void activityResumed(in IBinder token, in boolean handleSplashScreenExit);
+ oneway void activityRefreshed(in IBinder token);
/**
* This call is not one-way because {@link #activityPaused()) is not one-way, or
* the top-resumed-lost could be reported after activity paused.
diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl
index 6b6b820..0866d94 100644
--- a/core/java/android/app/IActivityManager.aidl
+++ b/core/java/android/app/IActivityManager.aidl
@@ -799,14 +799,14 @@
*/
@JavaPassthrough(annotation=
"@android.annotation.RequiresPermission(anyOf = {android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}, conditional = true)")
- boolean startUserInBackgroundOnSecondaryDisplay(int userid, int displayId);
+ boolean startUserInBackgroundVisibleOnDisplay(int userid, int displayId);
/**
- * Gets the ids of displays that can be used on {@link #startUserInBackgroundOnSecondaryDisplay(int userId, int displayId)}.
+ * Gets the ids of displays that can be used on {@link #startUserInBackgroundVisibleOnDisplay(int userId, int displayId)}.
*
* <p>Typically used only by automotive builds when the vehicle has multiple displays.
*/
- @nullable int[] getSecondaryDisplayIdsForStartingBackgroundUsers();
+ @nullable int[] getDisplayIdsForStartingVisibleBackgroundUsers();
/** Returns if the service is a short-service is still "alive" and past the timeout. */
boolean shouldServiceTimeOut(in ComponentName className, in IBinder token);
diff --git a/core/java/android/app/IActivityTaskManager.aidl b/core/java/android/app/IActivityTaskManager.aidl
index f20503c..461aa3c 100644
--- a/core/java/android/app/IActivityTaskManager.aidl
+++ b/core/java/android/app/IActivityTaskManager.aidl
@@ -198,9 +198,8 @@
* @param taskId The id of the task to set the bounds for.
* @param bounds The new bounds.
* @param resizeMode Resize mode defined as {@code ActivityTaskManager#RESIZE_MODE_*} constants.
- * @return Return true on success. Otherwise false.
*/
- boolean resizeTask(int taskId, in Rect bounds, int resizeMode);
+ void resizeTask(int taskId, in Rect bounds, int resizeMode);
void moveRootTaskToDisplay(int taskId, int displayId);
void moveTaskToRootTask(int taskId, int rootTaskId, boolean toTop);
diff --git a/core/java/android/app/IGrammaticalInflectionManager.aidl b/core/java/android/app/IGrammaticalInflectionManager.aidl
new file mode 100644
index 0000000..9366a45
--- /dev/null
+++ b/core/java/android/app/IGrammaticalInflectionManager.aidl
@@ -0,0 +1,19 @@
+package android.app;
+
+
+/**
+ * Internal interface used to control app-specific gender.
+ *
+ * <p>Use the {@link android.app.GrammarInflectionManager} class rather than going through
+ * this Binder interface directly. See {@link android.app.GrammarInflectionManager} for
+ * more complete documentation.
+ *
+ * @hide
+ */
+ interface IGrammaticalInflectionManager {
+
+ /**
+ * Sets a specified app’s app-specific grammatical gender.
+ */
+ void setRequestedApplicationGrammaticalGender(String appPackageName, int userId, int gender);
+ }
\ No newline at end of file
diff --git a/core/java/android/app/LocaleConfig.java b/core/java/android/app/LocaleConfig.java
index 5d50d29..50ba7db 100644
--- a/core/java/android/app/LocaleConfig.java
+++ b/core/java/android/app/LocaleConfig.java
@@ -64,7 +64,7 @@
public static final String TAG_LOCALE_CONFIG = "locale-config";
public static final String TAG_LOCALE = "locale";
private LocaleList mLocales;
- private int mStatus;
+ private int mStatus = STATUS_NOT_SPECIFIED;
/**
* succeeded reading the LocaleConfig structure stored in an XML file.
@@ -119,6 +119,7 @@
LocaleManager localeManager = context.getSystemService(LocaleManager.class);
if (localeManager == null) {
Slog.w(TAG, "LocaleManager is null, cannot get the override LocaleConfig");
+ mStatus = STATUS_NOT_SPECIFIED;
return;
}
LocaleConfig localeConfig = localeManager.getOverrideLocaleConfig();
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index df13a87..5b3b2a6 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -1547,6 +1547,7 @@
IAmbientContextManager.Stub.asInterface(iBinder);
return new AmbientContextManager(ctx.getOuterContext(), manager);
}});
+
registerService(Context.WEARABLE_SENSING_SERVICE, WearableSensingManager.class,
new CachedServiceFetcher<WearableSensingManager>() {
@Override
@@ -1559,6 +1560,18 @@
return new WearableSensingManager(ctx.getOuterContext(), manager);
}});
+ registerService(Context.GRAMMATICAL_INFLECTION_SERVICE, GrammaticalInflectionManager.class,
+ new CachedServiceFetcher<GrammaticalInflectionManager>() {
+ @Override
+ public GrammaticalInflectionManager createService(ContextImpl ctx)
+ throws ServiceNotFoundException {
+ return new GrammaticalInflectionManager(ctx,
+ IGrammaticalInflectionManager.Stub.asInterface(
+ ServiceManager.getServiceOrThrow(
+ Context.GRAMMATICAL_INFLECTION_SERVICE)));
+ }});
+
+
sInitializing = true;
try {
// Note: the following functions need to be @SystemApis, once they become mainline
diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java
index e655209..1187459 100644
--- a/core/java/android/app/WallpaperManager.java
+++ b/core/java/android/app/WallpaperManager.java
@@ -115,6 +115,9 @@
private static final @NonNull RectF LOCAL_COLOR_BOUNDS =
new RectF(0, 0, 1, 1);
+ /** Temporary feature flag for project b/197814683 */
+ private final boolean mLockscreenLiveWallpaper;
+
/** {@hide} */
private static final String PROP_WALLPAPER = "ro.config.wallpaper";
/** {@hide} */
@@ -750,6 +753,8 @@
mWcgEnabled = context.getResources().getConfiguration().isScreenWideColorGamut()
&& context.getResources().getBoolean(R.bool.config_enableWcgMode);
mCmProxy = new ColorManagementProxy(context);
+ mLockscreenLiveWallpaper = context.getResources()
+ .getBoolean(R.bool.config_independentLockscreenLiveWallpaper);
}
// no-op constructor called just by DisabledWallpaperManager
@@ -757,6 +762,7 @@
mContext = null;
mCmProxy = null;
mWcgEnabled = false;
+ mLockscreenLiveWallpaper = false;
}
/**
@@ -774,6 +780,15 @@
}
/**
+ * Temporary method for project b/197814683.
+ * @return true if the lockscreen wallpaper always uses a wallpaperService, not a static image
+ * @hide
+ */
+ public boolean isLockscreenLiveWallpaperEnabled() {
+ return mLockscreenLiveWallpaper;
+ }
+
+ /**
* Indicate whether wcg (Wide Color Gamut) should be enabled.
* <p>
* Some devices lack of capability of mixed color spaces composition,
diff --git a/core/java/android/app/ambientcontext/AmbientContextEvent.java b/core/java/android/app/ambientcontext/AmbientContextEvent.java
index 865e1fb..a6595fe 100644
--- a/core/java/android/app/ambientcontext/AmbientContextEvent.java
+++ b/core/java/android/app/ambientcontext/AmbientContextEvent.java
@@ -20,6 +20,7 @@
import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.os.Parcelable;
+import android.os.PersistableBundle;
import com.android.internal.util.DataClass;
import com.android.internal.util.Parcelling;
@@ -66,12 +67,25 @@
*/
public static final int EVENT_BACK_DOUBLE_TAP = 3;
+ /**
+ * Integer indicating the start of wearable vendor defined events that can be detected.
+ * These depend on the vendor implementation.
+ */
+ public static final int EVENT_VENDOR_WEARABLE_START = 100000;
+
+ /**
+ * Name for the mVendorData object for this AmbientContextEvent. The mVendorData must be present
+ * in the object, or it will be rejected.
+ */
+ public static final String KEY_VENDOR_WEARABLE_EVENT_NAME = "wearable_event_name";
+
/** @hide */
@IntDef(prefix = { "EVENT_" }, value = {
EVENT_UNKNOWN,
EVENT_COUGH,
EVENT_SNORE,
EVENT_BACK_DOUBLE_TAP,
+ EVENT_VENDOR_WEARABLE_START,
}) public @interface EventCode {}
/** The integer indicating an unknown level. */
@@ -139,6 +153,19 @@
return LEVEL_UNKNOWN;
}
+ /**
+ * Vendor defined specific values for vendor event types.
+ *
+ * <p> The use of this vendor data is discouraged. For data defined in the range above
+ * {@code EVENT_VENDOR_WEARABLE_START} this bundle must include the
+ * {@link KEY_VENDOR_WEARABLE_EVENT_NAME} field or it will be rejected. In addition, to increase
+ * transparency of this data contents of this bundle will be logged to logcat.</p>
+ */
+ private final @NonNull PersistableBundle mVendorData;
+ private static PersistableBundle defaultVendorData() {
+ return new PersistableBundle();
+ }
+
// Code below generated by codegen v1.0.23.
@@ -159,7 +186,8 @@
EVENT_UNKNOWN,
EVENT_COUGH,
EVENT_SNORE,
- EVENT_BACK_DOUBLE_TAP
+ EVENT_BACK_DOUBLE_TAP,
+ EVENT_VENDOR_WEARABLE_START
})
@Retention(RetentionPolicy.SOURCE)
@DataClass.Generated.Member
@@ -177,6 +205,8 @@
return "EVENT_SNORE";
case EVENT_BACK_DOUBLE_TAP:
return "EVENT_BACK_DOUBLE_TAP";
+ case EVENT_VENDOR_WEARABLE_START:
+ return "EVENT_VENDOR_WEARABLE_START";
default: return Integer.toHexString(value);
}
}
@@ -220,7 +250,8 @@
@NonNull Instant startTime,
@NonNull Instant endTime,
@LevelValue int confidenceLevel,
- @LevelValue int densityLevel) {
+ @LevelValue int densityLevel,
+ @NonNull PersistableBundle vendorData) {
this.mEventType = eventType;
com.android.internal.util.AnnotationValidations.validate(
EventCode.class, null, mEventType);
@@ -236,6 +267,9 @@
this.mDensityLevel = densityLevel;
com.android.internal.util.AnnotationValidations.validate(
LevelValue.class, null, mDensityLevel);
+ this.mVendorData = vendorData;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mVendorData);
// onConstructed(); // You can define this method to get a callback
}
@@ -279,6 +313,19 @@
return mDensityLevel;
}
+ /**
+ * Vendor defined specific values for vendor event types.
+ *
+ * <p> The use of this vendor data is discouraged. For data defined in the range above
+ * {@code EVENT_VENDOR_WEARABLE_START} this bundle must include the
+ * {@link KEY_VENDOR_WEARABLE_EVENT_NAME} field or it will be rejected. In addition, to increase
+ * transparency of this data contents of this bundle will be logged to logcat.</p>
+ */
+ @DataClass.Generated.Member
+ public @NonNull PersistableBundle getVendorData() {
+ return mVendorData;
+ }
+
@Override
@DataClass.Generated.Member
public String toString() {
@@ -290,7 +337,8 @@
"startTime = " + mStartTime + ", " +
"endTime = " + mEndTime + ", " +
"confidenceLevel = " + mConfidenceLevel + ", " +
- "densityLevel = " + mDensityLevel +
+ "densityLevel = " + mDensityLevel + ", " +
+ "vendorData = " + mVendorData +
" }";
}
@@ -327,6 +375,7 @@
sParcellingForEndTime.parcel(mEndTime, dest, flags);
dest.writeInt(mConfidenceLevel);
dest.writeInt(mDensityLevel);
+ dest.writeTypedObject(mVendorData, flags);
}
@Override
@@ -345,6 +394,7 @@
Instant endTime = sParcellingForEndTime.unparcel(in);
int confidenceLevel = in.readInt();
int densityLevel = in.readInt();
+ PersistableBundle vendorData = (PersistableBundle) in.readTypedObject(PersistableBundle.CREATOR);
this.mEventType = eventType;
com.android.internal.util.AnnotationValidations.validate(
@@ -361,6 +411,9 @@
this.mDensityLevel = densityLevel;
com.android.internal.util.AnnotationValidations.validate(
LevelValue.class, null, mDensityLevel);
+ this.mVendorData = vendorData;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mVendorData);
// onConstructed(); // You can define this method to get a callback
}
@@ -391,6 +444,7 @@
private @NonNull Instant mEndTime;
private @LevelValue int mConfidenceLevel;
private @LevelValue int mDensityLevel;
+ private @NonNull PersistableBundle mVendorData;
private long mBuilderFieldsSet = 0L;
@@ -451,10 +505,26 @@
return this;
}
+ /**
+ * Vendor defined specific values for vendor event types.
+ *
+ * <p> The use of this vendor data is discouraged. For data defined in the range above
+ * {@code EVENT_VENDOR_WEARABLE_START} this bundle must include the
+ * {@link KEY_VENDOR_WEARABLE_EVENT_NAME} field or it will be rejected. In addition, to increase
+ * transparency of this data contents of this bundle will be logged to logcat.</p>
+ */
+ @DataClass.Generated.Member
+ public @NonNull Builder setVendorData(@NonNull PersistableBundle value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x20;
+ mVendorData = value;
+ return this;
+ }
+
/** Builds the instance. This builder should not be touched after calling this! */
public @NonNull AmbientContextEvent build() {
checkNotUsed();
- mBuilderFieldsSet |= 0x20; // Mark builder used
+ mBuilderFieldsSet |= 0x40; // Mark builder used
if ((mBuilderFieldsSet & 0x1) == 0) {
mEventType = defaultEventType();
@@ -471,17 +541,21 @@
if ((mBuilderFieldsSet & 0x10) == 0) {
mDensityLevel = defaultDensityLevel();
}
+ if ((mBuilderFieldsSet & 0x20) == 0) {
+ mVendorData = defaultVendorData();
+ }
AmbientContextEvent o = new AmbientContextEvent(
mEventType,
mStartTime,
mEndTime,
mConfidenceLevel,
- mDensityLevel);
+ mDensityLevel,
+ mVendorData);
return o;
}
private void checkNotUsed() {
- if ((mBuilderFieldsSet & 0x20) != 0) {
+ if ((mBuilderFieldsSet & 0x40) != 0) {
throw new IllegalStateException(
"This Builder should not be reused. Use a new Builder instance instead");
}
@@ -489,10 +563,10 @@
}
@DataClass.Generated(
- time = 1659950304931L,
+ time = 1671217108067L,
codegenVersion = "1.0.23",
sourceFile = "frameworks/base/core/java/android/app/ambientcontext/AmbientContextEvent.java",
- inputSignatures = "public static final int EVENT_UNKNOWN\npublic static final int EVENT_COUGH\npublic static final int EVENT_SNORE\npublic static final int EVENT_BACK_DOUBLE_TAP\npublic static final int LEVEL_UNKNOWN\npublic static final int LEVEL_LOW\npublic static final int LEVEL_MEDIUM_LOW\npublic static final int LEVEL_MEDIUM\npublic static final int LEVEL_MEDIUM_HIGH\npublic static final int LEVEL_HIGH\nprivate final @android.app.ambientcontext.AmbientContextEvent.EventCode int mEventType\nprivate final @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInstant.class) @android.annotation.NonNull java.time.Instant mStartTime\nprivate final @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInstant.class) @android.annotation.NonNull java.time.Instant mEndTime\nprivate final @android.app.ambientcontext.AmbientContextEvent.LevelValue int mConfidenceLevel\nprivate final @android.app.ambientcontext.AmbientContextEvent.LevelValue int mDensityLevel\nprivate static int defaultEventType()\nprivate static @android.annotation.NonNull java.time.Instant defaultStartTime()\nprivate static @android.annotation.NonNull java.time.Instant defaultEndTime()\nprivate static int defaultConfidenceLevel()\nprivate static int defaultDensityLevel()\nclass AmbientContextEvent extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genBuilder=true, genConstructor=false, genHiddenConstDefs=true, genParcelable=true, genToString=true)")
+ inputSignatures = "public static final int EVENT_UNKNOWN\npublic static final int EVENT_COUGH\npublic static final int EVENT_SNORE\npublic static final int EVENT_BACK_DOUBLE_TAP\npublic static final int EVENT_VENDOR_WEARABLE_START\npublic static final java.lang.String KEY_VENDOR_WEARABLE_EVENT_NAME\npublic static final int LEVEL_UNKNOWN\npublic static final int LEVEL_LOW\npublic static final int LEVEL_MEDIUM_LOW\npublic static final int LEVEL_MEDIUM\npublic static final int LEVEL_MEDIUM_HIGH\npublic static final int LEVEL_HIGH\nprivate final @android.app.ambientcontext.AmbientContextEvent.EventCode int mEventType\nprivate final @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInstant.class) @android.annotation.NonNull java.time.Instant mStartTime\nprivate final @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInstant.class) @android.annotation.NonNull java.time.Instant mEndTime\nprivate final @android.app.ambientcontext.AmbientContextEvent.LevelValue int mConfidenceLevel\nprivate final @android.app.ambientcontext.AmbientContextEvent.LevelValue int mDensityLevel\nprivate final @android.annotation.NonNull android.os.PersistableBundle mVendorData\nprivate static int defaultEventType()\nprivate static @android.annotation.NonNull java.time.Instant defaultStartTime()\nprivate static @android.annotation.NonNull java.time.Instant defaultEndTime()\nprivate static int defaultConfidenceLevel()\nprivate static int defaultDensityLevel()\nprivate static android.os.PersistableBundle defaultVendorData()\nclass AmbientContextEvent extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genBuilder=true, genConstructor=false, genHiddenConstDefs=true, genParcelable=true, genToString=true)")
@Deprecated
private void __metadata() {}
diff --git a/core/java/android/app/ambientcontext/AmbientContextManager.java b/core/java/android/app/ambientcontext/AmbientContextManager.java
index 9cb1a20..bf383f1 100644
--- a/core/java/android/app/ambientcontext/AmbientContextManager.java
+++ b/core/java/android/app/ambientcontext/AmbientContextManager.java
@@ -117,7 +117,8 @@
*/
@NonNull public static List<AmbientContextEvent> getEventsFromIntent(@NonNull Intent intent) {
if (intent.hasExtra(AmbientContextManager.EXTRA_AMBIENT_CONTEXT_EVENTS)) {
- return intent.getParcelableArrayListExtra(EXTRA_AMBIENT_CONTEXT_EVENTS, android.app.ambientcontext.AmbientContextEvent.class);
+ return intent.getParcelableArrayListExtra(EXTRA_AMBIENT_CONTEXT_EVENTS,
+ android.app.ambientcontext.AmbientContextEvent.class);
} else {
return new ArrayList<>();
}
@@ -143,6 +144,9 @@
* If any of the events are not consented by user, the response has
* {@link AmbientContextManager#STATUS_ACCESS_DENIED}, and the app can
* call {@link #startConsentActivity} to redirect the user to the consent screen.
+ * If the AmbientContextRequest contains a mixed set of events containing values both greater
+ * than and less than {@link AmbientContextEvent.EVENT_VENDOR_WEARABLE_START}, the request
+ * will be rejected with {@link AmbientContextManager#STATUS_NOT_SUPPORTED}.
* <p />
*
* Example:
@@ -197,6 +201,9 @@
/**
* Requests the consent data host to open an activity that allows users to modify consent.
+ * If the eventTypes contains a mixed set of events containing values both greater than and less
+ * than {@link AmbientContextEvent.EVENT_VENDOR_WEARABLE_START}, the request will be rejected
+ * with {@link AmbientContextManager#STATUS_NOT_SUPPORTED}.
*
* @param eventTypes The set of event codes to be consented.
*/
@@ -226,6 +233,9 @@
* observer receives a callback on the provided {@link PendingIntent} when the requested
* event is detected. Registering another observer from the same package that has already been
* registered will override the previous observer.
+ * If the AmbientContextRequest contains a mixed set of events containing values both greater
+ * than and less than {@link AmbientContextEvent.EVENT_VENDOR_WEARABLE_START}, the request
+ * will be rejected with {@link AmbientContextManager#STATUS_NOT_SUPPORTED}.
* <p />
*
* Example:
@@ -308,6 +318,9 @@
* {@link #registerObserver(AmbientContextEventRequest, PendingIntent, Executor, Consumer)},
* the previous observer will be replaced with the new observer with the PendingIntent callback.
* Or vice versa.
+ * If the AmbientContextRequest contains a mixed set of events containing values both greater
+ * than and less than {@link AmbientContextEvent.EVENT_VENDOR_WEARABLE_START}, the request
+ * will be rejected with {@link AmbientContextManager#STATUS_NOT_SUPPORTED}.
*
* When the registration completes, a status will be returned to client through
* {@link AmbientContextCallback#onRegistrationComplete(int)}.
diff --git a/core/java/android/app/backup/BackupManager.java b/core/java/android/app/backup/BackupManager.java
index 7255c3e..bad282e 100644
--- a/core/java/android/app/backup/BackupManager.java
+++ b/core/java/android/app/backup/BackupManager.java
@@ -21,6 +21,7 @@
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
+import android.app.backup.BackupAnnotations.OperationType;
import android.app.compat.CompatChanges;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledAfter;
@@ -1041,6 +1042,42 @@
return backupAgent.getBackupRestoreEventLogger();
}
+ /**
+ * Get an instance of {@link BackupRestoreEventLogger} to report B&R related events during a
+ * delayed restore operation.
+ *
+ * @return an instance of {@link BackupRestoreEventLogger}.
+ *
+ * @hide
+ */
+ @NonNull
+ @SystemApi
+ public BackupRestoreEventLogger getDelayedRestoreLogger() {
+ return new BackupRestoreEventLogger(OperationType.RESTORE);
+ }
+
+ /**
+ * Report B&R related events following a delayed restore operation.
+ *
+ * @param logger an instance of {@link BackupRestoreEventLogger} to which the corresponding
+ * events have been logged.
+ *
+ * @hide
+ */
+ @NonNull
+ @SystemApi
+ public void reportDelayedRestoreResult(@NonNull BackupRestoreEventLogger logger) {
+ checkServiceBinder();
+ if (sService != null) {
+ try {
+ sService.reportDelayedRestoreResult(mContext.getPackageName(),
+ logger.getLoggingResults());
+ } catch (RemoteException e) {
+ Log.w(TAG, "reportDelayedRestoreResult() couldn't connect");
+ }
+ }
+ }
+
/*
* We wrap incoming binder calls with a private class implementation that
* redirects them into main-thread actions. This serializes the backup
diff --git a/core/java/android/app/backup/IBackupManager.aidl b/core/java/android/app/backup/IBackupManager.aidl
index bf5be95..aeb4987 100644
--- a/core/java/android/app/backup/IBackupManager.aidl
+++ b/core/java/android/app/backup/IBackupManager.aidl
@@ -16,6 +16,7 @@
package android.app.backup;
+import android.app.backup.BackupRestoreEventLogger.DataTypeResult;
import android.app.backup.IBackupObserver;
import android.app.backup.IBackupManagerMonitor;
import android.app.backup.IFullBackupRestoreObserver;
@@ -722,4 +723,6 @@
* that have been excluded will be passed to the agent to make it aware of the exclusions.
*/
void excludeKeysFromRestore(String packageName, in List<String> keys);
+
+ void reportDelayedRestoreResult(in String packageName, in List<DataTypeResult> results);
}
diff --git a/core/java/android/app/servertransaction/ClientTransactionItem.java b/core/java/android/app/servertransaction/ClientTransactionItem.java
index d94f08b..b159f33 100644
--- a/core/java/android/app/servertransaction/ClientTransactionItem.java
+++ b/core/java/android/app/servertransaction/ClientTransactionItem.java
@@ -38,6 +38,9 @@
return UNDEFINED;
}
+ boolean shouldHaveDefinedPreExecutionState() {
+ return true;
+ }
// Parcelable
diff --git a/core/java/android/app/servertransaction/ConfigurationChangeItem.java b/core/java/android/app/servertransaction/ConfigurationChangeItem.java
index 49a1c66..a563bbc 100644
--- a/core/java/android/app/servertransaction/ConfigurationChangeItem.java
+++ b/core/java/android/app/servertransaction/ConfigurationChangeItem.java
@@ -32,6 +32,7 @@
public class ConfigurationChangeItem extends ClientTransactionItem {
private Configuration mConfiguration;
+ private int mDeviceId;
@Override
public void preExecute(android.app.ClientTransactionHandler client, IBinder token) {
@@ -42,7 +43,7 @@
@Override
public void execute(ClientTransactionHandler client, IBinder token,
PendingTransactionActions pendingActions) {
- client.handleConfigurationChanged(mConfiguration);
+ client.handleConfigurationChanged(mConfiguration, mDeviceId);
}
@@ -51,12 +52,13 @@
private ConfigurationChangeItem() {}
/** Obtain an instance initialized with provided params. */
- public static ConfigurationChangeItem obtain(Configuration config) {
+ public static ConfigurationChangeItem obtain(Configuration config, int deviceId) {
ConfigurationChangeItem instance = ObjectPool.obtain(ConfigurationChangeItem.class);
if (instance == null) {
instance = new ConfigurationChangeItem();
}
instance.mConfiguration = config;
+ instance.mDeviceId = deviceId;
return instance;
}
@@ -64,6 +66,7 @@
@Override
public void recycle() {
mConfiguration = null;
+ mDeviceId = 0;
ObjectPool.recycle(this);
}
@@ -74,11 +77,13 @@
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeTypedObject(mConfiguration, flags);
+ dest.writeInt(mDeviceId);
}
/** Read from Parcel. */
private ConfigurationChangeItem(Parcel in) {
mConfiguration = in.readTypedObject(Configuration.CREATOR);
+ mDeviceId = in.readInt();
}
public static final @android.annotation.NonNull Creator<ConfigurationChangeItem> CREATOR =
@@ -101,16 +106,20 @@
return false;
}
final ConfigurationChangeItem other = (ConfigurationChangeItem) o;
- return Objects.equals(mConfiguration, other.mConfiguration);
+ return Objects.equals(mConfiguration, other.mConfiguration)
+ && mDeviceId == other.mDeviceId;
}
@Override
public int hashCode() {
- return mConfiguration.hashCode();
+ int result = 17;
+ result = 31 * result + mDeviceId;
+ result = 31 * result + mConfiguration.hashCode();
+ return result;
}
@Override
public String toString() {
- return "ConfigurationChangeItem{config=" + mConfiguration + "}";
+ return "ConfigurationChangeItem{deviceId=" + mDeviceId + ", config" + mConfiguration + "}";
}
}
diff --git a/core/java/android/app/servertransaction/LaunchActivityItem.java b/core/java/android/app/servertransaction/LaunchActivityItem.java
index 7e4db81..3d0aa25 100644
--- a/core/java/android/app/servertransaction/LaunchActivityItem.java
+++ b/core/java/android/app/servertransaction/LaunchActivityItem.java
@@ -58,6 +58,7 @@
private ActivityInfo mInfo;
private Configuration mCurConfig;
private Configuration mOverrideConfig;
+ private int mDeviceId;
private String mReferrer;
private IVoiceInteractor mVoiceInteractor;
private int mProcState;
@@ -95,7 +96,7 @@
PendingTransactionActions pendingActions) {
Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
ActivityClientRecord r = new ActivityClientRecord(token, mIntent, mIdent, mInfo,
- mOverrideConfig, mReferrer, mVoiceInteractor, mState, mPersistentState,
+ mOverrideConfig, mDeviceId, mReferrer, mVoiceInteractor, mState, mPersistentState,
mPendingResults, mPendingNewIntents, mActivityOptions, mIsForward, mProfilerInfo,
client, mAssistToken, mShareableActivityToken, mLaunchedFromBubble,
mTaskFragmentToken);
@@ -116,7 +117,7 @@
/** Obtain an instance initialized with provided params. */
public static LaunchActivityItem obtain(Intent intent, int ident, ActivityInfo info,
- Configuration curConfig, Configuration overrideConfig,
+ Configuration curConfig, Configuration overrideConfig, int deviceId,
String referrer, IVoiceInteractor voiceInteractor, int procState, Bundle state,
PersistableBundle persistentState, List<ResultInfo> pendingResults,
List<ReferrerIntent> pendingNewIntents, ActivityOptions activityOptions,
@@ -127,7 +128,7 @@
if (instance == null) {
instance = new LaunchActivityItem();
}
- setValues(instance, intent, ident, info, curConfig, overrideConfig, referrer,
+ setValues(instance, intent, ident, info, curConfig, overrideConfig, deviceId, referrer,
voiceInteractor, procState, state, persistentState, pendingResults,
pendingNewIntents, activityOptions, isForward, profilerInfo, assistToken,
activityClientController, shareableActivityToken,
@@ -138,7 +139,7 @@
@Override
public void recycle() {
- setValues(this, null, 0, null, null, null, null, null, 0, null, null, null, null,
+ setValues(this, null, 0, null, null, null, 0, null, null, 0, null, null, null, null,
null, false, null, null, null, null, false, null);
ObjectPool.recycle(this);
}
@@ -154,6 +155,7 @@
dest.writeTypedObject(mInfo, flags);
dest.writeTypedObject(mCurConfig, flags);
dest.writeTypedObject(mOverrideConfig, flags);
+ dest.writeInt(mDeviceId);
dest.writeString(mReferrer);
dest.writeStrongInterface(mVoiceInteractor);
dest.writeInt(mProcState);
@@ -175,7 +177,7 @@
private LaunchActivityItem(Parcel in) {
setValues(this, in.readTypedObject(Intent.CREATOR), in.readInt(),
in.readTypedObject(ActivityInfo.CREATOR), in.readTypedObject(Configuration.CREATOR),
- in.readTypedObject(Configuration.CREATOR), in.readString(),
+ in.readTypedObject(Configuration.CREATOR), in.readInt(), in.readString(),
IVoiceInteractor.Stub.asInterface(in.readStrongBinder()), in.readInt(),
in.readBundle(getClass().getClassLoader()),
in.readPersistableBundle(getClass().getClassLoader()),
@@ -215,6 +217,7 @@
return intentsEqual && mIdent == other.mIdent
&& activityInfoEqual(other.mInfo) && Objects.equals(mCurConfig, other.mCurConfig)
&& Objects.equals(mOverrideConfig, other.mOverrideConfig)
+ && mDeviceId == other.mDeviceId
&& Objects.equals(mReferrer, other.mReferrer)
&& mProcState == other.mProcState && areBundlesEqualRoughly(mState, other.mState)
&& areBundlesEqualRoughly(mPersistentState, other.mPersistentState)
@@ -235,6 +238,7 @@
result = 31 * result + mIdent;
result = 31 * result + Objects.hashCode(mCurConfig);
result = 31 * result + Objects.hashCode(mOverrideConfig);
+ result = 31 * result + mDeviceId;
result = 31 * result + Objects.hashCode(mReferrer);
result = 31 * result + Objects.hashCode(mProcState);
result = 31 * result + getRoughBundleHashCode(mState);
@@ -279,16 +283,17 @@
public String toString() {
return "LaunchActivityItem{intent=" + mIntent + ",ident=" + mIdent + ",info=" + mInfo
+ ",curConfig=" + mCurConfig + ",overrideConfig=" + mOverrideConfig
- + ",referrer=" + mReferrer + ",procState=" + mProcState + ",state=" + mState
- + ",persistentState=" + mPersistentState + ",pendingResults=" + mPendingResults
- + ",pendingNewIntents=" + mPendingNewIntents + ",options=" + mActivityOptions
- + ",profilerInfo=" + mProfilerInfo + ",assistToken=" + mAssistToken
- + ",shareableActivityToken=" + mShareableActivityToken + "}";
+ + ",deviceId=" + mDeviceId + ",referrer=" + mReferrer + ",procState=" + mProcState
+ + ",state=" + mState + ",persistentState=" + mPersistentState
+ + ",pendingResults=" + mPendingResults + ",pendingNewIntents=" + mPendingNewIntents
+ + ",options=" + mActivityOptions + ",profilerInfo=" + mProfilerInfo
+ + ",assistToken=" + mAssistToken + ",shareableActivityToken="
+ + mShareableActivityToken + "}";
}
// Using the same method to set and clear values to make sure we don't forget anything
private static void setValues(LaunchActivityItem instance, Intent intent, int ident,
- ActivityInfo info, Configuration curConfig, Configuration overrideConfig,
+ ActivityInfo info, Configuration curConfig, Configuration overrideConfig, int deviceId,
String referrer, IVoiceInteractor voiceInteractor,
int procState, Bundle state, PersistableBundle persistentState,
List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents,
@@ -300,6 +305,7 @@
instance.mInfo = info;
instance.mCurConfig = curConfig;
instance.mOverrideConfig = overrideConfig;
+ instance.mDeviceId = deviceId;
instance.mReferrer = referrer;
instance.mVoiceInteractor = voiceInteractor;
instance.mProcState = procState;
diff --git a/core/java/android/app/servertransaction/RefreshCallbackItem.java b/core/java/android/app/servertransaction/RefreshCallbackItem.java
new file mode 100644
index 0000000..74abab2
--- /dev/null
+++ b/core/java/android/app/servertransaction/RefreshCallbackItem.java
@@ -0,0 +1,145 @@
+/*
+ * 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.servertransaction;
+
+import static android.app.servertransaction.ActivityLifecycleItem.LifecycleState;
+import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE;
+import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityThread.ActivityClientRecord;
+import android.app.ClientTransactionHandler;
+import android.os.IBinder;
+import android.os.Parcel;
+
+/**
+ * Callback that allows to {@link TransactionExecutor#cycleToPath} to {@link ON_PAUSE} or
+ * {@link ON_STOP} in {@link TransactionExecutor#executeCallbacks} for activity "refresh" flow
+ * that goes through "paused -> resumed" or "stopped -> resumed" cycle.
+ *
+ * <p>This is used in combination with {@link com.android.server.wm.DisplayRotationCompatPolicy}
+ * for camera compatibility treatment that handles orientation mismatch between camera buffers and
+ * an app window. This allows to clear cached values in apps (e.g. display or camera rotation) that
+ * influence camera preview and can lead to sideways or stretching issues.
+ *
+ * @hide
+ */
+public class RefreshCallbackItem extends ActivityTransactionItem {
+
+ // Whether refresh should happen using the "stopped -> resumed" cycle or
+ // "paused -> resumed" cycle.
+ @LifecycleState
+ private int mPostExecutionState;
+
+ @Override
+ public void execute(@NonNull ClientTransactionHandler client,
+ @NonNull ActivityClientRecord r, PendingTransactionActions pendingActions) {}
+
+ @Override
+ public void postExecute(ClientTransactionHandler client, IBinder token,
+ PendingTransactionActions pendingActions) {
+ final ActivityClientRecord r = getActivityClientRecord(client, token);
+ client.reportRefresh(r);
+ }
+
+ @Override
+ public int getPostExecutionState() {
+ return mPostExecutionState;
+ }
+
+ @Override
+ boolean shouldHaveDefinedPreExecutionState() {
+ return false;
+ }
+
+ // ObjectPoolItem implementation
+
+ @Override
+ public void recycle() {
+ ObjectPool.recycle(this);
+ }
+
+ /**
+ * Obtain an instance initialized with provided params.
+ * @param postExecutionState indicating whether refresh should happen using the
+ * "stopped -> resumed" cycle or "paused -> resumed" cycle.
+ */
+ public static RefreshCallbackItem obtain(@LifecycleState int postExecutionState) {
+ if (postExecutionState != ON_STOP && postExecutionState != ON_PAUSE) {
+ throw new IllegalArgumentException(
+ "Only ON_STOP or ON_PAUSE are allowed as a post execution state for "
+ + "RefreshCallbackItem but got " + postExecutionState);
+ }
+ RefreshCallbackItem instance =
+ ObjectPool.obtain(RefreshCallbackItem.class);
+ if (instance == null) {
+ instance = new RefreshCallbackItem();
+ }
+ instance.mPostExecutionState = postExecutionState;
+ return instance;
+ }
+
+ private RefreshCallbackItem() {}
+
+ // Parcelable implementation
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mPostExecutionState);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ final RefreshCallbackItem other = (RefreshCallbackItem) o;
+ return mPostExecutionState == other.mPostExecutionState;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + mPostExecutionState;
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "RefreshCallbackItem{mPostExecutionState=" + mPostExecutionState + "}";
+ }
+
+ private RefreshCallbackItem(Parcel in) {
+ mPostExecutionState = in.readInt();
+ }
+
+ public static final @NonNull Creator<RefreshCallbackItem> CREATOR =
+ new Creator<RefreshCallbackItem>() {
+
+ public RefreshCallbackItem createFromParcel(Parcel in) {
+ return new RefreshCallbackItem(in);
+ }
+
+ public RefreshCallbackItem[] newArray(int size) {
+ return new RefreshCallbackItem[size];
+ }
+ };
+}
diff --git a/core/java/android/app/servertransaction/ResumeActivityItem.java b/core/java/android/app/servertransaction/ResumeActivityItem.java
index e6fdc00..222f8ca 100644
--- a/core/java/android/app/servertransaction/ResumeActivityItem.java
+++ b/core/java/android/app/servertransaction/ResumeActivityItem.java
@@ -39,6 +39,9 @@
private int mProcState;
private boolean mUpdateProcState;
private boolean mIsForward;
+ // Whether we should send compat fake focus when the activity is resumed. This is needed
+ // because some game engines wait to get focus before drawing the content of the app.
+ private boolean mShouldSendCompatFakeFocus;
@Override
public void preExecute(ClientTransactionHandler client, IBinder token) {
@@ -52,7 +55,7 @@
PendingTransactionActions pendingActions) {
Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityResume");
client.handleResumeActivity(r, true /* finalStateRequest */, mIsForward,
- "RESUME_ACTIVITY");
+ mShouldSendCompatFakeFocus, "RESUME_ACTIVITY");
Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
}
@@ -74,7 +77,8 @@
private ResumeActivityItem() {}
/** Obtain an instance initialized with provided params. */
- public static ResumeActivityItem obtain(int procState, boolean isForward) {
+ public static ResumeActivityItem obtain(int procState, boolean isForward,
+ boolean shouldSendCompatFakeFocus) {
ResumeActivityItem instance = ObjectPool.obtain(ResumeActivityItem.class);
if (instance == null) {
instance = new ResumeActivityItem();
@@ -82,12 +86,13 @@
instance.mProcState = procState;
instance.mUpdateProcState = true;
instance.mIsForward = isForward;
+ instance.mShouldSendCompatFakeFocus = shouldSendCompatFakeFocus;
return instance;
}
/** Obtain an instance initialized with provided params. */
- public static ResumeActivityItem obtain(boolean isForward) {
+ public static ResumeActivityItem obtain(boolean isForward, boolean shouldSendCompatFakeFocus) {
ResumeActivityItem instance = ObjectPool.obtain(ResumeActivityItem.class);
if (instance == null) {
instance = new ResumeActivityItem();
@@ -95,6 +100,7 @@
instance.mProcState = ActivityManager.PROCESS_STATE_UNKNOWN;
instance.mUpdateProcState = false;
instance.mIsForward = isForward;
+ instance.mShouldSendCompatFakeFocus = shouldSendCompatFakeFocus;
return instance;
}
@@ -105,6 +111,7 @@
mProcState = ActivityManager.PROCESS_STATE_UNKNOWN;
mUpdateProcState = false;
mIsForward = false;
+ mShouldSendCompatFakeFocus = false;
ObjectPool.recycle(this);
}
@@ -117,6 +124,7 @@
dest.writeInt(mProcState);
dest.writeBoolean(mUpdateProcState);
dest.writeBoolean(mIsForward);
+ dest.writeBoolean(mShouldSendCompatFakeFocus);
}
/** Read from Parcel. */
@@ -124,6 +132,7 @@
mProcState = in.readInt();
mUpdateProcState = in.readBoolean();
mIsForward = in.readBoolean();
+ mShouldSendCompatFakeFocus = in.readBoolean();
}
public static final @NonNull Creator<ResumeActivityItem> CREATOR =
@@ -147,7 +156,8 @@
}
final ResumeActivityItem other = (ResumeActivityItem) o;
return mProcState == other.mProcState && mUpdateProcState == other.mUpdateProcState
- && mIsForward == other.mIsForward;
+ && mIsForward == other.mIsForward
+ && mShouldSendCompatFakeFocus == other.mShouldSendCompatFakeFocus;
}
@Override
@@ -156,12 +166,14 @@
result = 31 * result + mProcState;
result = 31 * result + (mUpdateProcState ? 1 : 0);
result = 31 * result + (mIsForward ? 1 : 0);
+ result = 31 * result + (mShouldSendCompatFakeFocus ? 1 : 0);
return result;
}
@Override
public String toString() {
return "ResumeActivityItem{procState=" + mProcState
- + ",updateProcState=" + mUpdateProcState + ",isForward=" + mIsForward + "}";
+ + ",updateProcState=" + mUpdateProcState + ",isForward=" + mIsForward
+ + ",shouldSendCompatFakeFocus=" + mShouldSendCompatFakeFocus + "}";
}
}
diff --git a/core/java/android/app/servertransaction/TransactionExecutor.java b/core/java/android/app/servertransaction/TransactionExecutor.java
index de1d38a..c8f7d10 100644
--- a/core/java/android/app/servertransaction/TransactionExecutor.java
+++ b/core/java/android/app/servertransaction/TransactionExecutor.java
@@ -126,10 +126,13 @@
final ClientTransactionItem item = callbacks.get(i);
if (DEBUG_RESOLVER) Slog.d(TAG, tId(transaction) + "Resolving callback: " + item);
final int postExecutionState = item.getPostExecutionState();
- final int closestPreExecutionState = mHelper.getClosestPreExecutionState(r,
- item.getPostExecutionState());
- if (closestPreExecutionState != UNDEFINED) {
- cycleToPath(r, closestPreExecutionState, transaction);
+
+ if (item.shouldHaveDefinedPreExecutionState()) {
+ final int closestPreExecutionState = mHelper.getClosestPreExecutionState(r,
+ item.getPostExecutionState());
+ if (closestPreExecutionState != UNDEFINED) {
+ cycleToPath(r, closestPreExecutionState, transaction);
+ }
}
item.execute(mTransactionHandler, token, mPendingActions);
@@ -223,7 +226,8 @@
break;
case ON_RESUME:
mTransactionHandler.handleResumeActivity(r, false /* finalStateRequest */,
- r.isForward, "LIFECYCLER_RESUME_ACTIVITY");
+ r.isForward, false /* shouldSendCompatFakeFocus */,
+ "LIFECYCLER_RESUME_ACTIVITY");
break;
case ON_PAUSE:
mTransactionHandler.handlePauseActivity(r, false /* finished */,
diff --git a/core/java/android/app/servertransaction/TransactionExecutorHelper.java b/core/java/android/app/servertransaction/TransactionExecutorHelper.java
index cb6aa09..5311b09 100644
--- a/core/java/android/app/servertransaction/TransactionExecutorHelper.java
+++ b/core/java/android/app/servertransaction/TransactionExecutorHelper.java
@@ -202,7 +202,8 @@
lifecycleItem = StopActivityItem.obtain(0 /* configChanges */);
break;
default:
- lifecycleItem = ResumeActivityItem.obtain(false /* isForward */);
+ lifecycleItem = ResumeActivityItem.obtain(false /* isForward */,
+ false /* shouldSendCompatFakeFocus */);
break;
}
diff --git a/core/java/android/companion/virtual/IVirtualDevice.aidl b/core/java/android/companion/virtual/IVirtualDevice.aidl
index 22ea9f20..9ab7cf9 100644
--- a/core/java/android/companion/virtual/IVirtualDevice.aidl
+++ b/core/java/android/companion/virtual/IVirtualDevice.aidl
@@ -60,62 +60,73 @@
/**
* Closes the virtual device and frees all associated resources.
*/
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void close();
/**
* Notifies of an audio session being started.
*/
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void onAudioSessionStarting(
int displayId,
IAudioRoutingCallback routingCallback,
IAudioConfigChangedCallback configChangedCallback);
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void onAudioSessionEnded();
- @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)")
+
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void createVirtualDpad(
in VirtualDpadConfig config,
IBinder token);
- @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)")
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void createVirtualKeyboard(
in VirtualKeyboardConfig config,
IBinder token);
- @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)")
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void createVirtualMouse(
in VirtualMouseConfig config,
IBinder token);
- @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)")
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void createVirtualTouchscreen(
in VirtualTouchscreenConfig config,
IBinder token);
- @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)")
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void createVirtualNavigationTouchpad(
in VirtualNavigationTouchpadConfig config,
IBinder token);
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void unregisterInputDevice(IBinder token);
int getInputDeviceId(IBinder token);
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
boolean sendDpadKeyEvent(IBinder token, in VirtualKeyEvent event);
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
boolean sendKeyEvent(IBinder token, in VirtualKeyEvent event);
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
boolean sendButtonEvent(IBinder token, in VirtualMouseButtonEvent event);
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
boolean sendRelativeEvent(IBinder token, in VirtualMouseRelativeEvent event);
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
boolean sendScrollEvent(IBinder token, in VirtualMouseScrollEvent event);
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
boolean sendTouchEvent(IBinder token, in VirtualTouchEvent event);
/**
* Creates a virtual sensor, capable of injecting sensor events into the system.
*/
- @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)")
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void createVirtualSensor(IBinder tokenm, in VirtualSensorConfig config);
/**
* Removes the sensor corresponding to the given token from the system.
*/
- @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)")
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void unregisterSensor(IBinder token);
/**
* Sends an event to the virtual sensor corresponding to the given token.
*/
- @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)")
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
boolean sendSensorEvent(IBinder token, in VirtualSensorEvent event);
/**
@@ -126,6 +137,7 @@
PointF getCursorPosition(IBinder token);
/** Sets whether to show or hide the cursor while this virtual device is active. */
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void setShowPointerIcon(boolean showPointerIcon);
/**
@@ -133,9 +145,9 @@
* when matching the provided IntentFilter and calls the callback with the intercepted
* intent.
*/
- @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)")
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void registerIntentInterceptor(
in IVirtualDeviceIntentInterceptor intentInterceptor, in IntentFilter filter);
- @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)")
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void unregisterIntentInterceptor(in IVirtualDeviceIntentInterceptor intentInterceptor);
}
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index 088ac06..adf59fb 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -57,6 +57,7 @@
import android.hardware.input.VirtualNavigationTouchpadConfig;
import android.hardware.input.VirtualTouchscreen;
import android.hardware.input.VirtualTouchscreenConfig;
+import android.media.AudioManager;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
@@ -303,6 +304,22 @@
}
/**
+ * Requests sound effect to be played on virtual device.
+ *
+ * @see android.media.AudioManager#playSoundEffect(int)
+ *
+ * @param deviceId - id of the virtual audio device
+ * @param effectType the type of sound effect
+ * @hide
+ */
+ public void playSoundEffect(int deviceId, @AudioManager.SystemSoundEffect int effectType) {
+ //TODO - handle requests to play sound effects by custom callbacks or SoundPool asociated
+ // with device session id.
+ // For now, this is intentionally left empty and effectively disables sound effects for
+ // virtual devices with custom device audio policy.
+ }
+
+ /**
* A virtual device has its own virtual display, audio output, microphone, and camera etc. The
* creator of a virtual device can take the output from the virtual display and stream it over
* to another device, and inject input events that are received from the remote device.
@@ -758,13 +775,11 @@
final Point size = new Point();
display.getDisplay().getSize(size);
VirtualTouchscreenConfig touchscreenConfig =
- new VirtualTouchscreenConfig.Builder()
+ new VirtualTouchscreenConfig.Builder(size.x, size.y)
.setVendorId(vendorId)
.setProductId(productId)
.setInputDeviceName(inputDeviceName)
.setAssociatedDisplayId(display.getDisplay().getDisplayId())
- .setWidthInPixels(size.x)
- .setHeightInPixels(size.y)
.build();
return createVirtualTouchscreen(touchscreenConfig);
}
diff --git a/core/java/android/companion/virtual/VirtualDeviceParams.java b/core/java/android/companion/virtual/VirtualDeviceParams.java
index 597b0f5..d4a0a08 100644
--- a/core/java/android/companion/virtual/VirtualDeviceParams.java
+++ b/core/java/android/companion/virtual/VirtualDeviceParams.java
@@ -758,7 +758,7 @@
*/
@NonNull
public Builder setAudioPlaybackSessionId(int playbackSessionId) {
- if (playbackSessionId != AUDIO_SESSION_ID_GENERATE || playbackSessionId < 0) {
+ if (playbackSessionId < 0) {
throw new IllegalArgumentException("Invalid playback audio session id");
}
mAudioPlaybackSessionId = playbackSessionId;
@@ -782,7 +782,7 @@
*/
@NonNull
public Builder setAudioRecordingSessionId(int recordingSessionId) {
- if (recordingSessionId != AUDIO_SESSION_ID_GENERATE || recordingSessionId < 0) {
+ if (recordingSessionId < 0) {
throw new IllegalArgumentException("Invalid recording audio session id");
}
mAudioRecordingSessionId = recordingSessionId;
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 708a02d..7d7232e 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -40,6 +40,7 @@
import android.app.ActivityManager;
import android.app.BroadcastOptions;
import android.app.GameManager;
+import android.app.GrammaticalInflectionManager;
import android.app.IApplicationThread;
import android.app.IServiceConnection;
import android.app.VrManager;
@@ -3973,6 +3974,8 @@
CREDENTIAL_SERVICE,
DEVICE_LOCK_SERVICE,
VIRTUALIZATION_SERVICE,
+ GRAMMATICAL_INFLECTION_SERVICE,
+
})
@Retention(RetentionPolicy.SOURCE)
public @interface ServiceName {}
@@ -5784,7 +5787,6 @@
*
* @see #getSystemService(String)
* @see android.content.om.OverlayManager
- * @hide
*/
public static final String OVERLAY_SERVICE = "overlay";
@@ -6170,6 +6172,14 @@
public static final String VIRTUALIZATION_SERVICE = "virtualization";
/**
+ * Use with {@link #getSystemService(String)} to retrieve a
+ * {@link GrammaticalInflectionManager}.
+ *
+ * @see #getSystemService(String)
+ */
+ public static final String GRAMMATICAL_INFLECTION_SERVICE = "grammatical_inflection";
+
+ /**
* Determine whether the given permission is allowed for a particular
* process and user ID running in the system.
*
@@ -7281,7 +7291,9 @@
* Updates the device ID association of this Context. Since a Context created with
* {@link #createDeviceContext} cannot change its device association, this method must
* not be called for instances created with {@link #createDeviceContext}.
- *
+ *<p>
+ * Note that updating the deviceId of the Context will not update its associated display.
+ *</p>
* @param deviceId The new device ID to assign to this Context.
* @throws UnsupportedOperationException if the method is called on an instance that was
* created with {@link Context#createDeviceContext(int)}
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 8aa0454..7ee8f60 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -66,6 +66,7 @@
import android.provider.DocumentsProvider;
import android.provider.MediaStore;
import android.provider.OpenableColumns;
+import android.service.chooser.ChooserAction;
import android.telecom.PhoneAccount;
import android.telecom.TelecomManager;
import android.text.TextUtils;
@@ -1814,8 +1815,8 @@
* Package manager install result code. @hide because result codes are not
* yet ready to be exposed.
*/
- public static final String EXTRA_INSTALL_RESULT
- = "android.intent.extra.INSTALL_RESULT";
+ @SystemApi
+ public static final String EXTRA_INSTALL_RESULT = "android.intent.extra.INSTALL_RESULT";
/**
* Activity Action: Launch application uninstaller.
@@ -1841,6 +1842,7 @@
* Specify whether the package should be uninstalled for all users.
* @hide because these should not be part of normal application flow.
*/
+ @SystemApi
public static final String EXTRA_UNINSTALL_ALL_USERS
= "android.intent.extra.UNINSTALL_ALL_USERS";
@@ -5826,6 +5828,25 @@
= "android.intent.extra.CHOOSER_REFINEMENT_INTENT_SENDER";
/**
+ * A Parcelable[] of {@link ChooserAction} objects to provide the Android Sharesheet with
+ * app-specific actions to be presented to the user when invoking {@link #ACTION_CHOOSER}.
+ * @hide
+ */
+ public static final String EXTRA_CHOOSER_CUSTOM_ACTIONS =
+ "android.intent.extra.EXTRA_CHOOSER_CUSTOM_ACTIONS";
+
+ /**
+ * Optional argument to be used with {@link #ACTION_CHOOSER}.
+ * A {@link android.app.PendingIntent} to be sent when the user wants to do payload reselection
+ * in the sharesheet.
+ * A reselection action allows the user to return to the source app to change the content being
+ * shared.
+ * @hide
+ */
+ public static final String EXTRA_CHOOSER_PAYLOAD_RESELECTION_ACTION =
+ "android.intent.extra.EXTRA_CHOOSER_PAYLOAD_RESELECTION_ACTION";
+
+ /**
* An {@code ArrayList} of {@code String} annotations describing content for
* {@link #ACTION_CHOOSER}.
*
diff --git a/core/java/android/content/om/FabricatedOverlay.java b/core/java/android/content/om/FabricatedOverlay.java
index e4936bc..7e787c9 100644
--- a/core/java/android/content/om/FabricatedOverlay.java
+++ b/core/java/android/content/om/FabricatedOverlay.java
@@ -78,16 +78,14 @@
*
* @see OverlayManager
* @see OverlayManagerTransaction
- * @hide
*/
public class FabricatedOverlay {
/**
* Retrieves the identifier for this fabricated overlay.
* @return the overlay identifier
- *
- * @hide
*/
+ @NonNull
public OverlayIdentifier getIdentifier() {
return new OverlayIdentifier(
mOverlay.packageName, TextUtils.nullIfEmpty(mOverlay.overlayName));
@@ -325,7 +323,6 @@
* @param overlayName a name used to uniquely identify the fabricated overlay owned by the
* caller itself.
* @param targetPackage the name of the package to be overlaid
- * @hide
*/
public FabricatedOverlay(@NonNull String overlayName, @NonNull String targetPackage) {
this(generateFabricatedOverlayInternal(
@@ -344,7 +341,6 @@
* should specify which overlayable to be overlaid.
*
* @param targetOverlayable the overlayable name defined in target package.
- * @hide
*/
public void setTargetOverlayable(@Nullable String targetOverlayable) {
mOverlay.targetOverlayable = TextUtils.emptyIfNull(targetOverlayable);
@@ -438,7 +434,6 @@
* @param value the integer representing the new value
* @param configuration The string representation of the config this overlay is enabled for
* @see android.util.TypedValue#TYPE_INT_COLOR_ARGB8 android.util.TypedValue#type
- * @hide
*/
@NonNull
public void setResourceValue(
@@ -470,7 +465,6 @@
* @param value the string representing the new value
* @param configuration The string representation of the config this overlay is enabled for
* @see android.util.TypedValue#TYPE_STRING android.util.TypedValue#type
- * @hide
*/
@NonNull
public void setResourceValue(
@@ -491,7 +485,6 @@
* [package]:type/entry)
* @param value the file descriptor whose contents are the value of the frro
* @param configuration The string representation of the config this overlay is enabled for
- * @hide
*/
@NonNull
public void setResourceValue(
diff --git a/core/java/android/content/om/OverlayIdentifier.java b/core/java/android/content/om/OverlayIdentifier.java
index 454d0d1..f256372 100644
--- a/core/java/android/content/om/OverlayIdentifier.java
+++ b/core/java/android/content/om/OverlayIdentifier.java
@@ -27,32 +27,43 @@
/**
* A key used to uniquely identify a Runtime Resource Overlay (RRO).
+ * <!-- For applications -->
*
- * An overlay always belongs to a package and may optionally have a name associated with it.
- * The name helps uniquely identify a particular overlay within a package.
- * @hide
+ * <p>An overlay always belongs to a package and have a mandatory name associated with it. The name
+ * helps uniquely identify a particular overlay within a package.
+ *
+ * <!-- For OverlayManagerService, it isn't public part and hidden by HTML comment. -->
+ * <!--
+ * <p>An overlay always belongs to a package and may optionally have a name associated with it. The
+ * name helps uniquely identify a particular overlay within a package.
+ * -->
+ *
+ * @see OverlayInfo#getOverlayIdentifier()
+ * @see OverlayManagerTransaction.Builder#unregisterFabricatedOverlay(OverlayIdentifier)
*/
-/** @hide */
@DataClass(genConstructor = false, genBuilder = false, genHiddenBuilder = false,
genEqualsHashCode = true, genToString = false)
-public class OverlayIdentifier implements Parcelable {
+public final class OverlayIdentifier implements Parcelable {
/**
* The package name containing or owning the overlay.
+ *
+ * @hide
*/
- @Nullable
- private final String mPackageName;
+ @Nullable private final String mPackageName;
/**
* The unique name within the package of the overlay.
+ *
+ * @hide
*/
- @Nullable
- private final String mOverlayName;
+ @Nullable private final String mOverlayName;
/**
* Creates an identifier from a package and unique name within the package.
*
* @param packageName the package containing or owning the overlay
* @param overlayName the unique name of the overlay within the package
+ * @hide
*/
public OverlayIdentifier(@NonNull String packageName, @Nullable String overlayName) {
mPackageName = packageName;
@@ -63,18 +74,24 @@
* Creates an identifier for an overlay without a name.
*
* @param packageName the package containing or owning the overlay
+ * @hide
*/
public OverlayIdentifier(@NonNull String packageName) {
mPackageName = packageName;
mOverlayName = null;
}
+ /**
+ * {@inheritDoc}
+ * @hide
+ */
@Override
public String toString() {
return mOverlayName == null ? mPackageName : mPackageName + ":" + mOverlayName;
}
/** @hide */
+ @NonNull
public static OverlayIdentifier fromString(@NonNull String text) {
final String[] parts = text.split(":", 2);
if (parts.length == 2) {
@@ -86,7 +103,7 @@
- // Code below generated by codegen v1.0.22.
+ // Code below generated by codegen v1.0.23.
//
// DO NOT MODIFY!
// CHECKSTYLE:OFF Generated code
@@ -101,6 +118,8 @@
/**
* Retrieves the package name containing or owning the overlay.
+ *
+ * @hide
*/
@DataClass.Generated.Member
public @Nullable String getPackageName() {
@@ -109,12 +128,18 @@
/**
* Retrieves the unique name within the package of the overlay.
+ *
+ * @hide
*/
@DataClass.Generated.Member
public @Nullable String getOverlayName() {
return mOverlayName;
}
+ /**
+ * {@inheritDoc}
+ * @hide
+ */
@Override
@DataClass.Generated.Member
public boolean equals(@Nullable Object o) {
@@ -132,6 +157,10 @@
&& Objects.equals(mOverlayName, that.mOverlayName);
}
+ /**
+ * {@inheritDoc}
+ * @hide
+ */
@Override
@DataClass.Generated.Member
public int hashCode() {
@@ -144,6 +173,9 @@
return _hash;
}
+ /**
+ * {@inheritDoc}
+ */
@Override
@DataClass.Generated.Member
public void writeToParcel(@NonNull Parcel dest, int flags) {
@@ -158,6 +190,9 @@
if (mOverlayName != null) dest.writeString(mOverlayName);
}
+ /**
+ * {@inheritDoc}
+ */
@Override
@DataClass.Generated.Member
public int describeContents() { return 0; }
@@ -165,7 +200,7 @@
/** @hide */
@SuppressWarnings({"unchecked", "RedundantCast"})
@DataClass.Generated.Member
- protected OverlayIdentifier(@NonNull Parcel in) {
+ /* package-private */ OverlayIdentifier(@NonNull Parcel in) {
// You can override field unparcelling by defining methods like:
// static FieldType unparcelFieldName(Parcel in) { ... }
@@ -194,10 +229,10 @@
};
@DataClass.Generated(
- time = 1612482438728L,
- codegenVersion = "1.0.22",
+ time = 1670404485646L,
+ codegenVersion = "1.0.23",
sourceFile = "frameworks/base/core/java/android/content/om/OverlayIdentifier.java",
- inputSignatures = "private final @android.annotation.Nullable java.lang.String mPackageName\nprivate final @android.annotation.Nullable java.lang.String mOverlayName\npublic @java.lang.Override java.lang.String toString()\npublic static android.content.om.OverlayIdentifier fromString(java.lang.String)\nclass OverlayIdentifier extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genConstructor=false, genBuilder=false, genHiddenBuilder=false, genEqualsHashCode=true, genToString=false)")
+ inputSignatures = "private final @android.annotation.Nullable java.lang.String mPackageName\nprivate final @android.annotation.Nullable java.lang.String mOverlayName\npublic @java.lang.Override java.lang.String toString()\npublic static @android.annotation.NonNull android.content.om.OverlayIdentifier fromString(java.lang.String)\nclass OverlayIdentifier extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genConstructor=false, genBuilder=false, genHiddenBuilder=false, genEqualsHashCode=true, genToString=false)")
@Deprecated
private void __metadata() {}
diff --git a/core/java/android/content/om/OverlayInfo.java b/core/java/android/content/om/OverlayInfo.java
index a470de2..ff1c088 100644
--- a/core/java/android/content/om/OverlayInfo.java
+++ b/core/java/android/content/om/OverlayInfo.java
@@ -33,12 +33,20 @@
import java.util.Objects;
/**
+ * An immutable information about an overlay.
+ *
+ * <p>Applications calling {@link OverlayManager#getOverlayInfosForTarget(String)} get the
+ * information list of the registered overlays. Each element in the list presents the information of
+ * the particular overlay.
+ *
+ * <!-- For OverlayManagerService, it isn't public part and hidden by HTML comment. -->
+ * <!--
* Immutable overlay information about a package. All PackageInfos that
* represent an overlay package will have a corresponding OverlayInfo.
+ * -->
*
- * @hide
+ * @see OverlayManager#getOverlayInfosForTarget(String)
*/
-@SystemApi
public final class OverlayInfo implements CriticalOverlayInfo, Parcelable {
/** @hide */
@@ -49,7 +57,6 @@
STATE_DISABLED,
STATE_ENABLED,
STATE_ENABLED_IMMUTABLE,
- // @Deprecated STATE_TARGET_IS_BEING_REPLACED,
STATE_OVERLAY_IS_BEING_REPLACED,
STATE_SYSTEM_UPDATE_UNINSTALL,
})
@@ -174,14 +181,14 @@
*
* @hide
*/
- public final String targetOverlayableName;
+ @Nullable public final String targetOverlayableName;
/**
* Category of the overlay package
*
* @hide
*/
- public final String category;
+ @Nullable public final String category;
/**
* Full path to the base APK for this overlay package
@@ -272,7 +279,7 @@
}
/** @hide */
- public OverlayInfo(Parcel source) {
+ public OverlayInfo(@NonNull Parcel source) {
packageName = source.readString();
overlayName = source.readString();
targetPackageName = source.readString();
@@ -299,8 +306,9 @@
}
/**
- * {@inheritDoc}
- * @hide
+ * Get the overlay name from the registered fabricated overlay.
+ *
+ * @return the overlay name
*/
@Override
@Nullable
@@ -309,11 +317,11 @@
}
/**
- * {@inheritDoc}
- * @hide
+ * Returns the name of the target overlaid package.
+ *
+ * @return the target package name
*/
@Override
- @SystemApi
@NonNull
public String getTargetPackageName() {
return targetPackageName;
@@ -342,11 +350,11 @@
}
/**
- * {@inheritDoc}
- * @hide
+ * Return the target overlayable name.
+ *
+ * @return the name of the target overlayable resources set
*/
@Override
- @SystemApi
@Nullable
public String getTargetOverlayableName() {
return targetOverlayableName;
@@ -366,13 +374,18 @@
*
* @hide
*/
+ @NonNull
public String getBaseCodePath() {
return baseCodePath;
}
/**
- * {@inheritDoc}
- * @hide
+ * Get the unique identifier from the overlay information.
+ *
+ * <p>The return value of this function can be used to unregister the related overlay.
+ *
+ * @return an identifier representing the current overlay.
+ * @see OverlayManagerTransaction.Builder#unregisterFabricatedOverlay(OverlayIdentifier)
*/
@Override
@NonNull
@@ -415,7 +428,7 @@
}
@Override
- public void writeToParcel(Parcel dest, int flags) {
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeString(packageName);
dest.writeString(overlayName);
dest.writeString(targetPackageName);
@@ -429,7 +442,7 @@
dest.writeBoolean(isFabricated);
}
- public static final @android.annotation.NonNull Parcelable.Creator<OverlayInfo> CREATOR =
+ public static final @NonNull Parcelable.Creator<OverlayInfo> CREATOR =
new Parcelable.Creator<OverlayInfo>() {
@Override
public OverlayInfo createFromParcel(Parcel source) {
@@ -492,6 +505,11 @@
}
}
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
@Override
public int hashCode() {
final int prime = 31;
@@ -508,6 +526,11 @@
return result;
}
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
@@ -547,6 +570,11 @@
return true;
}
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
@NonNull
@Override
public String toString() {
diff --git a/core/java/android/content/om/OverlayManager.java b/core/java/android/content/om/OverlayManager.java
index 7803cb8..96b7603 100644
--- a/core/java/android/content/om/OverlayManager.java
+++ b/core/java/android/content/om/OverlayManager.java
@@ -54,9 +54,7 @@
* </ul>
*
* @see OverlayManagerTransaction
- * @hide
*/
-@SystemApi
@SystemService(Context.OVERLAY_SERVICE)
public class OverlayManager {
@@ -392,7 +390,6 @@
*
* @param targetPackageName the target package name
* @return a list of overlay information
- * @hide
*/
@NonNull
@NonUiContext
diff --git a/core/java/android/content/om/OverlayManagerTransaction.java b/core/java/android/content/om/OverlayManagerTransaction.java
index c7c605d..5fd695b 100644
--- a/core/java/android/content/om/OverlayManagerTransaction.java
+++ b/core/java/android/content/om/OverlayManagerTransaction.java
@@ -58,7 +58,6 @@
*
* @see OverlayManager
* @see FabricatedOverlay
- * @hide
*/
public final class OverlayManagerTransaction implements Parcelable {
// TODO: remove @hide from this class when OverlayManager is added to the
@@ -92,8 +91,6 @@
/**
* Get an overlay manager transaction with the specified handler.
* @param overlayManager handles this transaction.
- *
- * @hide
*/
public OverlayManagerTransaction(@NonNull OverlayManager overlayManager) {
this(new ArrayList<>(), Objects.requireNonNull(overlayManager));
@@ -291,8 +288,6 @@
/**
* {@inheritDoc}
- *
- * @hide
*/
@Override
public int describeContents() {
@@ -301,8 +296,6 @@
/**
* {@inheritDoc}
- *
- * @hide
*/
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
@@ -340,7 +333,6 @@
*
* @throws IOException if there is a file operation error.
* @throws PackageManager.NameNotFoundException if the package name is not found.
- * @hide
*/
@NonUiContext
public void commit() throws PackageManager.NameNotFoundException, IOException {
@@ -374,8 +366,6 @@
* package or target overlayable is changed.
*
* @param overlay the overlay to register with the overlay manager
- *
- * @hide
*/
@NonNull
public void registerFabricatedOverlay(@NonNull FabricatedOverlay overlay) {
@@ -389,7 +379,6 @@
*
* @see OverlayManager#getOverlayInfosForTarget(String)
* @see OverlayInfo#getOverlayIdentifier()
- * @hide
*/
@NonNull
public void unregisterFabricatedOverlay(@NonNull OverlayIdentifier overlay) {
diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java
index dab57fd..14d88de 100644
--- a/core/java/android/content/pm/ActivityInfo.java
+++ b/core/java/android/content/pm/ActivityInfo.java
@@ -39,6 +39,7 @@
import android.os.UserHandle;
import android.util.ArraySet;
import android.util.Printer;
+import android.window.OnBackInvokedCallback;
import com.android.internal.util.Parcelling;
@@ -624,7 +625,7 @@
* See {@link android.R.attr#inheritShowWhenLocked}.
* @hide
*/
- public static final int FLAG_INHERIT_SHOW_WHEN_LOCKED = 0x1;
+ public static final int FLAG_INHERIT_SHOW_WHEN_LOCKED = 1 << 0;
/**
* Bit in {@link #privateFlags} indicating whether a home sound effect should be played if the
@@ -632,13 +633,34 @@
* Set from the {@link android.R.attr#playHomeTransitionSound} attribute.
* @hide
*/
- public static final int PRIVATE_FLAG_HOME_TRANSITION_SOUND = 0x2;
+ public static final int PRIVATE_FLAG_HOME_TRANSITION_SOUND = 1 << 1;
+
+ /**
+ * Bit in {@link #privateFlags} indicating {@link android.view.KeyEvent#KEYCODE_BACK} related
+ * events will be replaced by a call to {@link OnBackInvokedCallback#onBackInvoked()} on the
+ * focused window.
+ * @hide
+ * @see android.R.styleable.AndroidManifestActivity_enableOnBackInvokedCallback
+ */
+ public static final int PRIVATE_FLAG_ENABLE_ON_BACK_INVOKED_CALLBACK = 1 << 2;
+
+ /**
+ * Bit in {@link #privateFlags} indicating {@link android.view.KeyEvent#KEYCODE_BACK} related
+ * events will be forwarded to the Activity and its dialogs and views and
+ * the {@link android.app.Activity#onBackPressed()}, {@link android.app.Dialog#onBackPressed}
+ * will be called.
+ * @hide
+ * @see android.R.styleable.AndroidManifestActivity_enableOnBackInvokedCallback
+ */
+ public static final int PRIVATE_FLAG_DISABLE_ON_BACK_INVOKED_CALLBACK = 1 << 3;
/**
* Options that have been set in the activity declaration in the manifest.
* These include:
* {@link #FLAG_INHERIT_SHOW_WHEN_LOCKED},
* {@link #PRIVATE_FLAG_HOME_TRANSITION_SOUND}.
+ * {@link #PRIVATE_FLAG_ENABLE_ON_BACK_INVOKED_CALLBACK}
+ * {@link #PRIVATE_FLAG_DISABLE_ON_BACK_INVOKED_CALLBACK}
* @hide
*/
public int privateFlags;
@@ -807,6 +829,7 @@
CONFIG_LAYOUT_DIRECTION,
CONFIG_COLOR_MODE,
CONFIG_FONT_SCALE,
+ CONFIG_GRAMMATICAL_GENDER,
})
@Retention(RetentionPolicy.SOURCE)
public @interface Config {}
@@ -917,6 +940,12 @@
public static final int CONFIG_COLOR_MODE = 0x4000;
/**
* Bit in {@link #configChanges} that indicates that the activity
+ * can itself handle the change to gender. Set from the
+ * {@link android.R.attr#configChanges} attribute.
+ */
+ public static final int CONFIG_GRAMMATICAL_GENDER = 0x8000;
+ /**
+ * Bit in {@link #configChanges} that indicates that the activity
* can itself handle asset path changes. Set from the {@link android.R.attr#configChanges}
* attribute. This is not a core resource configuration, but a higher-level value, so its
* constant starts at the high bits.
@@ -946,7 +975,6 @@
* not a core resource configuration, but a higher-level value, so its
* constant starts at the high bits.
*/
-
public static final int CONFIG_FONT_WEIGHT_ADJUSTMENT = 0x10000000;
/** @hide
@@ -1612,6 +1640,32 @@
return isChangeEnabled(CHECK_MIN_WIDTH_HEIGHT_FOR_MULTI_WINDOW);
}
+ /**
+ * Returns whether the activity will set the
+ * {@link R.styleable.AndroidManifestActivity_enableOnBackInvokedCallback} attribute.
+ *
+ * @hide
+ */
+ public boolean hasOnBackInvokedCallbackEnabled() {
+ return (privateFlags & (PRIVATE_FLAG_ENABLE_ON_BACK_INVOKED_CALLBACK
+ | PRIVATE_FLAG_DISABLE_ON_BACK_INVOKED_CALLBACK)) != 0;
+ }
+
+ /**
+ * Returns whether the activity will use the {@link android.window.OnBackInvokedCallback}
+ * navigation system instead of the {@link android.view.KeyEvent#KEYCODE_BACK} and related
+ * callbacks.
+ *
+ * Valid when the {@link R.styleable.AndroidManifestActivity_enableOnBackInvokedCallback}
+ * attribute has been set, or it won't indicate if the activity should use the
+ * navigation system and the {@link hasOnBackInvokedCallbackEnabled} will return false.
+ * @hide
+ */
+ public boolean isOnBackInvokedCallbackEnabled() {
+ return hasOnBackInvokedCallbackEnabled()
+ && (privateFlags & PRIVATE_FLAG_ENABLE_ON_BACK_INVOKED_CALLBACK) != 0;
+ }
+
public void dump(Printer pw, String prefix) {
dump(pw, prefix, DUMP_FLAG_ALL);
}
diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java
index 84811ea..94c5e25 100644
--- a/core/java/android/content/pm/ApplicationInfo.java
+++ b/core/java/android/content/pm/ApplicationInfo.java
@@ -18,9 +18,11 @@
import static android.os.Build.VERSION_CODES.DONUT;
+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.annotation.TestApi;
import android.compat.annotation.UnsupportedAppUsage;
@@ -2253,6 +2255,8 @@
*
* @hide
*/
+ @SystemApi
+ @RequiresPermission(Manifest.permission.DELETE_PACKAGES)
public boolean hasFragileUserData() {
return (privateFlags & PRIVATE_FLAG_HAS_FRAGILE_USER_DATA) != 0;
}
@@ -2487,8 +2491,13 @@
return (privateFlags & ApplicationInfo.PRIVATE_FLAG_SIGNED_WITH_PLATFORM_KEY) != 0;
}
- /** @hide */
+ /**
+ * @return {@code true} if the application is permitted to hold privileged permissions.
+ *
+ * @hide */
@TestApi
+ @SystemApi
+ @RequiresPermission(Manifest.permission.INSTALL_PACKAGES)
public boolean isPrivilegedApp() {
return (privateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0;
}
diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java
index febdaed..703a9252 100644
--- a/core/java/android/content/pm/PackageInstaller.java
+++ b/core/java/android/content/pm/PackageInstaller.java
@@ -26,6 +26,9 @@
import static android.content.pm.Checksum.TYPE_WHOLE_SHA1;
import static android.content.pm.Checksum.TYPE_WHOLE_SHA256;
import static android.content.pm.Checksum.TYPE_WHOLE_SHA512;
+import static android.content.pm.PackageInfo.INSTALL_LOCATION_AUTO;
+import static android.content.pm.PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY;
+import static android.content.pm.PackageInfo.INSTALL_LOCATION_PREFER_EXTERNAL;
import android.Manifest;
import android.annotation.CallbackExecutor;
@@ -48,6 +51,10 @@
import android.content.pm.PackageManager.DeleteFlags;
import android.content.pm.PackageManager.InstallReason;
import android.content.pm.PackageManager.InstallScenario;
+import android.content.pm.parsing.ApkLiteParseUtils;
+import android.content.pm.parsing.PackageLite;
+import android.content.pm.parsing.result.ParseResult;
+import android.content.pm.parsing.result.ParseTypeImpl;
import android.graphics.Bitmap;
import android.icu.util.ULocale;
import android.net.Uri;
@@ -70,12 +77,14 @@
import android.util.ArraySet;
import android.util.ExceptionUtils;
+import com.android.internal.content.InstallLocationUtils;
import com.android.internal.util.DataClass;
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.Preconditions;
import com.android.internal.util.function.pooled.PooledLambda;
import java.io.Closeable;
+import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
@@ -172,10 +181,22 @@
public static final String ACTION_SESSION_UPDATED =
"android.content.pm.action.SESSION_UPDATED";
- /** {@hide} */
+ /**
+ * Intent action to indicate that user action is required for current install. This action can
+ * be used only by system apps.
+ *
+ * @hide
+ */
+ @SystemApi
public static final String ACTION_CONFIRM_INSTALL = "android.content.pm.action.CONFIRM_INSTALL";
- /** @hide */
+ /**
+ * Activity Action: Intent sent to the installer when a session for requesting
+ * user pre-approval, and user needs to confirm the installation.
+ *
+ * @hide
+ */
+ @SystemApi
public static final String ACTION_CONFIRM_PRE_APPROVAL =
"android.content.pm.action.CONFIRM_PRE_APPROVAL";
@@ -272,11 +293,23 @@
@Deprecated
public static final String EXTRA_PACKAGE_NAMES = "android.content.pm.extra.PACKAGE_NAMES";
- /** {@hide} */
+ /**
+ * The status as used internally in the package manager. Refer to {@link PackageManager} for
+ * a list of all valid legacy statuses.
+ *
+ * @hide
+ */
+ @SystemApi
public static final String EXTRA_LEGACY_STATUS = "android.content.pm.extra.LEGACY_STATUS";
/** {@hide} */
public static final String EXTRA_LEGACY_BUNDLE = "android.content.pm.extra.LEGACY_BUNDLE";
- /** {@hide} */
+ /**
+ * The callback to execute once an uninstall is completed (used for both successful and
+ * unsuccessful uninstalls).
+ *
+ * @hide
+ */
+ @SystemApi
public static final String EXTRA_CALLBACK = "android.content.pm.extra.CALLBACK";
/**
@@ -293,6 +326,17 @@
public static final String EXTRA_DATA_LOADER_TYPE = "android.content.pm.extra.DATA_LOADER_TYPE";
/**
+ * Path to the validated base APK for this session, which may point at an
+ * APK inside the session (when the session defines the base), or it may
+ * point at the existing base APK (when adding splits to an existing app).
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final String EXTRA_RESOLVED_BASE_PATH =
+ "android.content.pm.extra.RESOLVED_BASE_PATH";
+
+ /**
* Streaming installation pending.
* Caller should make sure DataLoader is able to prepare image and reinitiate the operation.
*
@@ -796,8 +840,6 @@
* @param statusReceiver Where to deliver the result of the operation indicated by the extra
* {@link #EXTRA_STATUS}. Refer to the individual status codes
* on how to handle them.
- *
- * @hide
*/
@RequiresPermission(anyOf = {
Manifest.permission.DELETE_PACKAGES,
@@ -1871,6 +1913,101 @@
}
/**
+ * Parse a single APK or a directory of APKs to get install relevant information about
+ * the package wrapped in {@link InstallInfo}.
+ * @throws PackageParsingException if the package source file(s) provided is(are) not valid,
+ * or the parser isn't able to parse the supplied source(s).
+ * @hide
+ */
+ @SystemApi
+ @NonNull
+ public InstallInfo getInstallInfo(@NonNull File file, int flags)
+ throws PackageParsingException {
+ final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing();
+ final ParseResult<PackageLite> result = ApkLiteParseUtils.parsePackageLite(
+ input.reset(), file, flags);
+ if (result.isError()) {
+ throw new PackageParsingException(result.getErrorCode(), result.getErrorMessage());
+ }
+ return new InstallInfo(result);
+ }
+
+ // (b/239722738) This class serves as a bridge between the PackageLite class, which
+ // is a hidden class, and the consumers of this class. (e.g. InstallInstalling.java)
+ // This is a part of an effort to remove dependency on hidden APIs and use SystemAPIs or
+ // public APIs.
+ /**
+ * Install related details from an APK or a folder of APK(s).
+ *
+ * @hide
+ */
+ @SystemApi
+ public static class InstallInfo {
+
+ /** @hide */
+ @IntDef(prefix = { "INSTALL_LOCATION_" }, value = {
+ INSTALL_LOCATION_AUTO,
+ INSTALL_LOCATION_INTERNAL_ONLY,
+ INSTALL_LOCATION_PREFER_EXTERNAL
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface InstallLocation{}
+
+ private PackageLite mPkg;
+
+ InstallInfo(ParseResult<PackageLite> result) {
+ mPkg = result.getResult();
+ }
+
+ /**
+ * See {@link PackageLite#getPackageName()}
+ */
+ @NonNull
+ public String getPackageName() {
+ return mPkg.getPackageName();
+ }
+
+ /**
+ * @return The default install location defined by an application in
+ * {@link android.R.attr#installLocation} attribute.
+ */
+ public @InstallLocation int getInstallLocation() {
+ return mPkg.getInstallLocation();
+ }
+
+ /**
+ * @param params {@link SessionParams} of the installation
+ * @return Total disk space occupied by an application after installation.
+ * Includes the size of the raw APKs, possibly unpacked resources, raw dex metadata files,
+ * and all relevant native code.
+ * @throws IOException when size of native binaries cannot be calculated.
+ */
+ public long calculateInstalledSize(@NonNull SessionParams params) throws IOException {
+ return InstallLocationUtils.calculateInstalledSize(mPkg, params.abiOverride);
+ }
+ }
+
+ /**
+ * Generic exception class for using with parsing operations.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static class PackageParsingException extends Exception {
+ private final int mErrorCode;
+
+ /** {@hide} */
+ public PackageParsingException(int errorCode, @Nullable String detailedMessage) {
+ super(detailedMessage);
+ mErrorCode = errorCode;
+ }
+
+ public int getErrorCode() {
+ return mErrorCode;
+ }
+ }
+
+ /**
* Parameters for creating a new {@link PackageInstaller.Session}.
*/
public static class SessionParams implements Parcelable {
@@ -2431,9 +2568,7 @@
* By default this is the app that created the {@link PackageInstaller} object.
*
* @param installerPackageName name of the installer package
- * {@hide}
*/
- @TestApi
public void setInstallerPackageName(@Nullable String installerPackageName) {
this.installerPackageName = installerPackageName;
}
@@ -3430,8 +3565,6 @@
/**
* Returns the Uid of the owner of the session.
- *
- * @hide
*/
public int getInstallerUid() {
return installerUid;
@@ -3445,6 +3578,13 @@
return keepApplicationEnabledSetting;
}
+ /**
+ * Returns whether this session has requested user pre-approval.
+ */
+ public @NonNull boolean getIsPreApprovalRequested() {
+ return isPreapprovalRequested;
+ }
+
@Override
public int describeContents() {
return 0;
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index cbdcc02..4ad657e 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -2250,6 +2250,7 @@
*
* @hide
*/
+ @SystemApi
public static final int DELETE_KEEP_DATA = 0x00000001;
/**
@@ -2258,6 +2259,7 @@
*
* @hide
*/
+ @SystemApi
public static final int DELETE_ALL_USERS = 0x00000002;
/**
@@ -2295,6 +2297,7 @@
*
* @hide
*/
+ @SystemApi
public static final int DELETE_SUCCEEDED = 1;
/**
@@ -2304,6 +2307,7 @@
*
* @hide
*/
+ @SystemApi
public static final int DELETE_FAILED_INTERNAL_ERROR = -1;
/**
@@ -2313,6 +2317,7 @@
*
* @hide
*/
+ @SystemApi
public static final int DELETE_FAILED_DEVICE_POLICY_MANAGER = -2;
/**
@@ -2332,9 +2337,11 @@
*
* @hide
*/
+ @SystemApi
public static final int DELETE_FAILED_OWNER_BLOCKED = -4;
/** {@hide} */
+ @SystemApi
public static final int DELETE_FAILED_ABORTED = -5;
/**
@@ -4090,6 +4097,17 @@
public static final String FEATURE_IPSEC_TUNNELS = "android.software.ipsec_tunnels";
/**
+ * Feature for {@link #getSystemAvailableFeatures} and {@link #hasSystemFeature}: The device has
+ * the requisite kernel support for migrating IPsec tunnels to new source/destination addresses.
+ *
+ * <p>This feature implies that the device supports XFRM Migration (CONFIG_XFRM_MIGRATE) and has
+ * the kernel fixes to support cross-address-family IPsec tunnel migration
+ */
+ @SdkConstant(SdkConstantType.FEATURE)
+ public static final String FEATURE_IPSEC_TUNNEL_MIGRATION =
+ "android.software.ipsec_tunnel_migration";
+
+ /**
* Feature for {@link #getSystemAvailableFeatures} and
* {@link #hasSystemFeature}: The device supports a system interface for the user to select
* and bind device control services provided by applications.
@@ -5336,17 +5354,7 @@
throws NameNotFoundException;
/**
- * Return the UID associated with the given package name.
- * <p>
- * Note that the same package will have different UIDs under different
- * {@link UserHandle} on the same device.
- *
- * @param packageName The full name (i.e. com.google.apps.contacts) of the
- * desired package.
- * @param userId The user handle identifier to look up the package under.
- * @return Returns an integer UID who owns the given package name.
- * @throws NameNotFoundException if no such package is available to the
- * caller.
+ * See {@link #getPackageUidAsUser(String, PackageInfoFlags, int)}.
* @deprecated Use {@link #getPackageUidAsUser(String, PackageInfoFlags, int)} instead.
* @hide
*/
@@ -5357,9 +5365,22 @@
int flags, @UserIdInt int userId) throws NameNotFoundException;
/**
- * See {@link #getPackageUidAsUser(String, int, int)}.
+ * Return the UID associated with the given package name.
+ * <p>
+ * Note that the same package will have different UIDs under different
+ * {@link UserHandle} on the same device.
+ *
+ * @param packageName The full name (i.e. com.google.apps.contacts) of the
+ * desired package.
+ * @param flags Additional option flags to modify the data returned.
+ * @param userId The user handle identifier to look up the package under.
+ * @return Returns an integer UID who owns the given package name.
+ * @throws NameNotFoundException if no such package is available to the
+ * caller.
* @hide
*/
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL)
public int getPackageUidAsUser(@NonNull String packageName, @NonNull PackageInfoFlags flags,
@UserIdInt int userId) throws NameNotFoundException {
throw new UnsupportedOperationException(
@@ -9805,6 +9826,83 @@
}
/**
+ * A parcelable class to pass as an intent extra to the PackageInstaller. When an uninstall is
+ * completed (both successfully or unsuccessfully), the result is sent to the uninstall
+ * initiators.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final class UninstallCompleteCallback implements Parcelable {
+ private IPackageDeleteObserver2 mBinder;
+
+ /** @hide */
+ @IntDef(prefix = { "DELETE_" }, value = {
+ DELETE_SUCCEEDED,
+ DELETE_FAILED_INTERNAL_ERROR,
+ DELETE_FAILED_DEVICE_POLICY_MANAGER,
+ DELETE_FAILED_USER_RESTRICTED,
+ DELETE_FAILED_OWNER_BLOCKED,
+ DELETE_FAILED_ABORTED,
+ DELETE_FAILED_USED_SHARED_LIBRARY,
+ DELETE_FAILED_APP_PINNED,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface DeleteStatus{}
+
+ /** @hide */
+ public UninstallCompleteCallback(@NonNull IBinder binder) {
+ mBinder = IPackageDeleteObserver2.Stub.asInterface(binder);
+ }
+
+ /** @hide */
+ private UninstallCompleteCallback(Parcel in) {
+ mBinder = IPackageDeleteObserver2.Stub.asInterface(in.readStrongBinder());
+ }
+
+ public static final @NonNull Parcelable.Creator<UninstallCompleteCallback> CREATOR =
+ new Parcelable.Creator<>() {
+ public UninstallCompleteCallback createFromParcel(Parcel source) {
+ return new UninstallCompleteCallback(source);
+ }
+
+ public UninstallCompleteCallback[] newArray(int size) {
+ return new UninstallCompleteCallback[size];
+ }
+ };
+
+ /**
+ * Called when an uninstallation is completed successfully or unsuccessfully.
+ *
+ * @param packageName The name of the package being uninstalled.
+ * @param resultCode Result code of the operation.
+ * @param errorMessage Error message if any.
+ *
+ * @hide */
+ @SystemApi
+ public void onUninstallComplete(@NonNull String packageName, @DeleteStatus int resultCode,
+ @Nullable String errorMessage) {
+ try {
+ mBinder.onPackageDeleted(packageName, resultCode, errorMessage);
+ } catch (RemoteException e) {
+ // no-op
+ }
+ }
+
+ /** @hide */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /** @hide */
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeStrongBinder(mBinder.asBinder());
+ }
+ }
+
+ /**
* Return the install reason that was recorded when a package was first
* installed for a specific user. Requesting the install reason for another
* user will require the permission INTERACT_ACROSS_USERS_FULL.
@@ -10723,4 +10821,33 @@
throw e.rethrowFromSystemServer();
}
}
+
+ /**
+ * Checks if a package is blocked from uninstall for a particular user. A package can be
+ * blocked from being uninstalled by a device owner or profile owner.
+ * See {@link DevicePolicyManager#setUninstallBlocked(ComponentName, String, boolean)}.
+ *
+ * @param packageName Name of the package being uninstalled.
+ * @param user UserHandle who's ability to uninstall a package is being checked.
+ *
+ * @hide
+ */
+ @SystemApi
+ @NonNull
+ public boolean canUserUninstall(@NonNull String packageName, @NonNull UserHandle user){
+ throw new UnsupportedOperationException(
+ "canUserUninstall not implemented in subclass");
+ }
+
+ /**
+ * See {@link android.provider.Settings.Global#SHOW_NEW_APP_INSTALLED_NOTIFICATION_ENABLED}.
+ *
+ * @hide
+ */
+ @SystemApi
+ @NonNull
+ public boolean shouldShowNewAppInstalledNotification() {
+ throw new UnsupportedOperationException(
+ "isShowNewAppInstalledNotificationEnabled not implemented in subclass");
+ }
}
diff --git a/core/java/android/content/res/Configuration.java b/core/java/android/content/res/Configuration.java
index f47c1e0..96aa624 100644
--- a/core/java/android/content/res/Configuration.java
+++ b/core/java/android/content/res/Configuration.java
@@ -46,6 +46,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.TestApi;
+import android.app.GrammaticalInflectionManager;
import android.app.WindowConfiguration;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.LocaleProto;
@@ -141,6 +142,44 @@
@UnsupportedAppUsage
public boolean userSetLocale;
+ /**
+ * Current user preference for the grammatical gender.
+ */
+ @GrammaticalGender
+ private int mGrammaticalGender;
+
+ /** @hide */
+ @IntDef(prefix = { "GRAMMATICAL_GENDER_" }, value = {
+ GRAMMATICAL_GENDER_NOT_SPECIFIED,
+ GRAMMATICAL_GENDER_NEUTRAL,
+ GRAMMATICAL_GENDER_FEMININE,
+ GRAMMATICAL_GENDER_MASCULINE,
+ })
+ public @interface GrammaticalGender {}
+
+ /**
+ * Constant for grammatical gender: to indicate the user has not specified the terms
+ * of address for the application.
+ */
+ public static final int GRAMMATICAL_GENDER_NOT_SPECIFIED = 0;
+
+ /**
+ * Constant for grammatical gender: to indicate the terms of address the user
+ * preferred in an application is neuter.
+ */
+ public static final int GRAMMATICAL_GENDER_NEUTRAL = 2;
+
+ /**
+ * Constant for grammatical gender: to indicate the terms of address the user
+ * preferred in an application is feminine.
+ */
+ public static final int GRAMMATICAL_GENDER_FEMININE = 3;
+
+ /**
+ * Constant for grammatical gender: to indicate the terms of address the user
+ * preferred in an application is masculine.
+ */
+ public static final int GRAMMATICAL_GENDER_MASCULINE = 4;
/** Constant for {@link #colorMode}: bits that encode whether the screen is wide gamut. */
public static final int COLOR_MODE_WIDE_COLOR_GAMUT_MASK = 0x3;
@@ -1024,6 +1063,7 @@
}
o.fixUpLocaleList();
mLocaleList = o.mLocaleList;
+ mGrammaticalGender = o.mGrammaticalGender;
userSetLocale = o.userSetLocale;
touchscreen = o.touchscreen;
keyboard = o.keyboard;
@@ -1510,6 +1550,7 @@
seq = 0;
windowConfiguration.setToDefaults();
fontWeightAdjustment = FONT_WEIGHT_ADJUSTMENT_UNDEFINED;
+ mGrammaticalGender = GRAMMATICAL_GENDER_NOT_SPECIFIED;
}
/**
@@ -1712,6 +1753,10 @@
changed |= ActivityInfo.CONFIG_FONT_WEIGHT_ADJUSTMENT;
fontWeightAdjustment = delta.fontWeightAdjustment;
}
+ if (delta.mGrammaticalGender != mGrammaticalGender) {
+ changed |= ActivityInfo.CONFIG_GRAMMATICAL_GENDER;
+ mGrammaticalGender = delta.mGrammaticalGender;
+ }
return changed;
}
@@ -1929,6 +1974,10 @@
&& fontWeightAdjustment != delta.fontWeightAdjustment) {
changed |= ActivityInfo.CONFIG_FONT_WEIGHT_ADJUSTMENT;
}
+
+ if (!publicOnly&& mGrammaticalGender != delta.mGrammaticalGender) {
+ changed |= ActivityInfo.CONFIG_GRAMMATICAL_GENDER;
+ }
return changed;
}
@@ -2023,6 +2072,7 @@
dest.writeInt(assetsSeq);
dest.writeInt(seq);
dest.writeInt(fontWeightAdjustment);
+ dest.writeInt(mGrammaticalGender);
}
public void readFromParcel(Parcel source) {
@@ -2055,6 +2105,7 @@
assetsSeq = source.readInt();
seq = source.readInt();
fontWeightAdjustment = source.readInt();
+ mGrammaticalGender = source.readInt();
}
public static final @android.annotation.NonNull Parcelable.Creator<Configuration> CREATOR
@@ -2155,6 +2206,8 @@
if (n != 0) return n;
n = this.fontWeightAdjustment - that.fontWeightAdjustment;
if (n != 0) return n;
+ n = this.mGrammaticalGender - that.mGrammaticalGender;
+ if (n != 0) return n;
// if (n != 0) return n;
return n;
@@ -2196,10 +2249,37 @@
result = 31 * result + densityDpi;
result = 31 * result + assetsSeq;
result = 31 * result + fontWeightAdjustment;
+ result = 31 * result + mGrammaticalGender;
return result;
}
/**
+ * Returns the user preference for the grammatical gender. Will be
+ * {@link #GRAMMATICAL_GENDER_NOT_SPECIFIED} or
+ * {@link #GRAMMATICAL_GENDER_NEUTRAL} or
+ * {@link #GRAMMATICAL_GENDER_FEMININE} or
+ * {@link #GRAMMATICAL_GENDER_MASCULINE}.
+ *
+ * @return The preferred grammatical gender.
+ */
+ @GrammaticalGender
+ public int getGrammaticalGender() {
+ return mGrammaticalGender;
+ }
+
+ /**
+ * Sets the user preference for the grammatical gender. This is only for frameworks to easily
+ * override the gender in the configuration. To update the grammatical gender for an application
+ * use {@link GrammaticalInflectionManager#setRequestedApplicationGrammaticalGender(int)}.
+ *
+ * @param grammaticalGender The preferred grammatical gender.
+ * @hide
+ */
+ public void setGrammaticalGender(@GrammaticalGender int grammaticalGender) {
+ mGrammaticalGender = grammaticalGender;
+ }
+
+ /**
* Get the locale list. This is the preferred way for getting the locales (instead of using
* the direct accessor to {@link #locale}, which would only provide the primary locale).
*
diff --git a/core/java/android/content/res/loader/ResourcesProvider.java b/core/java/android/content/res/loader/ResourcesProvider.java
index a5a1fa689..b097bc0 100644
--- a/core/java/android/content/res/loader/ResourcesProvider.java
+++ b/core/java/android/content/res/loader/ResourcesProvider.java
@@ -83,7 +83,6 @@
* @return the resources provider instance for the {@code overlayInfo}
* @throws IOException when the files can't be loaded.
* @see OverlayManager#getOverlayInfosForTarget(String) to get the list of overlay info.
- * @hide
*/
@SuppressLint("WrongConstant") // TODO(b/238713267): ApkAssets blocks PROPERTY_LOADER
@NonNull
diff --git a/core/java/android/credentials/CredentialManager.java b/core/java/android/credentials/CredentialManager.java
index 23bb22f..d4daf364 100644
--- a/core/java/android/credentials/CredentialManager.java
+++ b/core/java/android/credentials/CredentialManager.java
@@ -276,9 +276,22 @@
*
* @hide
*/
- public static boolean isServiceEnabled() {
+ public static boolean isServiceEnabled(Context context) {
+ if (context == null) {
+ return false;
+ }
+ CredentialManager credentialManager =
+ (CredentialManager) context.getSystemService(Context.CREDENTIAL_SERVICE);
+ if (credentialManager != null) {
+ return credentialManager.isServiceEnabled();
+ }
+ return false;
+ }
+
+ private boolean isServiceEnabled() {
return DeviceConfig.getBoolean(
- DeviceConfig.NAMESPACE_CREDENTIAL, DEVICE_CONFIG_ENABLE_CREDENTIAL_MANAGER, true);
+ DeviceConfig.NAMESPACE_CREDENTIAL, DEVICE_CONFIG_ENABLE_CREDENTIAL_MANAGER,
+ true);
}
private static class GetCredentialTransport extends IGetCredentialCallback.Stub {
diff --git a/core/java/android/credentials/GetCredentialRequest.java b/core/java/android/credentials/GetCredentialRequest.java
index 96a0ce1..85b4468 100644
--- a/core/java/android/credentials/GetCredentialRequest.java
+++ b/core/java/android/credentials/GetCredentialRequest.java
@@ -19,6 +19,7 @@
import static java.util.Objects.requireNonNull;
import android.annotation.NonNull;
+import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
@@ -41,6 +42,12 @@
private final List<GetCredentialOption> mGetCredentialOptions;
/**
+ * The top request level data.
+ */
+ @NonNull
+ private final Bundle mData;
+
+ /**
* Returns the list of credential options to be requested.
*/
@NonNull
@@ -48,9 +55,18 @@
return mGetCredentialOptions;
}
+ /**
+ * Returns the top request level data.
+ */
+ @NonNull
+ public Bundle getData() {
+ return mData;
+ }
+
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeTypedList(mGetCredentialOptions, flags);
+ dest.writeBundle(mData);
}
@Override
@@ -60,10 +76,13 @@
@Override
public String toString() {
- return "GetCredentialRequest {getCredentialOption=" + mGetCredentialOptions + "}";
+ return "GetCredentialRequest {getCredentialOption=" + mGetCredentialOptions
+ + ", data=" + mData
+ + "}";
}
- private GetCredentialRequest(@NonNull List<GetCredentialOption> getCredentialOptions) {
+ private GetCredentialRequest(@NonNull List<GetCredentialOption> getCredentialOptions,
+ @NonNull Bundle data) {
Preconditions.checkCollectionNotEmpty(
getCredentialOptions,
/*valueName=*/ "getCredentialOptions");
@@ -71,6 +90,8 @@
getCredentialOptions,
/*valueName=*/ "getCredentialOptions");
mGetCredentialOptions = getCredentialOptions;
+ mData = requireNonNull(data,
+ "data must not be null");
}
private GetCredentialRequest(@NonNull Parcel in) {
@@ -78,30 +99,47 @@
in.readTypedList(getCredentialOptions, GetCredentialOption.CREATOR);
mGetCredentialOptions = getCredentialOptions;
AnnotationValidations.validate(NonNull.class, null, mGetCredentialOptions);
+
+
+ Bundle data = in.readBundle();
+ mData = data;
+ AnnotationValidations.validate(NonNull.class, null, mData);
}
public static final @NonNull Parcelable.Creator<GetCredentialRequest> CREATOR =
new Parcelable.Creator<GetCredentialRequest>() {
- @Override
- public GetCredentialRequest[] newArray(int size) {
- return new GetCredentialRequest[size];
- }
+ @Override
+ public GetCredentialRequest[] newArray(int size) {
+ return new GetCredentialRequest[size];
+ }
- @Override
- public GetCredentialRequest createFromParcel(@NonNull Parcel in) {
- return new GetCredentialRequest(in);
- }
- };
+ @Override
+ public GetCredentialRequest createFromParcel(@NonNull Parcel in) {
+ return new GetCredentialRequest(in);
+ }
+ };
/** A builder for {@link GetCredentialRequest}. */
public static final class Builder {
- private @NonNull List<GetCredentialOption> mGetCredentialOptions = new ArrayList<>();
+ @NonNull
+ private List<GetCredentialOption> mGetCredentialOptions = new ArrayList<>();
+
+ @NonNull
+ private final Bundle mData;
+
+ /**
+ * @param data the top request level data
+ */
+ public Builder(@NonNull Bundle data) {
+ mData = requireNonNull(data, "data must not be null");
+ }
/**
* Adds a specific type of {@link GetCredentialOption}.
*/
- public @NonNull Builder addGetCredentialOption(
+ @NonNull
+ public Builder addGetCredentialOption(
@NonNull GetCredentialOption getCredentialOption) {
mGetCredentialOptions.add(requireNonNull(
getCredentialOption, "getCredentialOption must not be null"));
@@ -111,7 +149,8 @@
/**
* Sets the list of {@link GetCredentialOption}.
*/
- public @NonNull Builder setGetCredentialOptions(
+ @NonNull
+ public Builder setGetCredentialOptions(
@NonNull List<GetCredentialOption> getCredentialOptions) {
Preconditions.checkCollectionElementsNotNull(
getCredentialOptions,
@@ -125,14 +164,15 @@
*
* @throws IllegalArgumentException If getCredentialOptions is empty.
*/
- public @NonNull GetCredentialRequest build() {
+ @NonNull
+ public GetCredentialRequest build() {
Preconditions.checkCollectionNotEmpty(
mGetCredentialOptions,
/*valueName=*/ "getCredentialOptions");
Preconditions.checkCollectionElementsNotNull(
mGetCredentialOptions,
/*valueName=*/ "getCredentialOptions");
- return new GetCredentialRequest(mGetCredentialOptions);
+ return new GetCredentialRequest(mGetCredentialOptions, mData);
}
}
}
diff --git a/core/java/android/hardware/DataSpace.java b/core/java/android/hardware/DataSpace.java
index 0a14574..b8b1eaa 100644
--- a/core/java/android/hardware/DataSpace.java
+++ b/core/java/android/hardware/DataSpace.java
@@ -521,9 +521,7 @@
public static final int DATASPACE_BT2020_HLG = 168165376;
/**
- * ITU-R Recommendation 2020 (BT.2020)
- *
- * Ultra High-definition television.
+ * Perceptual Quantizer encoding.
*
* <p>Composed of the following -</p>
* <pre>
diff --git a/core/java/android/hardware/camera2/CameraCaptureSession.java b/core/java/android/hardware/camera2/CameraCaptureSession.java
index 8e4a108..ff6e897 100644
--- a/core/java/android/hardware/camera2/CameraCaptureSession.java
+++ b/core/java/android/hardware/camera2/CameraCaptureSession.java
@@ -1245,10 +1245,28 @@
* the start of exposure, particularly when autoexposure is changing exposure duration
* between frames.</p>
*
- * <p>This timestamp may not match {@link CaptureResult#SENSOR_TIMESTAMP the result
- * timestamp field}. It will, however, match the timestamp of buffers sent to the
- * output surfaces with {@link OutputConfiguration#TIMESTAMP_BASE_READOUT_SENSOR}
- * timestamp base.</p>
+ * <p>The timestamps match the timestamps of the output surfaces with readout timestamp
+ * enabled (via {@link OutputConfiguration#useReadoutTimestamp}) if:</p>
+ * <ul>
+ * <li> Timestamp base is {@link OutputConfiguration#TIMESTAMP_BASE_DEFAULT} and the
+ * output
+ * <ul>
+ * <li> is not a SurfaceView surface, and </li>
+ * <li> is not a MediaRecoder, MediaCodec, or ImageReader surface with {@link
+ * android.hardware.HardwareBuffer#USAGE_VIDEO_ENCODE} usage flag or the device's {@link
+ * CameraCharacteristics#SENSOR_INFO_TIMESTAMP_SOURCE} is {@code UNKNOWN}</li>
+ * </ul>
+ * </li>
+ * <li> Timestamp base is {@link OutputConfiguration#TIMESTAMP_BASE_SENSOR},</li>
+ * <li> Timestamp base is {@link OutputConfiguration#TIMESTAMP_BASE_MONOTONIC} and the
+ * device's {@link CameraCharacteristics#SENSOR_INFO_TIMESTAMP_SOURCE} is {@code
+ * UNKNOWN},</li>
+ * <li> Timestamp base is {@link OutputConfiguration#TIMESTAMP_BASE_REALTIME} and the
+ * device's {@link CameraCharacteristics#SENSOR_INFO_TIMESTAMP_SOURCE} is {@code REALTIME}
+ * </li>
+ * </ul>
+ * <p>Otherwise, the timestamps won't match the timestamp of the output surfaces. See
+ * the possible parameters for {@link OutputConfiguration#setTimestampBase} for details.</p>
*
* <p>This callback will be called only if {@link
* CameraCharacteristics#SENSOR_READOUT_TIMESTAMP} is
@@ -1261,8 +1279,6 @@
* the timestamp at the input image's start of readout for a
* reprocess request, in nanoseconds.
* @param frameNumber the frame number for this capture
- *
- * @hide
*/
public void onReadoutStarted(@NonNull CameraCaptureSession session,
@NonNull CaptureRequest request, long timestamp, long frameNumber) {
diff --git a/core/java/android/hardware/camera2/CameraCharacteristics.java b/core/java/android/hardware/camera2/CameraCharacteristics.java
index a6f7e94..11b80cc 100644
--- a/core/java/android/hardware/camera2/CameraCharacteristics.java
+++ b/core/java/android/hardware/camera2/CameraCharacteristics.java
@@ -3756,6 +3756,7 @@
* <li>{@link #SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_RECORD VIDEO_RECORD}</li>
* <li>{@link #SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW_VIDEO_STILL PREVIEW_VIDEO_STILL}</li>
* <li>{@link #SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_CALL VIDEO_CALL}</li>
+ * <li>{@link #SCALER_AVAILABLE_STREAM_USE_CASES_CROPPED_RAW CROPPED_RAW}</li>
* </ul>
*
* <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
@@ -3765,6 +3766,7 @@
* @see #SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_RECORD
* @see #SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW_VIDEO_STILL
* @see #SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_CALL
+ * @see #SCALER_AVAILABLE_STREAM_USE_CASES_CROPPED_RAW
*/
@PublicKey
@NonNull
@@ -4588,22 +4590,26 @@
/**
* <p>Whether or not the camera device supports readout timestamp and
- * onReadoutStarted callback.</p>
- * <p>If this tag is HARDWARE, the camera device calls onReadoutStarted in addition to the
- * onCaptureStarted callback for each capture. The timestamp passed into the callback
- * is the start of camera image readout rather than the start of the exposure. In
- * addition, the application can configure an
- * {@link android.hardware.camera2.params.OutputConfiguration } with
- * TIMESTAMP_BASE_READOUT_SENSOR timestamp base, in which case, the timestamp of the
- * output surface matches the timestamp from the corresponding onReadoutStarted callback.</p>
+ * {@code onReadoutStarted} callback.</p>
+ * <p>If this tag is {@code HARDWARE}, the camera device calls
+ * {@link CameraCaptureSession.CaptureCallback#onReadoutStarted } in addition to the
+ * {@link CameraCaptureSession.CaptureCallback#onCaptureStarted } callback for each capture.
+ * The timestamp passed into the callback is the start of camera image readout rather than
+ * the start of the exposure. The timestamp source of
+ * {@link CameraCaptureSession.CaptureCallback#onReadoutStarted } is the same as that of
+ * {@link CameraCaptureSession.CaptureCallback#onCaptureStarted }.</p>
+ * <p>In addition, the application can switch an output surface's timestamp from start of
+ * exposure to start of readout by calling
+ * {@link android.hardware.camera2.params.OutputConfiguration#useReadoutTimestamp }.</p>
* <p>The readout timestamp is beneficial for video recording, because the encoder favors
* uniform timestamps, and the readout timestamps better reflect the cadence camera sensors
* output data.</p>
- * <p>If this tag is HARDWARE, the camera device produces the start-of-exposure and
- * start-of-readout together. As a result, the onReadoutStarted is called right after
- * onCaptureStarted. The difference in start-of-readout and start-of-exposure is the sensor
- * exposure time, plus certain constant offset. The offset is usually due to camera sensor
- * level crop, and it remains constant for a given camera sensor mode.</p>
+ * <p>Note that the camera device produces the start-of-exposure and start-of-readout callbacks
+ * together. As a result, the {@link CameraCaptureSession.CaptureCallback#onReadoutStarted }
+ * is called right after {@link CameraCaptureSession.CaptureCallback#onCaptureStarted }. The
+ * difference in start-of-readout and start-of-exposure is the sensor exposure time, plus
+ * certain constant offset. The offset is usually due to camera sensor level crop, and it is
+ * generally constant over time for the same set of output resolutions and capture settings.</p>
* <p><b>Possible values:</b></p>
* <ul>
* <li>{@link #SENSOR_READOUT_TIMESTAMP_NOT_SUPPORTED NOT_SUPPORTED}</li>
diff --git a/core/java/android/hardware/camera2/CameraDevice.java b/core/java/android/hardware/camera2/CameraDevice.java
index bf2e563..577c8a3 100644
--- a/core/java/android/hardware/camera2/CameraDevice.java
+++ b/core/java/android/hardware/camera2/CameraDevice.java
@@ -900,13 +900,27 @@
* <tr> <td>{@code PRIV}</td><td id="rb">{@code PREVIEW}</td><td id="rb">{@code PREVIEW}</td> <td>{@code YUV / PRIV}</td><td id="rb">{@code RECORD}</td><td id="rb">{@code VIDEO_RECORD}</td> <td colspan="3" id="rb"></td> <td>Preview with video recording or in-app video processing</td> </tr>
* <tr> <td>{@code PRIV}</td><td id="rb">{@code PREVIEW}</td><td id="rb">{@code PREVIEW}</td> <td>{@code YUV}</td><td id="rb">{@code PREVIEW}</td><td id="rb">{@code PREVIEW}</td> <td colspan="3" id="rb"></td> <td>Preview with in-application image processing</td> </tr>
* <tr> <td>{@code PRIV}</td><td id="rb">{@code PREVIEW}</td><td id="rb">{@code PREVIEW}</td> <td>{@code YUV / PRIV}</td><td id="rb">{@code s1440p}</td><td id="rb">{@code VIDEO_CALL}</td> <td colspan="3" id="rb"></td> <td>Preview with video call</td> </tr>
- * <tr> <td>{@code YUV / PRIV}</td><td id="rb">{@code s1440p}</td><td id="rb">{@code PREVIEW_VIDEO_STILL}</td> <td>{@code YUV / JPEG}</td><td id="rb">{@code MAXIMUM}</td><td id="rb">{@code STILL_CAPTURE}</td> <td colspan="3" id="rb"></td> <td>Multi-purpose stream with JPEG or YUV still capture</td> </tr>
+ * <tr> <td>{@code YUV / PRIV}</td><td id="rb">{@code s1440p}</td><td id="rb">{@code PREVIEW_VIDEO_STILL}</td> <td>{@code YUV / JPEG}</td><td id="rb">{@code MAXIMUM}</td><td id="rb">{@code STILL_CAPTURE}</td> <td colspan="3" id="rb"></td> <td>MultI-purpose stream with JPEG or YUV still capture</td> </tr>
* <tr> <td>{@code YUV}</td><td id="rb">{@code PREVIEW}</td><td id="rb">{@code STILL_CAPTURE}</td> <td>{@code JPEG}</td><td id="rb">{@code MAXIMUM}</td><td id="rb">{@code STILL_CAPTURE}</td> <td colspan="3" id="rb"></td> <td>YUV and JPEG concurrent still image capture (for testing)</td> </tr>
* <tr> <td>{@code PRIV}</td><td id="rb">{@code PREVIEW}</td><td id="rb">{@code PREVIEW}</td> <td>{@code YUV / PRIV}</td><td id="rb">{@code RECORD}</td><td id="rb">{@code VIDEO_RECORD}</td> <td>{@code YUV / JPEG}</td><td id="rb">{@code RECORD}</td><td id="rb">{@code STILL_CAPTURE}</td> <td>Preview, video record and JPEG or YUV video snapshot</td> </tr>
* <tr> <td>{@code PRIV}</td><td id="rb">{@code PREVIEW}</td><td id="rb">{@code PREVIEW}</td> <td>{@code YUV}</td><td id="rb">{@code PREVIEW}</td><td id="rb">{@code PREVIEW}</td> <td>{@code YUV / JPEG}</td><td id="rb">{@code MAXIMUM}</td><td id="rb">{@code STILL_CAPTURE}</td> <td>Preview, in-application image processing, and JPEG or YUV still image capture</td> </tr>
* </table><br>
* </p>
*
+ * <p>Devices that include the {@link CameraMetadata#SCALER_AVAILABLE_STREAM_USE_CASES_CROPPED_RAW}
+ * stream use-case in {@link CameraCharacteristics#SCALER_AVAILABLE_STREAM_USE_CASES},
+ * support the additional stream combinations below:
+ *
+ * <table>
+ * <tr><th colspan="10">STREAM_USE_CASE_CROPPED_RAW capability additional guaranteed configurations</th></tr>
+ * <tr><th colspan="3" id="rb">Target 1</th><th colspan="3" id="rb">Target 2</th><th colspan="3" id="rb">Target 3</th> <th rowspan="2">Sample use case(s)</th> </tr>
+ * <tr><th>Type</th><th id="rb">Max size</th><th>Usecase</th><th>Type</th><th id="rb">Max size</th><th>Usecase</th><th>Type</th><th id="rb">Max size</th><th>Usecase</th> </tr>
+ * <tr> <td>{@code RAW}</td><td id="rb">{@code MAXIMUM}</td><td id="rb">{@code CROPPED_RAW}</td> <td colspan="3" id="rb"></td> <td colspan="3" id="rb"></td> <td>Cropped RAW still capture without preview</td> </tr>
+ * <tr> <td>{@code PRIV / YUV}</td><td id="rb">{@code PREVIEW}</td><td id="rb">{@code PREVIEW}</td> <td>{@code RAW}</td><td id="rb">{@code MAXIMUM}</td><td id="rb">{@code CROPPED_RAW}</td> <td colspan="3" id="rb"></td> <td>Preview with cropped RAW still capture</td> </tr>
+ * <tr> <td>{@code PRIV / YUV}</td><td id="rb">{@code PREVIEW}</td><td id="rb">{@code PREVIEW}</td> <td>{@code YUV / JPEG}</td><td id="rb">{@code MAXIMUM}</td><td id="rb">{@code STILL_CAPTURE}</td> <td>{@code RAW}</td><td id="rb">{@code MAXIMUM}</td><td id="rb">{@code CROPPED_RAW}</td> <td>Preview with YUV / JPEG and cropped RAW still capture</td> </tr>
+ * <tr> <td>{@code PRIV / YUV}</td><td id="rb">{@code PREVIEW}</td><td id="rb">{@code PREVIEW}</td> <td>{@code PRIV / YUV}</td><td id="rb">{@code PREVIEW}</td><td id="rb">{@code VIDEO_RECORD / PREVIEW}</td> <td>{@code RAW}</td><td id="rb">{@code MAXIMUM}</td><td id="rb">{@code CROPPED_RAW}</td> <td>Video recording with preview and cropped RAW still capture</td> </tr>
+ *
+ *
*<p> For devices where {@link CameraCharacteristics#CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES}
* includes {@link CameraMetadata#CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION},
* the following stream combinations are guaranteed,
diff --git a/core/java/android/hardware/camera2/CameraMetadata.java b/core/java/android/hardware/camera2/CameraMetadata.java
index b2428b1..788302b 100644
--- a/core/java/android/hardware/camera2/CameraMetadata.java
+++ b/core/java/android/hardware/camera2/CameraMetadata.java
@@ -1489,6 +1489,31 @@
public static final int SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_CALL = 0x5;
/**
+ * <p>Cropped RAW stream when the client chooses to crop the field of view.</p>
+ * <p>Certain types of image sensors can run in binned modes in order to improve signal to
+ * noise ratio while capturing frames. However, at certain zoom levels and / or when
+ * other scene conditions are deemed fit, the camera sub-system may choose to un-bin and
+ * remosaic the sensor's output. This results in a RAW frame which is cropped in field
+ * of view and yet has the same number of pixels as full field of view RAW, thereby
+ * improving image detail.</p>
+ * <p>The resultant field of view of the RAW stream will be greater than or equal to
+ * croppable non-RAW streams. The effective crop region for this RAW stream will be
+ * reflected in the CaptureResult key {@link CaptureResult#SCALER_RAW_CROP_REGION android.scaler.rawCropRegion}.</p>
+ * <p>If this stream use case is set on a non-RAW stream, i.e. not one of :</p>
+ * <ul>
+ * <li>{@link android.graphics.ImageFormat#RAW_SENSOR RAW_SENSOR}</li>
+ * <li>{@link android.graphics.ImageFormat#RAW10 RAW10}</li>
+ * <li>{@link android.graphics.ImageFormat#RAW12 RAW12}</li>
+ * </ul>
+ * <p>session configuration is not guaranteed to succeed.</p>
+ * <p>This stream use case may not be supported on some devices.</p>
+ *
+ * @see CaptureResult#SCALER_RAW_CROP_REGION
+ * @see CameraCharacteristics#SCALER_AVAILABLE_STREAM_USE_CASES
+ */
+ public static final int SCALER_AVAILABLE_STREAM_USE_CASES_CROPPED_RAW = 0x6;
+
+ /**
* <p>Vendor defined use cases. These depend on the vendor implementation.</p>
* @see CameraCharacteristics#SCALER_AVAILABLE_STREAM_USE_CASES
* @hide
@@ -1700,9 +1725,8 @@
/**
* <p>This camera device supports the onReadoutStarted callback as well as outputting
- * readout timestamp for streams with TIMESTAMP_BASE_READOUT_SENSOR timestamp base. The
- * readout timestamp is generated by the camera hardware and it has the same accuracy
- * and timing characteristics of the start-of-exposure time.</p>
+ * readout timestamps. The readout timestamp is generated by the camera hardware and it
+ * has the same accuracy and timing characteristics of the start-of-exposure time.</p>
* @see CameraCharacteristics#SENSOR_READOUT_TIMESTAMP
*/
public static final int SENSOR_READOUT_TIMESTAMP_HARDWARE = 1;
diff --git a/core/java/android/hardware/camera2/CaptureRequest.java b/core/java/android/hardware/camera2/CaptureRequest.java
index 43bfdcc..3d83009 100644
--- a/core/java/android/hardware/camera2/CaptureRequest.java
+++ b/core/java/android/hardware/camera2/CaptureRequest.java
@@ -3086,9 +3086,9 @@
* <p>Output streams use this rectangle to produce their output, cropping to a smaller region
* if necessary to maintain the stream's aspect ratio, then scaling the sensor input to
* match the output's configured resolution.</p>
- * <p>The crop region is applied after the RAW to other color space (e.g. YUV)
- * conversion. Since raw streams (e.g. RAW16) don't have the conversion stage, they are not
- * croppable. The crop region will be ignored by raw streams.</p>
+ * <p>The crop region is usually applied after the RAW to other color space (e.g. YUV)
+ * conversion. As a result RAW streams are not croppable unless supported by the
+ * camera device. See {@link CameraCharacteristics#SCALER_AVAILABLE_STREAM_USE_CASES android.scaler.availableStreamUseCases}#CROPPED_RAW for details.</p>
* <p>For non-raw streams, any additional per-stream cropping will be done to maximize the
* final pixel area of the stream.</p>
* <p>For example, if the crop region is set to a 4:3 aspect ratio, then 4:3 streams will use
@@ -3183,6 +3183,7 @@
* @see CaptureRequest#CONTROL_ZOOM_RATIO
* @see CaptureRequest#DISTORTION_CORRECTION_MODE
* @see CameraCharacteristics#SCALER_AVAILABLE_MAX_DIGITAL_ZOOM
+ * @see CameraCharacteristics#SCALER_AVAILABLE_STREAM_USE_CASES
* @see CameraCharacteristics#SCALER_CROPPING_TYPE
* @see CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE
* @see CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION
diff --git a/core/java/android/hardware/camera2/CaptureResult.java b/core/java/android/hardware/camera2/CaptureResult.java
index fb52cc6..dad7d3e 100644
--- a/core/java/android/hardware/camera2/CaptureResult.java
+++ b/core/java/android/hardware/camera2/CaptureResult.java
@@ -3748,9 +3748,9 @@
* <p>Output streams use this rectangle to produce their output, cropping to a smaller region
* if necessary to maintain the stream's aspect ratio, then scaling the sensor input to
* match the output's configured resolution.</p>
- * <p>The crop region is applied after the RAW to other color space (e.g. YUV)
- * conversion. Since raw streams (e.g. RAW16) don't have the conversion stage, they are not
- * croppable. The crop region will be ignored by raw streams.</p>
+ * <p>The crop region is usually applied after the RAW to other color space (e.g. YUV)
+ * conversion. As a result RAW streams are not croppable unless supported by the
+ * camera device. See {@link CameraCharacteristics#SCALER_AVAILABLE_STREAM_USE_CASES android.scaler.availableStreamUseCases}#CROPPED_RAW for details.</p>
* <p>For non-raw streams, any additional per-stream cropping will be done to maximize the
* final pixel area of the stream.</p>
* <p>For example, if the crop region is set to a 4:3 aspect ratio, then 4:3 streams will use
@@ -3845,6 +3845,7 @@
* @see CaptureRequest#CONTROL_ZOOM_RATIO
* @see CaptureRequest#DISTORTION_CORRECTION_MODE
* @see CameraCharacteristics#SCALER_AVAILABLE_MAX_DIGITAL_ZOOM
+ * @see CameraCharacteristics#SCALER_AVAILABLE_STREAM_USE_CASES
* @see CameraCharacteristics#SCALER_CROPPING_TYPE
* @see CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE
* @see CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION
@@ -3952,6 +3953,60 @@
new Key<Integer>("android.scaler.rotateAndCrop", int.class);
/**
+ * <p>The region of the sensor that corresponds to the RAW read out for this
+ * capture when the stream use case of a RAW stream is set to CROPPED_RAW.</p>
+ * <p>The coordinate system follows that of {@link CameraCharacteristics#SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE android.sensor.info.preCorrectionActiveArraySize}.</p>
+ * <p>This CaptureResult key will be set when the corresponding CaptureRequest has a RAW target
+ * with stream use case set to
+ * {@link android.hardware.camera2.CameraMetadata#SCALER_AVAILABLE_STREAM_USE_CASES_CROPPED_RAW },
+ * otherwise it will be {@code null}.
+ * The value of this key specifies the region of the sensor used for the RAW capture and can
+ * be used to calculate the corresponding field of view of RAW streams.
+ * This field of view will always be >= field of view for (processed) non-RAW streams for the
+ * capture. Note: The region specified may not necessarily be centered.</p>
+ * <p>For example: Assume a camera device has a pre correction active array size of
+ * {@code {0, 0, 1500, 2000}}. If the RAW_CROP_REGION is {@code {500, 375, 1500, 1125}}, that
+ * corresponds to a centered crop of 1/4th of the full field of view RAW stream.</p>
+ * <p>The metadata keys which describe properties of RAW frames:</p>
+ * <ul>
+ * <li>{@link CaptureResult#STATISTICS_HOT_PIXEL_MAP android.statistics.hotPixelMap}</li>
+ * <li>{@link CaptureResult#STATISTICS_LENS_SHADING_CORRECTION_MAP android.statistics.lensShadingCorrectionMap}</li>
+ * <li>{@link CameraCharacteristics#LENS_DISTORTION android.lens.distortion}</li>
+ * <li>{@link CameraCharacteristics#LENS_POSE_TRANSLATION android.lens.poseTranslation}</li>
+ * <li>{@link CameraCharacteristics#LENS_POSE_ROTATION android.lens.poseRotation}</li>
+ * <li>{@link CameraCharacteristics#LENS_DISTORTION android.lens.distortion}</li>
+ * <li>{@link CameraCharacteristics#LENS_INTRINSIC_CALIBRATION android.lens.intrinsicCalibration}</li>
+ * </ul>
+ * <p>should be interpreted in the effective after raw crop field-of-view coordinate system.
+ * In this coordinate system,
+ * {preCorrectionActiveArraySize.left, preCorrectionActiveArraySize.top} corresponds to the
+ * the top left corner of the cropped RAW frame and
+ * {preCorrectionActiveArraySize.right, preCorrectionActiveArraySize.bottom} corresponds to
+ * the bottom right corner. Client applications must use the values of the keys
+ * in the CaptureResult metadata if present.</p>
+ * <p>Crop regions (android.scaler.CropRegion), AE/AWB/AF regions and face coordinates still
+ * use the {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE android.sensor.info.activeArraySize} coordinate system as usual.</p>
+ * <p><b>Units</b>: Pixel coordinates relative to
+ * {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE android.sensor.info.activeArraySize} or
+ * {@link CameraCharacteristics#SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE android.sensor.info.preCorrectionActiveArraySize} depending on distortion correction
+ * capability and mode</p>
+ * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+ *
+ * @see CameraCharacteristics#LENS_DISTORTION
+ * @see CameraCharacteristics#LENS_INTRINSIC_CALIBRATION
+ * @see CameraCharacteristics#LENS_POSE_ROTATION
+ * @see CameraCharacteristics#LENS_POSE_TRANSLATION
+ * @see CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE
+ * @see CameraCharacteristics#SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE
+ * @see CaptureResult#STATISTICS_HOT_PIXEL_MAP
+ * @see CaptureResult#STATISTICS_LENS_SHADING_CORRECTION_MAP
+ */
+ @PublicKey
+ @NonNull
+ public static final Key<android.graphics.Rect> SCALER_RAW_CROP_REGION =
+ new Key<android.graphics.Rect>("android.scaler.rawCropRegion", android.graphics.Rect.class);
+
+ /**
* <p>Duration each pixel is exposed to
* light.</p>
* <p>If the sensor can't expose this exact duration, it will shorten the
@@ -5643,4 +5698,5 @@
+
}
diff --git a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
index 9a16474..8d742b5 100644
--- a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
+++ b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
@@ -1461,6 +1461,23 @@
return ret;
}
+ private boolean isCroppedRawSupported() {
+ boolean ret = false;
+
+ long[] streamUseCases =
+ getBase(CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES);
+ if (streamUseCases == null) {
+ return false;
+ }
+ for (long useCase : streamUseCases) {
+ if (useCase == CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_CROPPED_RAW) {
+ return true;
+ }
+ }
+
+ return ret;
+ }
+
private MandatoryStreamCombination[] getMandatoryStreamCombinationsHelper(
int mandatoryStreamsType) {
int[] capabilities = getBase(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES);
@@ -1472,7 +1489,8 @@
int hwLevel = getBase(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
MandatoryStreamCombination.Builder build = new MandatoryStreamCombination.Builder(
mCameraId, hwLevel, mDisplaySize, caps, getStreamConfigurationMap(),
- getStreamConfigurationMapMaximumResolution(), isPreviewStabilizationSupported());
+ getStreamConfigurationMapMaximumResolution(), isPreviewStabilizationSupported(),
+ isCroppedRawSupported());
List<MandatoryStreamCombination> combs = null;
switch (mandatoryStreamsType) {
diff --git a/core/java/android/hardware/camera2/params/MandatoryStreamCombination.java b/core/java/android/hardware/camera2/params/MandatoryStreamCombination.java
index 0905e1b..6f77d12 100644
--- a/core/java/android/hardware/camera2/params/MandatoryStreamCombination.java
+++ b/core/java/android/hardware/camera2/params/MandatoryStreamCombination.java
@@ -375,6 +375,8 @@
CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW_VIDEO_STILL;
private static final long STREAM_USE_CASE_VIDEO_CALL =
CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_CALL;
+ private static final long STREAM_USE_CASE_CROPPED_RAW =
+ CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_CROPPED_RAW;
/**
* Create a new {@link MandatoryStreamCombination}.
@@ -1262,6 +1264,86 @@
"Preview, in-application image processing, and YUV still image capture"),
};
+ private static StreamCombinationTemplate sCroppedRawStreamUseCaseCombinations[] = {
+ // Single stream combination
+ new StreamCombinationTemplate(new StreamTemplate [] {
+ new StreamTemplate(ImageFormat.RAW_SENSOR, SizeThreshold.MAXIMUM,
+ STREAM_USE_CASE_CROPPED_RAW)},
+ "Cropped RAW still image capture without preview"),
+
+ // 2 Stream combinations
+ new StreamCombinationTemplate(new StreamTemplate [] {
+ new StreamTemplate(ImageFormat.PRIVATE, SizeThreshold.PREVIEW,
+ STREAM_USE_CASE_PREVIEW),
+ new StreamTemplate(ImageFormat.RAW_SENSOR, SizeThreshold.MAXIMUM,
+ STREAM_USE_CASE_CROPPED_RAW)},
+ "Cropped RAW still image capture with preview"),
+ new StreamCombinationTemplate(new StreamTemplate [] {
+ new StreamTemplate(ImageFormat.YUV_420_888, SizeThreshold.PREVIEW,
+ STREAM_USE_CASE_PREVIEW),
+ new StreamTemplate(ImageFormat.RAW_SENSOR, SizeThreshold.MAXIMUM,
+ STREAM_USE_CASE_CROPPED_RAW)},
+ "In-app image processing with cropped RAW still image capture"),
+
+ // 3 stream combinations
+ new StreamCombinationTemplate(new StreamTemplate [] {
+ new StreamTemplate(ImageFormat.PRIVATE, SizeThreshold.PREVIEW,
+ STREAM_USE_CASE_PREVIEW),
+ new StreamTemplate(ImageFormat.YUV_420_888, SizeThreshold.MAXIMUM,
+ STREAM_USE_CASE_STILL_CAPTURE),
+ new StreamTemplate(ImageFormat.RAW_SENSOR, SizeThreshold.MAXIMUM,
+ STREAM_USE_CASE_CROPPED_RAW)},
+ "Preview with YUV and RAW still image capture"),
+ new StreamCombinationTemplate(new StreamTemplate [] {
+ new StreamTemplate(ImageFormat.YUV_420_888, SizeThreshold.PREVIEW,
+ STREAM_USE_CASE_PREVIEW),
+ new StreamTemplate(ImageFormat.YUV_420_888, SizeThreshold.MAXIMUM,
+ STREAM_USE_CASE_STILL_CAPTURE),
+ new StreamTemplate(ImageFormat.RAW_SENSOR, SizeThreshold.MAXIMUM,
+ STREAM_USE_CASE_CROPPED_RAW)},
+ "In-app image processing with YUV and RAW still image capture"),
+ new StreamCombinationTemplate(new StreamTemplate [] {
+ new StreamTemplate(ImageFormat.PRIVATE, SizeThreshold.PREVIEW,
+ STREAM_USE_CASE_PREVIEW),
+ new StreamTemplate(ImageFormat.JPEG, SizeThreshold.MAXIMUM,
+ STREAM_USE_CASE_STILL_CAPTURE),
+ new StreamTemplate(ImageFormat.RAW_SENSOR, SizeThreshold.MAXIMUM,
+ STREAM_USE_CASE_CROPPED_RAW)},
+ "Preview with JPEG and RAW still image capture"),
+ new StreamCombinationTemplate(new StreamTemplate [] {
+ new StreamTemplate(ImageFormat.YUV_420_888, SizeThreshold.PREVIEW,
+ STREAM_USE_CASE_PREVIEW),
+ new StreamTemplate(ImageFormat.JPEG, SizeThreshold.MAXIMUM,
+ STREAM_USE_CASE_STILL_CAPTURE),
+ new StreamTemplate(ImageFormat.RAW_SENSOR, SizeThreshold.MAXIMUM,
+ STREAM_USE_CASE_CROPPED_RAW)},
+ "In-app image processing with JPEG and RAW still image capture"),
+ new StreamCombinationTemplate(new StreamTemplate [] {
+ new StreamTemplate(ImageFormat.PRIVATE, SizeThreshold.PREVIEW,
+ STREAM_USE_CASE_PREVIEW),
+ new StreamTemplate(ImageFormat.PRIVATE, SizeThreshold.PREVIEW,
+ STREAM_USE_CASE_RECORD),
+ new StreamTemplate(ImageFormat.RAW_SENSOR, SizeThreshold.MAXIMUM,
+ STREAM_USE_CASE_CROPPED_RAW)},
+ "Preview with video recording and RAW snapshot"),
+ new StreamCombinationTemplate(new StreamTemplate [] {
+ new StreamTemplate(ImageFormat.PRIVATE, SizeThreshold.PREVIEW,
+ STREAM_USE_CASE_PREVIEW),
+ new StreamTemplate(ImageFormat.YUV_420_888, SizeThreshold.PREVIEW,
+ STREAM_USE_CASE_PREVIEW),
+ new StreamTemplate(ImageFormat.RAW_SENSOR, SizeThreshold.MAXIMUM,
+ STREAM_USE_CASE_CROPPED_RAW)},
+ "Preview with in-app image processing and RAW still image capture"),
+ new StreamCombinationTemplate(new StreamTemplate [] {
+ new StreamTemplate(ImageFormat.YUV_420_888, SizeThreshold.PREVIEW,
+ STREAM_USE_CASE_PREVIEW),
+ new StreamTemplate(ImageFormat.YUV_420_888, SizeThreshold.PREVIEW,
+ STREAM_USE_CASE_PREVIEW),
+ new StreamTemplate(ImageFormat.RAW_SENSOR, SizeThreshold.MAXIMUM,
+ STREAM_USE_CASE_CROPPED_RAW)},
+ "Two input in-app processing and RAW still image capture"),
+ };
+
private static StreamCombinationTemplate sPreviewStabilizedStreamCombinations[] = {
// 1 stream combinations
new StreamCombinationTemplate(new StreamTemplate [] {
@@ -1317,7 +1399,8 @@
private StreamConfigurationMap mStreamConfigMap;
private StreamConfigurationMap mStreamConfigMapMaximumResolution;
private boolean mIsHiddenPhysicalCamera;
- private boolean mIsPreviewStabilizationSupported;
+ private boolean mIsPreviewStabilizationSupported = false;
+ private boolean mIsCroppedRawSupported = false;
private final Size kPreviewSizeBound = new Size(1920, 1088);
@@ -1331,10 +1414,13 @@
* @param sm The camera device stream configuration map.
* @param smMaxResolution The camera device stream configuration map when it runs in max
* resolution mode.
+ * @param previewStabilization The camera device supports preview stabilization.
+ * @param croppedRaw The camera device supports the cropped raw stream use case.
*/
public Builder(int cameraId, int hwLevel, @NonNull Size displaySize,
@NonNull List<Integer> capabilities, @NonNull StreamConfigurationMap sm,
- StreamConfigurationMap smMaxResolution, boolean previewStabilization) {
+ StreamConfigurationMap smMaxResolution, boolean previewStabilization,
+ boolean isCroppedRawSupported) {
mCameraId = cameraId;
mDisplaySize = displaySize;
mCapabilities = capabilities;
@@ -1344,6 +1430,7 @@
mIsHiddenPhysicalCamera =
CameraManager.isHiddenPhysicalCamera(Integer.toString(mCameraId));
mIsPreviewStabilizationSupported = previewStabilization;
+ mIsCroppedRawSupported = isCroppedRawSupported;
}
private @Nullable List<MandatoryStreamCombination>
@@ -1483,10 +1570,23 @@
Log.e(TAG, "Available size enumeration failed!");
return null;
}
+ ArrayList<StreamCombinationTemplate> availableTemplates =
+ new ArrayList<StreamCombinationTemplate> ();
+ availableTemplates.addAll(Arrays.asList(sStreamUseCaseCombinations));
ArrayList<MandatoryStreamCombination> availableStreamCombinations = new ArrayList<>();
- availableStreamCombinations.ensureCapacity(sStreamUseCaseCombinations.length);
- for (StreamCombinationTemplate combTemplate : sStreamUseCaseCombinations) {
+ int capacity = sStreamUseCaseCombinations.length;
+ if (mIsCroppedRawSupported) {
+ capacity += sCroppedRawStreamUseCaseCombinations.length;
+ availableStreamCombinations.ensureCapacity(capacity);
+ availableTemplates.addAll(Arrays.asList(sCroppedRawStreamUseCaseCombinations));
+ }
+ else {
+ availableStreamCombinations.ensureCapacity(capacity);
+ }
+
+
+ for (StreamCombinationTemplate combTemplate : availableTemplates) {
ArrayList<MandatoryStreamInformation> streamsInfo =
new ArrayList<MandatoryStreamInformation>();
streamsInfo.ensureCapacity(combTemplate.mStreamTemplates.length);
@@ -2012,6 +2112,7 @@
private @Nullable HashMap<Pair<SizeThreshold, Integer>, List<Size>>
enumerateAvailableSizes() {
final int[] formats = {
+ ImageFormat.RAW_SENSOR,
ImageFormat.PRIVATE,
ImageFormat.YUV_420_888,
ImageFormat.JPEG,
diff --git a/core/java/android/hardware/camera2/params/OutputConfiguration.java b/core/java/android/hardware/camera2/params/OutputConfiguration.java
index f4b87b9..8b7c5ec 100644
--- a/core/java/android/hardware/camera2/params/OutputConfiguration.java
+++ b/core/java/android/hardware/camera2/params/OutputConfiguration.java
@@ -191,7 +191,9 @@
*
* <p>The timestamps of the output images are in the time base as specified by {@link
* CameraCharacteristics#SENSOR_INFO_TIMESTAMP_SOURCE}. The application can look up the
- * corresponding result metadata for a particular output image using this timestamp.</p>
+ * corresponding result metadata by matching the timestamp with a {@link
+ * CameraCaptureSession.CaptureCallback#onCaptureStarted}, or with a {@link
+ * CameraCaptureSession.CaptureCallback#onReadoutStarted} if readout timestamp is used.</p>
*/
public static final int TIMESTAMP_BASE_SENSOR = 1;
@@ -204,7 +206,8 @@
*
* <p>If the camera device's {@link CameraCharacteristics#SENSOR_INFO_TIMESTAMP_SOURCE} is
* REALTIME, timestamps with this time base cannot directly match the timestamps in
- * {@link CameraCaptureSession.CaptureCallback#onCaptureStarted} or the sensor timestamps in
+ * {@link CameraCaptureSession.CaptureCallback#onCaptureStarted}, {@link
+ * CameraCaptureSession.CaptureCallback#onReadoutStarted}, or the sensor timestamps in
* {@link android.hardware.camera2.CaptureResult}.</p>
*/
public static final int TIMESTAMP_BASE_MONOTONIC = 2;
@@ -218,7 +221,8 @@
*
* <p>If the camera device's {@link CameraCharacteristics#SENSOR_INFO_TIMESTAMP_SOURCE} is
* UNKNOWN, timestamps with this time base cannot directly match the timestamps in
- * {@link CameraCaptureSession.CaptureCallback#onCaptureStarted} or the sensor timestamps in
+ * {@link CameraCaptureSession.CaptureCallback#onCaptureStarted}, {@link
+ * CameraCaptureSession.CaptureCallback#onReadoutStarted}, or the sensor timestamps in
* {@link android.hardware.camera2.CaptureResult}.</p>
*
* <p>If using a REALTIME timestamp base on a device that supports only
@@ -243,7 +247,8 @@
* displayed right away.</p>
*
* <p>Timestamps with this time base cannot directly match the timestamps in
- * {@link CameraCaptureSession.CaptureCallback#onCaptureStarted} or the sensor timestamps in
+ * {@link CameraCaptureSession.CaptureCallback#onCaptureStarted}, {@link
+ * CameraCaptureSession.CaptureCallback#onReadoutStarted}, or the sensor timestamps in
* {@link android.hardware.camera2.CaptureResult}. This timestamp base shouldn't be used if the
* timestamp needs to be used for audio-video synchronization.</p>
*/
@@ -252,18 +257,7 @@
/**
* Timestamp is the start of readout in the same time domain as TIMESTAMP_BASE_SENSOR.
*
- * <p>The start of the camera sensor readout after exposure. For a rolling shutter camera
- * sensor, the timestamp is typically equal to the start of exposure time +
- * exposure time + certain fixed offset. The fixed offset could be due to camera sensor
- * level crop. The benefit of using readout time is that when camera runs in a fixed
- * frame rate, the timestamp intervals between frames are constant.</p>
- *
- * <p>This timestamp is in the same time domain as in TIMESTAMP_BASE_SENSOR, with the exception
- * that one is start of exposure, and the other is start of readout.</p>
- *
- * <p>This timestamp base is supported only if {@link
- * CameraCharacteristics#SENSOR_READOUT_TIMESTAMP} is
- * {@link CameraMetadata#SENSOR_READOUT_TIMESTAMP_HARDWARE}.</p>
+ * <p>NOTE: do not use! Use useReadoutTimestamp instead.</p>
*
* @hide
*/
@@ -295,7 +289,8 @@
CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_STILL_CAPTURE,
CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_RECORD,
CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW_VIDEO_STILL,
- CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_CALL})
+ CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_CALL,
+ CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_CROPPED_RAW})
public @interface StreamUseCase {};
/**
@@ -579,6 +574,8 @@
mStreamUseCase = CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_DEFAULT;
mTimestampBase = TIMESTAMP_BASE_DEFAULT;
mMirrorMode = MIRROR_MODE_AUTO;
+ mUseReadoutTimestamp = false;
+ mIsReadoutSensorTimestampBase = false;
}
/**
@@ -679,6 +676,8 @@
mDynamicRangeProfile = DynamicRangeProfiles.STANDARD;
mColorSpace = ColorSpaceProfiles.UNSPECIFIED;
mStreamUseCase = CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_DEFAULT;
+ mUseReadoutTimestamp = false;
+ mIsReadoutSensorTimestampBase = false;
}
/**
@@ -1000,7 +999,7 @@
*/
public void setStreamUseCase(@StreamUseCase long streamUseCase) {
// Verify that the value is in range
- long maxUseCaseValue = CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_CALL;
+ long maxUseCaseValue = CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_CROPPED_RAW;
if (streamUseCase > maxUseCaseValue &&
streamUseCase < CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_VENDOR_START) {
throw new IllegalArgumentException("Not a valid stream use case value " +
@@ -1048,7 +1047,15 @@
throw new IllegalArgumentException("Not a valid timestamp base value " +
timestampBase);
}
- mTimestampBase = timestampBase;
+
+ if (timestampBase == TIMESTAMP_BASE_READOUT_SENSOR) {
+ mTimestampBase = TIMESTAMP_BASE_SENSOR;
+ mUseReadoutTimestamp = true;
+ mIsReadoutSensorTimestampBase = true;
+ } else {
+ mTimestampBase = timestampBase;
+ mIsReadoutSensorTimestampBase = false;
+ }
}
/**
@@ -1060,7 +1067,11 @@
* @return The currently set timestamp base
*/
public @TimestampBase int getTimestampBase() {
- return mTimestampBase;
+ if (mIsReadoutSensorTimestampBase) {
+ return TIMESTAMP_BASE_READOUT_SENSOR;
+ } else {
+ return mTimestampBase;
+ }
}
/**
@@ -1100,6 +1111,37 @@
}
/**
+ * Use the camera sensor's readout time for the image timestamp.
+ *
+ * <p>The start of the camera sensor readout after exposure. For a rolling shutter camera
+ * sensor, the timestamp is typically equal to {@code (the start of exposure time) +
+ * (exposure time) + (certain fixed offset)}. The fixed offset can vary per session, depending
+ * on the underlying sensor configuration. The benefit of using readout time is that when
+ * camera runs in a fixed frame rate, the timestamp intervals between frames are constant.</p>
+ *
+ * <p>Readout timestamp is supported only if {@link
+ * CameraCharacteristics#SENSOR_READOUT_TIMESTAMP} is
+ * {@link CameraMetadata#SENSOR_READOUT_TIMESTAMP_HARDWARE}.</p>
+ *
+ * <p>As long as readout timestamp is supported, if the timestamp base isi
+ * {@link #TIMESTAMP_BASE_CHOREOGRAPHER_SYNCED}, or if the timestamp base is DEFAULT for a
+ * SurfaceView output, the image timestamps for the output are always readout time regardless
+ * of whether this function is called.</p>
+ *
+ * @param on The output image timestamp is the start of exposure time if false, and
+ * the start of readout time if true.
+ */
+ public void useReadoutTimestamp(boolean on) {
+ mUseReadoutTimestamp = on;
+ }
+
+ /** Whether readout timestamp is used for this OutputConfiguration.
+ */
+ public boolean isReadoutTimestampUsed() {
+ return mUseReadoutTimestamp;
+ }
+
+ /**
* Create a new {@link OutputConfiguration} instance with another {@link OutputConfiguration}
* instance.
*
@@ -1130,6 +1172,7 @@
this.mStreamUseCase = other.mStreamUseCase;
this.mTimestampBase = other.mTimestampBase;
this.mMirrorMode = other.mMirrorMode;
+ this.mUseReadoutTimestamp = other.mUseReadoutTimestamp;
}
/**
@@ -1157,6 +1200,7 @@
int timestampBase = source.readInt();
int mirrorMode = source.readInt();
+ boolean useReadoutTimestamp = source.readInt() == 1;
mSurfaceGroupId = surfaceSetId;
mRotation = rotation;
@@ -1185,6 +1229,7 @@
mStreamUseCase = streamUseCase;
mTimestampBase = timestampBase;
mMirrorMode = mirrorMode;
+ mUseReadoutTimestamp = useReadoutTimestamp;
}
/**
@@ -1305,6 +1350,7 @@
dest.writeLong(mStreamUseCase);
dest.writeInt(mTimestampBase);
dest.writeInt(mMirrorMode);
+ dest.writeInt(mUseReadoutTimestamp ? 1 : 0);
}
/**
@@ -1338,7 +1384,8 @@
mIsMultiResolution != other.mIsMultiResolution ||
mStreamUseCase != other.mStreamUseCase ||
mTimestampBase != other.mTimestampBase ||
- mMirrorMode != other.mMirrorMode)
+ mMirrorMode != other.mMirrorMode ||
+ mUseReadoutTimestamp != other.mUseReadoutTimestamp)
return false;
if (mSensorPixelModesUsed.size() != other.mSensorPixelModesUsed.size()) {
return false;
@@ -1381,7 +1428,7 @@
mPhysicalCameraId == null ? 0 : mPhysicalCameraId.hashCode(),
mIsMultiResolution ? 1 : 0, mSensorPixelModesUsed.hashCode(),
mDynamicRangeProfile, mColorSpace, mStreamUseCase,
- mTimestampBase, mMirrorMode);
+ mTimestampBase, mMirrorMode, mUseReadoutTimestamp ? 1 : 0);
}
return HashCodeHelpers.hashCode(
@@ -1391,7 +1438,7 @@
mPhysicalCameraId == null ? 0 : mPhysicalCameraId.hashCode(),
mIsMultiResolution ? 1 : 0, mSensorPixelModesUsed.hashCode(),
mDynamicRangeProfile, mColorSpace, mStreamUseCase, mTimestampBase,
- mMirrorMode);
+ mMirrorMode, mUseReadoutTimestamp ? 1 : 0);
}
private static final String TAG = "OutputConfiguration";
@@ -1433,4 +1480,8 @@
private int mTimestampBase;
// Mirroring mode
private int mMirrorMode;
+ // Use readout timestamp
+ private boolean mUseReadoutTimestamp;
+ // Whether the timestamp base is set to READOUT_SENSOR
+ private boolean mIsReadoutSensorTimestampBase;
}
diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java
index 23d108f..42803a1 100644
--- a/core/java/android/hardware/display/DisplayManager.java
+++ b/core/java/android/hardware/display/DisplayManager.java
@@ -139,6 +139,7 @@
VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED,
VIRTUAL_DISPLAY_FLAG_TOUCH_FEEDBACK_DISABLED,
VIRTUAL_DISPLAY_FLAG_OWN_FOCUS,
+ VIRTUAL_DISPLAY_FLAG_STEAL_TOP_FOCUS_DISABLED,
})
@Retention(RetentionPolicy.SOURCE)
public @interface VirtualDisplayFlag {}
@@ -429,6 +430,18 @@
public static final int VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP = 1 << 15;
+ /**
+ * Virtual display flags: Indicates that the display should not become the top focused display
+ * by stealing the top focus from another display.
+ *
+ * @see Display#FLAG_STEAL_TOP_FOCUS_DISABLED
+ * @see #createVirtualDisplay
+ * @see #VIRTUAL_DISPLAY_FLAG_OWN_FOCUS
+ * @hide
+ */
+ @SystemApi
+ public static final int VIRTUAL_DISPLAY_FLAG_STEAL_TOP_FOCUS_DISABLED = 1 << 16;
+
/** @hide */
@IntDef(prefix = {"MATCH_CONTENT_FRAMERATE_"}, value = {
MATCH_CONTENT_FRAMERATE_UNKNOWN,
diff --git a/core/java/android/hardware/display/VirtualDisplayConfig.java b/core/java/android/hardware/display/VirtualDisplayConfig.java
index 891ba36..abd647f 100644
--- a/core/java/android/hardware/display/VirtualDisplayConfig.java
+++ b/core/java/android/hardware/display/VirtualDisplayConfig.java
@@ -23,7 +23,6 @@
import android.annotation.Nullable;
import android.hardware.display.DisplayManager.VirtualDisplayFlag;
import android.media.projection.MediaProjection;
-import android.os.Handler;
import android.os.Parcel;
import android.os.Parcelable;
import android.view.Surface;
@@ -35,7 +34,8 @@
/**
* Holds configuration used to create {@link VirtualDisplay} instances. See
- * {@link MediaProjection#createVirtualDisplay(VirtualDisplayConfig, VirtualDisplay.Callback, Handler)}.
+ * {@link MediaProjection#createVirtualDisplay} and
+ * {@link android.companion.virtual.VirtualDeviceManager.VirtualDevice#createVirtualDisplay}.
*
* @hide
*/
diff --git a/core/java/android/hardware/hdmi/HdmiPortInfo.java b/core/java/android/hardware/hdmi/HdmiPortInfo.java
index 625a6a5..5bb1f03 100644
--- a/core/java/android/hardware/hdmi/HdmiPortInfo.java
+++ b/core/java/android/hardware/hdmi/HdmiPortInfo.java
@@ -23,7 +23,7 @@
/**
* A class to encapsulate HDMI port information. Contains the capability of the ports such as
- * HDMI-CEC, MHL, ARC(Audio Return Channel), and physical address assigned to each port.
+ * HDMI-CEC, MHL, ARC(Audio Return Channel), eARC and physical address assigned to each port.
*
* @hide
*/
@@ -40,6 +40,7 @@
private final int mAddress;
private final boolean mCecSupported;
private final boolean mArcSupported;
+ private final boolean mEarcSupported;
private final boolean mMhlSupported;
/**
@@ -53,11 +54,28 @@
* @param arc {@code true} if audio return channel is supported on the port
*/
public HdmiPortInfo(int id, int type, int address, boolean cec, boolean mhl, boolean arc) {
+ this(id, type, address, cec, mhl, arc, false);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param id identifier assigned to each port. 1 for HDMI port 1
+ * @param type HDMI port input/output type
+ * @param address physical address of the port
+ * @param cec {@code true} if HDMI-CEC is supported on the port
+ * @param mhl {@code true} if MHL is supported on the port
+ * @param arc {@code true} if audio return channel is supported on the port
+ * @param earc {@code true} if eARC is supported on the port
+ */
+ public HdmiPortInfo(int id, int type, int address,
+ boolean cec, boolean mhl, boolean arc, boolean earc) {
mId = id;
mType = type;
mAddress = address;
mCecSupported = cec;
mArcSupported = arc;
+ mEarcSupported = earc;
mMhlSupported = mhl;
}
@@ -116,6 +134,15 @@
}
/**
+ * Returns {@code true} if the port supports eARC.
+ *
+ * @return {@code true} if the port supports eARC.
+ */
+ public boolean isEarcSupported() {
+ return mEarcSupported;
+ }
+
+ /**
* Describes the kinds of special objects contained in this Parcelable's
* marshalled representation.
*/
@@ -138,7 +165,8 @@
boolean cec = (source.readInt() == 1);
boolean arc = (source.readInt() == 1);
boolean mhl = (source.readInt() == 1);
- return new HdmiPortInfo(id, type, address, cec, mhl, arc);
+ boolean earc = (source.readInt() == 1);
+ return new HdmiPortInfo(id, type, address, cec, mhl, arc, earc);
}
@Override
@@ -164,6 +192,7 @@
dest.writeInt(mCecSupported ? 1 : 0);
dest.writeInt(mArcSupported ? 1 : 0);
dest.writeInt(mMhlSupported ? 1 : 0);
+ dest.writeInt(mEarcSupported ? 1 : 0);
}
@NonNull
@@ -175,7 +204,8 @@
s.append("address: ").append(String.format("0x%04x", mAddress)).append(", ");
s.append("cec: ").append(mCecSupported).append(", ");
s.append("arc: ").append(mArcSupported).append(", ");
- s.append("mhl: ").append(mMhlSupported);
+ s.append("mhl: ").append(mMhlSupported).append(", ");
+ s.append("earc: ").append(mEarcSupported);
return s.toString();
}
@@ -187,12 +217,12 @@
final HdmiPortInfo other = (HdmiPortInfo) o;
return mId == other.mId && mType == other.mType && mAddress == other.mAddress
&& mCecSupported == other.mCecSupported && mArcSupported == other.mArcSupported
- && mMhlSupported == other.mMhlSupported;
+ && mMhlSupported == other.mMhlSupported && mEarcSupported == other.mEarcSupported;
}
@Override
public int hashCode() {
return java.util.Objects.hash(
- mId, mType, mAddress, mCecSupported, mArcSupported, mMhlSupported);
+ mId, mType, mAddress, mCecSupported, mArcSupported, mMhlSupported, mEarcSupported);
}
}
diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java
index 9c42160..655e598 100644
--- a/core/java/android/hardware/input/InputManager.java
+++ b/core/java/android/hardware/input/InputManager.java
@@ -166,6 +166,14 @@
* The <code>android:keyboardLayout</code> attribute refers to a
* <a href="http://source.android.com/tech/input/key-character-map-files.html">
* key character map</a> resource that defines the keyboard layout.
+ * The <code>android:keyboardLocale</code> attribute specifies a comma separated list of BCP 47
+ * language tags depicting the locales supported by the keyboard layout. This attribute is
+ * optional and will be used for auto layout selection for external physical keyboards.
+ * The <code>android:keyboardLayoutType</code> attribute specifies the layoutType for the
+ * keyboard layout. This can be either empty or one of the following supported layout types:
+ * qwerty, qwertz, azerty, dvorak, colemak, workman, extended, turkish_q, turkish_f. This
+ * attribute is optional and will be used for auto layout selection for external physical
+ * keyboards.
* </p>
*/
@SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
@@ -705,6 +713,30 @@
}
/**
+ * Returns the layout type of the queried layout
+ * <p>
+ * The input manager consults the built-in keyboard layouts as well as all keyboard layouts
+ * advertised by applications using a {@link #ACTION_QUERY_KEYBOARD_LAYOUTS} broadcast receiver.
+ * </p>
+ *
+ * @param layoutDescriptor The layout descriptor of the queried layout
+ * @return layout type of the queried layout
+ *
+ * @hide
+ */
+ @TestApi
+ @NonNull
+ public String getKeyboardLayoutTypeForLayoutDescriptor(@NonNull String layoutDescriptor) {
+ KeyboardLayout[] layouts = getKeyboardLayouts();
+ for (KeyboardLayout kl : layouts) {
+ if (layoutDescriptor.equals(kl.getDescriptor())) {
+ return kl.getLayoutType();
+ }
+ }
+ return "";
+ }
+
+ /**
* Gets information about all supported keyboard layouts appropriate
* for a specific input device.
* <p>
diff --git a/core/java/android/hardware/input/KeyboardLayout.java b/core/java/android/hardware/input/KeyboardLayout.java
index 52c1551..58f7759 100644
--- a/core/java/android/hardware/input/KeyboardLayout.java
+++ b/core/java/android/hardware/input/KeyboardLayout.java
@@ -21,24 +21,74 @@
import android.os.Parcel;
import android.os.Parcelable;
+import java.util.HashMap;
+import java.util.Map;
+
/**
* Describes a keyboard layout.
*
* @hide
*/
-public final class KeyboardLayout implements Parcelable,
- Comparable<KeyboardLayout> {
+public final class KeyboardLayout implements Parcelable, Comparable<KeyboardLayout> {
private final String mDescriptor;
private final String mLabel;
private final String mCollection;
private final int mPriority;
@NonNull
private final LocaleList mLocales;
+ private final LayoutType mLayoutType;
private final int mVendorId;
private final int mProductId;
- public static final @android.annotation.NonNull Parcelable.Creator<KeyboardLayout> CREATOR =
- new Parcelable.Creator<KeyboardLayout>() {
+ /** Currently supported Layout types in the KCM files */
+ private enum LayoutType {
+ UNDEFINED(0, "undefined"),
+ QWERTY(1, "qwerty"),
+ QWERTZ(2, "qwertz"),
+ AZERTY(3, "azerty"),
+ DVORAK(4, "dvorak"),
+ COLEMAK(5, "colemak"),
+ WORKMAN(6, "workman"),
+ TURKISH_F(7, "turkish_f"),
+ TURKISH_Q(8, "turkish_q"),
+ EXTENDED(9, "extended");
+
+ private final int mValue;
+ private final String mName;
+ private static final Map<Integer, LayoutType> VALUE_TO_ENUM_MAP = new HashMap<>();
+ static {
+ VALUE_TO_ENUM_MAP.put(UNDEFINED.mValue, UNDEFINED);
+ VALUE_TO_ENUM_MAP.put(QWERTY.mValue, QWERTY);
+ VALUE_TO_ENUM_MAP.put(QWERTZ.mValue, QWERTZ);
+ VALUE_TO_ENUM_MAP.put(AZERTY.mValue, AZERTY);
+ VALUE_TO_ENUM_MAP.put(DVORAK.mValue, DVORAK);
+ VALUE_TO_ENUM_MAP.put(COLEMAK.mValue, COLEMAK);
+ VALUE_TO_ENUM_MAP.put(WORKMAN.mValue, WORKMAN);
+ VALUE_TO_ENUM_MAP.put(TURKISH_F.mValue, TURKISH_F);
+ VALUE_TO_ENUM_MAP.put(TURKISH_Q.mValue, TURKISH_Q);
+ VALUE_TO_ENUM_MAP.put(EXTENDED.mValue, EXTENDED);
+ }
+
+ private static LayoutType of(int value) {
+ return VALUE_TO_ENUM_MAP.getOrDefault(value, UNDEFINED);
+ }
+
+ LayoutType(int value, String name) {
+ this.mValue = value;
+ this.mName = name;
+ }
+
+ private int getValue() {
+ return mValue;
+ }
+
+ private String getName() {
+ return mName;
+ }
+ }
+
+ @NonNull
+ public static final Parcelable.Creator<KeyboardLayout> CREATOR = new Parcelable.Creator<>() {
public KeyboardLayout createFromParcel(Parcel source) {
return new KeyboardLayout(source);
}
@@ -48,12 +98,13 @@
};
public KeyboardLayout(String descriptor, String label, String collection, int priority,
- LocaleList locales, int vid, int pid) {
+ LocaleList locales, int layoutValue, int vid, int pid) {
mDescriptor = descriptor;
mLabel = label;
mCollection = collection;
mPriority = priority;
mLocales = locales;
+ mLayoutType = LayoutType.of(layoutValue);
mVendorId = vid;
mProductId = pid;
}
@@ -64,6 +115,7 @@
mCollection = source.readString();
mPriority = source.readInt();
mLocales = LocaleList.CREATOR.createFromParcel(source);
+ mLayoutType = LayoutType.of(source.readInt());
mVendorId = source.readInt();
mProductId = source.readInt();
}
@@ -106,6 +158,15 @@
}
/**
+ * Gets the layout type that this keyboard layout is intended for.
+ * This may be "undefined" if a layoutType has not been assigned to this keyboard layout.
+ * @return The keyboard layout's intended layout type.
+ */
+ public String getLayoutType() {
+ return mLayoutType.getName();
+ }
+
+ /**
* Gets the vendor ID of the hardware device this keyboard layout is intended for.
* Returns -1 if this is not specific to any piece of hardware.
* @return The hardware vendor ID of the keyboard layout's intended device.
@@ -135,6 +196,7 @@
dest.writeString(mCollection);
dest.writeInt(mPriority);
mLocales.writeToParcel(dest, 0);
+ dest.writeInt(mLayoutType.getValue());
dest.writeInt(mVendorId);
dest.writeInt(mProductId);
}
@@ -160,6 +222,7 @@
+ ", descriptor: " + mDescriptor
+ ", priority: " + mPriority
+ ", locales: " + mLocales.toString()
+ + ", layout type: " + mLayoutType.getName()
+ ", vendorId: " + mVendorId
+ ", productId: " + mProductId;
}
diff --git a/core/java/android/hardware/input/VirtualNavigationTouchpadConfig.java b/core/java/android/hardware/input/VirtualNavigationTouchpadConfig.java
index f2805bb..5ad5fd9 100644
--- a/core/java/android/hardware/input/VirtualNavigationTouchpadConfig.java
+++ b/core/java/android/hardware/input/VirtualNavigationTouchpadConfig.java
@@ -88,12 +88,17 @@
* 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) {
+ /**
+ * Creates a new instance for the given dimensions of the {@link VirtualNavigationTouchpad}.
+ *
+ * @param touchpadWidth The width of the touchpad.
+ * @param touchpadHeight The height of the touchpad.
+ */
+ public Builder(@IntRange(from = 1) int touchpadWidth,
+ @IntRange(from = 1) int touchpadHeight) {
if (touchpadHeight <= 0 || touchpadWidth <= 0) {
throw new IllegalArgumentException(
"Cannot create a virtual navigation touchpad, touchpad dimensions must be "
diff --git a/core/java/android/hardware/input/VirtualTouchscreenConfig.java b/core/java/android/hardware/input/VirtualTouchscreenConfig.java
index e358619..aac341cc 100644
--- a/core/java/android/hardware/input/VirtualTouchscreenConfig.java
+++ b/core/java/android/hardware/input/VirtualTouchscreenConfig.java
@@ -16,6 +16,7 @@
package android.hardware.input;
+import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.os.Parcel;
@@ -29,31 +30,31 @@
@SystemApi
public final class VirtualTouchscreenConfig extends VirtualInputDeviceConfig implements Parcelable {
- /** The touchscreen width in pixels. */
- private final int mWidthInPixels;
- /** The touchscreen height in pixels. */
- private final int mHeightInPixels;
+ /** The touchscreen width. */
+ private final int mWidth;
+ /** The touchscreen height. */
+ private final int mHeight;
private VirtualTouchscreenConfig(@NonNull Builder builder) {
super(builder);
- mWidthInPixels = builder.mWidthInPixels;
- mHeightInPixels = builder.mHeightInPixels;
+ mWidth = builder.mWidth;
+ mHeight = builder.mHeight;
}
private VirtualTouchscreenConfig(@NonNull Parcel in) {
super(in);
- mWidthInPixels = in.readInt();
- mHeightInPixels = in.readInt();
+ mWidth = in.readInt();
+ mHeight = in.readInt();
}
- /** Returns the touchscreen width in pixels. */
- public int getWidthInPixels() {
- return mWidthInPixels;
+ /** Returns the touchscreen width. */
+ public int getWidth() {
+ return mWidth;
}
- /** Returns the touchscreen height in pixels. */
- public int getHeightInPixels() {
- return mHeightInPixels;
+ /** Returns the touchscreen height. */
+ public int getHeight() {
+ return mHeight;
}
@Override
@@ -64,8 +65,8 @@
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
super.writeToParcel(dest, flags);
- dest.writeInt(mWidthInPixels);
- dest.writeInt(mHeightInPixels);
+ dest.writeInt(mWidth);
+ dest.writeInt(mHeight);
}
@NonNull
@@ -86,25 +87,29 @@
* Builder for creating a {@link VirtualTouchscreenConfig}.
*/
public static final class Builder extends VirtualInputDeviceConfig.Builder<Builder> {
- private int mWidthInPixels;
- private int mHeightInPixels;
+ private int mWidth;
+ private int mHeight;
/**
- * @see VirtualTouchscreenConfig#getWidthInPixels().
+ * Creates a new instance for the given dimensions of the {@link VirtualTouchscreen}.
+ *
+ * <p>The dimensions are not pixels but in the touchscreens raw coordinate space. They do
+ * not necessarily have to correspond to the display size or aspect ratio. In this case the
+ * framework will handle the scaling appropriately.
+ *
+ * @param touchscreenWidth The width of the touchscreen.
+ * @param touchscreenHeight The height of the touchscreen.
*/
- @NonNull
- public Builder setWidthInPixels(int widthInPixels) {
- mWidthInPixels = widthInPixels;
- return this;
- }
-
- /**
- * @see VirtualTouchscreenConfig#getHeightInPixels().
- */
- @NonNull
- public Builder setHeightInPixels(int heightInPixels) {
- mHeightInPixels = heightInPixels;
- return this;
+ public Builder(@IntRange(from = 1) int touchscreenWidth,
+ @IntRange(from = 1) int touchscreenHeight) {
+ if (touchscreenHeight <= 0 || touchscreenWidth <= 0) {
+ throw new IllegalArgumentException(
+ "Cannot create a virtual touchscreen, touchscreen dimensions must be "
+ + "positive. Got: (" + touchscreenHeight + ", "
+ + touchscreenWidth + ")");
+ }
+ mHeight = touchscreenHeight;
+ mWidth = touchscreenWidth;
}
/**
diff --git a/core/java/android/hardware/radio/ProgramSelector.java b/core/java/android/hardware/radio/ProgramSelector.java
index 7faa285..727716e 100644
--- a/core/java/android/hardware/radio/ProgramSelector.java
+++ b/core/java/android/hardware/radio/ProgramSelector.java
@@ -65,12 +65,12 @@
*/
@Deprecated
public static final int PROGRAM_TYPE_INVALID = 0;
- /** Analogue AM radio (with or without RDS).
+ /** Analog AM radio (with or without RDS).
* @deprecated use {@link ProgramIdentifier} instead
*/
@Deprecated
public static final int PROGRAM_TYPE_AM = 1;
- /** analogue FM radio (with or without RDS).
+ /** analog FM radio (with or without RDS).
* @deprecated use {@link ProgramIdentifier} instead
*/
@Deprecated
@@ -125,25 +125,50 @@
public @interface ProgramType {}
public static final int IDENTIFIER_TYPE_INVALID = 0;
- /** kHz */
+ /**
+ * Primary identifier for analog (without RDS) AM/FM stations:
+ * frequency in kHz.
+ *
+ * <p>This identifier also contains band information:
+ * <li>
+ * <ul><500kHz: AM LW.
+ * <ul>500kHz - 1705kHz: AM MW.
+ * <ul>1.71MHz - 30MHz: AM SW.
+ * <ul>>60MHz: FM.
+ * </li>
+ */
public static final int IDENTIFIER_TYPE_AMFM_FREQUENCY = 1;
- /** 16bit */
+ /**
+ * 16bit primary identifier for FM RDS station.
+ */
public static final int IDENTIFIER_TYPE_RDS_PI = 2;
/**
* 64bit compound primary identifier for HD Radio.
*
- * Consists of (from the LSB):
- * - 32bit: Station ID number;
- * - 4bit: HD_SUBCHANNEL;
- * - 18bit: AMFM_FREQUENCY.
- * The remaining bits should be set to zeros when writing on the chip side
+ * <p>Consists of (from the LSB):
+ * <li>
+ * <ul>132bit: Station ID number.
+ * <ul>14bit: HD_SUBCHANNEL.
+ * <ul>18bit: AMFM_FREQUENCY.
+ * </li>
+ *
+ * <p>While station ID number should be unique globally, it sometimes gets
+ * abused by broadcasters (i.e. not being set at all). To ensure local
+ * uniqueness, AMFM_FREQUENCY_KHZ was added here. Global uniqueness is
+ * a best-effort - see {@link IDENTIFIER_TYPE_HD_STATION_NAME}.
+ *
+ * <p>HD Radio subchannel is a value in range of 0-7.
+ * This index is 0-based (where 0 is MPS and 1..7 are SPS),
+ * as opposed to HD Radio standard (where it's 1-based).
+ *
+ * <p>The remaining bits should be set to zeros when writing on the chip side
* and ignored when read.
*/
public static final int IDENTIFIER_TYPE_HD_STATION_ID_EXT = 3;
/**
- * HD Radio subchannel - a value of range 0-7.
+ * HD Radio subchannel - a value in range of 0-7.
*
- * The subchannel index is 0-based (where 0 is MPS and 1..7 are SPS),
+ * <p>The subchannel index is 0-based (where 0 is MPS and 1..7 are SPS),
* as opposed to HD Radio standard (where it's 1-based).
*
* @deprecated use IDENTIFIER_TYPE_HD_STATION_ID_EXT instead
@@ -153,16 +178,16 @@
/**
* 64bit additional identifier for HD Radio.
*
- * Due to Station ID abuse, some HD_STATION_ID_EXT identifiers may be not
+ * <p>Due to Station ID abuse, some HD_STATION_ID_EXT identifiers may be not
* globally unique. To provide a best-effort solution, a short version of
* station name may be carried as additional identifier and may be used
* by the tuner hardware to double-check tuning.
*
- * The name is limited to the first 8 A-Z0-9 characters (lowercase letters
- * must be converted to uppercase). Encoded in little-endian ASCII:
- * the first character of the name is the LSB.
+ * <p>The name is limited to the first 8 A-Z0-9 characters (lowercase
+ * letters must be converted to uppercase). Encoded in little-endian
+ * ASCII: the first character of the name is the LSB.
*
- * For example: "Abc" is encoded as 0x434241.
+ * <p>For example: "Abc" is encoded as 0x434241.
*/
public static final int IDENTIFIER_TYPE_HD_STATION_NAME = 10004;
/**
@@ -175,17 +200,19 @@
/**
* 28bit compound primary identifier for Digital Audio Broadcasting.
*
- * Consists of (from the LSB):
- * - 16bit: SId;
- * - 8bit: ECC code;
- * - 4bit: SCIdS.
+ * <p>Consists of (from the LSB):
+ * <li>
+ * <ul>16bit: SId.
+ * <ul>8bit: ECC code.
+ * <ul>4bit: SCIdS.
+ * </li>
*
- * SCIdS (Service Component Identifier within the Service) value
+ * <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.
+ * <p>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
*/
@@ -197,7 +224,9 @@
public static final int IDENTIFIER_TYPE_DAB_SCID = 7;
/** kHz */
public static final int IDENTIFIER_TYPE_DAB_FREQUENCY = 8;
- /** 24bit */
+ /**
+ * 24bit primary identifier for Digital Radio Mondiale.
+ */
public static final int IDENTIFIER_TYPE_DRMO_SERVICE_ID = 9;
/** kHz */
public static final int IDENTIFIER_TYPE_DRMO_FREQUENCY = 10;
@@ -207,7 +236,9 @@
*/
@Deprecated
public static final int IDENTIFIER_TYPE_DRMO_MODULATION = 11;
- /** 32bit */
+ /**
+ * 32bit primary identifier for SiriusXM Satellite Radio.
+ */
public static final int IDENTIFIER_TYPE_SXM_SERVICE_ID = 12;
/** 0-999 range */
public static final int IDENTIFIER_TYPE_SXM_CHANNEL = 13;
@@ -224,15 +255,15 @@
* 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.
+ * <p>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.
*
- * It must not be used in any other programType than corresponding VENDOR
+ * <p>It must not be used in any other programType than corresponding VENDOR
* type between VENDOR_START and VENDOR_END (eg. identifier type 1015 must
* not be used in any program type other than 1015).
*/
diff --git a/core/java/android/hardware/usb/UsbPort.java b/core/java/android/hardware/usb/UsbPort.java
index e0f9cad..cdd67b7 100644
--- a/core/java/android/hardware/usb/UsbPort.java
+++ b/core/java/android/hardware/usb/UsbPort.java
@@ -46,6 +46,8 @@
import static android.hardware.usb.UsbPortStatus.DATA_STATUS_DISABLED_DOCK;
import static android.hardware.usb.UsbPortStatus.DATA_STATUS_DISABLED_FORCE;
import static android.hardware.usb.UsbPortStatus.DATA_STATUS_DISABLED_DEBUG;
+import static android.hardware.usb.UsbPortStatus.DATA_STATUS_DISABLED_DOCK_HOST_MODE;
+import static android.hardware.usb.UsbPortStatus.DATA_STATUS_DISABLED_DOCK_DEVICE_MODE;
import static android.hardware.usb.UsbPortStatus.COMPLIANCE_WARNING_DEBUG_ACCESSORY;
import static android.hardware.usb.UsbPortStatus.COMPLIANCE_WARNING_BC_1_2;
import static android.hardware.usb.UsbPortStatus.COMPLIANCE_WARNING_MISSING_RP;
@@ -676,6 +678,15 @@
statusString.append("disabled-debug, ");
}
+ if ((usbDataStatus & DATA_STATUS_DISABLED_DOCK_HOST_MODE) ==
+ DATA_STATUS_DISABLED_DOCK_HOST_MODE) {
+ statusString.append("disabled-host-dock, ");
+ }
+
+ if ((usbDataStatus & DATA_STATUS_DISABLED_DOCK_DEVICE_MODE) ==
+ DATA_STATUS_DISABLED_DOCK_DEVICE_MODE) {
+ statusString.append("disabled-device-dock, ");
+ }
return statusString.toString().replaceAll(", $", "");
}
diff --git a/core/java/android/hardware/usb/UsbPortStatus.java b/core/java/android/hardware/usb/UsbPortStatus.java
index ed3e40d..e61703d 100644
--- a/core/java/android/hardware/usb/UsbPortStatus.java
+++ b/core/java/android/hardware/usb/UsbPortStatus.java
@@ -219,7 +219,11 @@
public static final int DATA_STATUS_DISABLED_CONTAMINANT = 1 << 2;
/**
- * USB data is disabled due to docking event.
+ * This flag indicates that some or all data modes are disabled
+ * due to docking event, and the specific sub-statuses viz.,
+ * {@link #DATA_STATUS_DISABLED_DOCK_HOST_MODE},
+ * {@link #DATA_STATUS_DISABLED_DOCK_DEVICE_MODE}
+ * can be checked for individual modes.
*/
public static final int DATA_STATUS_DISABLED_DOCK = 1 << 3;
@@ -235,6 +239,18 @@
public static final int DATA_STATUS_DISABLED_DEBUG = 1 << 5;
/**
+ * USB host mode is disabled due to docking event.
+ * {@link #DATA_STATUS_DISABLED_DOCK} will be set as well.
+ */
+ public static final int DATA_STATUS_DISABLED_DOCK_HOST_MODE = 1 << 6;
+
+ /**
+ * USB device mode is disabled due to docking event.
+ * {@link #DATA_STATUS_DISABLED_DOCK} will be set as well.
+ */
+ public static final int DATA_STATUS_DISABLED_DOCK_DEVICE_MODE = 1 << 7;
+
+ /**
* Unknown whether a power brick is connected.
*/
public static final int POWER_BRICK_STATUS_UNKNOWN = 0;
@@ -329,6 +345,8 @@
DATA_STATUS_DISABLED_OVERHEAT,
DATA_STATUS_DISABLED_CONTAMINANT,
DATA_STATUS_DISABLED_DOCK,
+ DATA_STATUS_DISABLED_DOCK_HOST_MODE,
+ DATA_STATUS_DISABLED_DOCK_DEVICE_MODE,
DATA_STATUS_DISABLED_FORCE,
DATA_STATUS_DISABLED_DEBUG
})
@@ -357,6 +375,20 @@
mSupportedRoleCombinations = supportedRoleCombinations;
mContaminantProtectionStatus = contaminantProtectionStatus;
mContaminantDetectionStatus = contaminantDetectionStatus;
+
+ // Older implementations that only set the DISABLED_DOCK_MODE will have the other two
+ // set at the HAL interface level, so the "dock mode only" state shouldn't be visible here.
+ // But the semantics are ensured here.
+ int disabledDockModes = (usbDataStatus &
+ (DATA_STATUS_DISABLED_DOCK_HOST_MODE | DATA_STATUS_DISABLED_DOCK_DEVICE_MODE));
+ if (disabledDockModes != 0) {
+ // Set DATA_STATUS_DISABLED_DOCK when one of DATA_STATUS_DISABLED_DOCK_*_MODE is set
+ usbDataStatus |= DATA_STATUS_DISABLED_DOCK;
+ } else {
+ // Clear DATA_STATUS_DISABLED_DOCK when none of DATA_STATUS_DISABLED_DOCK_*_MODE is set
+ usbDataStatus &= ~DATA_STATUS_DISABLED_DOCK;
+ }
+
mUsbDataStatus = usbDataStatus;
mPowerTransferLimited = powerTransferLimited;
mPowerBrickConnectionStatus = powerBrickConnectionStatus;
@@ -472,7 +504,8 @@
* {@link #DATA_STATUS_UNKNOWN}, {@link #DATA_STATUS_ENABLED},
* {@link #DATA_STATUS_DISABLED_OVERHEAT}, {@link #DATA_STATUS_DISABLED_CONTAMINANT},
* {@link #DATA_STATUS_DISABLED_DOCK}, {@link #DATA_STATUS_DISABLED_FORCE},
- * {@link #DATA_STATUS_DISABLED_DEBUG}
+ * {@link #DATA_STATUS_DISABLED_DEBUG}, {@link #DATA_STATUS_DISABLED_DOCK_HOST_MODE},
+ * {@link #DATA_STATUS_DISABLED_DOCK_DEVICE_MODE}
*/
public @UsbDataStatus int getUsbDataStatus() {
return mUsbDataStatus;
diff --git a/core/java/android/os/Debug.java b/core/java/android/os/Debug.java
index a31c2e6..ada5c55 100644
--- a/core/java/android/os/Debug.java
+++ b/core/java/android/os/Debug.java
@@ -982,6 +982,43 @@
/**
+ * Wait until a debugger attaches. As soon as a debugger attaches,
+ * suspend all Java threads and send VM_START (a.k.a VM_INIT)
+ * packet.
+ *
+ * @hide
+ */
+ public static void suspendAllAndSendVmStart() {
+ if (!VMDebug.isDebuggingEnabled()) {
+ return;
+ }
+
+ // if DDMS is listening, inform them of our plight
+ System.out.println("Sending WAIT chunk");
+ byte[] data = new byte[] { 0 }; // 0 == "waiting for debugger"
+ Chunk waitChunk = new Chunk(ChunkHandler.type("WAIT"), data, 0, 1);
+ DdmServer.sendChunk(waitChunk);
+
+ // We must wait until a debugger is connected (debug socket is
+ // open and at least one non-DDM JDWP packedt has been received.
+ // This guarantees that oj-libjdwp has been attached and that
+ // ART's default implementation of suspendAllAndSendVmStart has
+ // been replaced with an implementation that will suspendAll and
+ // send VM_START.
+ System.out.println("Waiting for debugger first packet");
+ while (!isDebuggerConnected()) {
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException ie) {
+ }
+ }
+
+ System.out.println("Debug.suspendAllAndSentVmStart");
+ VMDebug.suspendAllAndSendVmStart();
+ System.out.println("Debug.suspendAllAndSendVmStart, resumed");
+ }
+
+ /**
* Wait until a debugger attaches. As soon as the debugger attaches,
* this returns, so you will need to place a breakpoint after the
* waitForDebugger() call if you want to start tracing immediately.
diff --git a/core/java/android/os/IUserManager.aidl b/core/java/android/os/IUserManager.aidl
index 1490c6a..3b4e8cd 100644
--- a/core/java/android/os/IUserManager.aidl
+++ b/core/java/android/os/IUserManager.aidl
@@ -51,6 +51,7 @@
String[] getPreInstallableSystemPackages(in String userType);
void setUserEnabled(int userId);
void setUserAdmin(int userId);
+ void revokeUserAdmin(int userId);
void evictCredentialEncryptionKey(int userId);
boolean removeUser(int userId);
boolean removeUserEvenWhenDisallowed(int userId);
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index 08d15c7..9915234 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -2912,14 +2912,15 @@
/**
* @hide
*/
- public static boolean isUsersOnSecondaryDisplaysEnabled() {
- return SystemProperties.getBoolean("fw.users_on_secondary_displays",
+ public static boolean isVisibleBackgroundUsersEnabled() {
+ return SystemProperties.getBoolean("fw.visible_bg_users",
Resources.getSystem()
- .getBoolean(R.bool.config_multiuserUsersOnSecondaryDisplays));
+ .getBoolean(R.bool.config_multiuserVisibleBackgroundUsers));
}
/**
- * Returns whether the device allows users to run (and launch activities) on secondary displays.
+ * Returns whether the device allows (full) users to be started in background visible in a given
+ * display (which would allow them to launch activities in that display).
*
* @return {@code false} for most devices, except on automotive builds for vehiches with
* passenger displays.
@@ -2927,8 +2928,8 @@
* @hide
*/
@TestApi
- public boolean isUsersOnSecondaryDisplaysSupported() {
- return isUsersOnSecondaryDisplaysEnabled();
+ public boolean isVisibleBackgroundUsersSupported() {
+ return isVisibleBackgroundUsersEnabled();
}
/**
@@ -4102,6 +4103,26 @@
}
/**
+ * Revokes admin privileges from the user, if such a user exists.
+ *
+ * <p>Note that this does not alter the user's pre-existing user restrictions.
+ *
+ * @param userId the id of the user to revoke admin rights from
+ * @hide
+ */
+ @RequiresPermission(allOf = {
+ Manifest.permission.INTERACT_ACROSS_USERS_FULL,
+ Manifest.permission.MANAGE_USERS
+ })
+ public void revokeUserAdmin(@UserIdInt int userId) {
+ try {
+ mService.revokeUserAdmin(userId);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Evicts the user's credential encryption key from memory by stopping and restarting the user.
*
* @hide
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index efe8238..b236d66 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -224,6 +224,7 @@
* @hide
*/
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ @SystemApi
public static final String ACTION_USER_SETTINGS =
"android.settings.USER_SETTINGS";
diff --git a/core/java/android/security/net/config/SystemCertificateSource.java b/core/java/android/security/net/config/SystemCertificateSource.java
index cfb195b..4892312 100644
--- a/core/java/android/security/net/config/SystemCertificateSource.java
+++ b/core/java/android/security/net/config/SystemCertificateSource.java
@@ -18,6 +18,7 @@
import android.os.Environment;
import android.os.UserHandle;
+
import java.io.File;
/**
@@ -32,11 +33,20 @@
private final File mUserRemovedCaDir;
private SystemCertificateSource() {
- super(new File(System.getenv("ANDROID_ROOT") + "/etc/security/cacerts"));
+ super(getDirectory());
File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());
mUserRemovedCaDir = new File(configDir, "cacerts-removed");
}
+ private static File getDirectory() {
+ // TODO(miguelaranda): figure out correct code path.
+ File updatable_dir = new File("/apex/com.android.conscrypt/cacerts");
+ if (updatable_dir.exists()) {
+ return updatable_dir;
+ }
+ return new File(System.getenv("ANDROID_ROOT") + "/etc/security/cacerts");
+ }
+
public static SystemCertificateSource getInstance() {
return NoPreloadHolder.INSTANCE;
}
diff --git a/core/java/android/service/chooser/ChooserAction.java b/core/java/android/service/chooser/ChooserAction.java
new file mode 100644
index 0000000..3010049
--- /dev/null
+++ b/core/java/android/service/chooser/ChooserAction.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.chooser;
+
+import android.annotation.NonNull;
+import android.app.PendingIntent;
+import android.graphics.drawable.Icon;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import java.util.Objects;
+
+/**
+ * A ChooserAction is an app-defined action that can be provided to the Android Sharesheet to
+ * be shown to the user when {@link android.content.Intent.ACTION_CHOOSER} is invoked.
+ *
+ * @see android.content.Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS
+ * @see android.content.Intent.EXTRA_CHOOSER_PAYLOAD_RESELECTION_ACTION
+ * @hide
+ */
+public final class ChooserAction implements Parcelable {
+ private final Icon mIcon;
+ private final CharSequence mLabel;
+ private final PendingIntent mAction;
+
+ private ChooserAction(
+ Icon icon,
+ CharSequence label,
+ PendingIntent action) {
+ mIcon = icon;
+ mLabel = label;
+ mAction = action;
+ }
+
+ /**
+ * Return a user-readable label for this action.
+ */
+ @NonNull
+ public CharSequence getLabel() {
+ return mLabel;
+ }
+
+ /**
+ * Return an {@link Icon} representing this action.
+ */
+ @NonNull
+ public Icon getIcon() {
+ return mIcon;
+ }
+
+ /**
+ * Return the action intent.
+ */
+ @NonNull
+ public PendingIntent getAction() {
+ return mAction;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ mIcon.writeToParcel(dest, flags);
+ TextUtils.writeToParcel(mLabel, dest, flags);
+ mAction.writeToParcel(dest, flags);
+ }
+
+ @Override
+ public String toString() {
+ return "ChooserAction {" + "label=" + mLabel + ", intent=" + mAction + "}";
+ }
+
+ public static final Parcelable.Creator<ChooserAction> CREATOR =
+ new Creator<ChooserAction>() {
+ @Override
+ public ChooserAction createFromParcel(Parcel source) {
+ return new ChooserAction(
+ Icon.CREATOR.createFromParcel(source),
+ TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source),
+ PendingIntent.CREATOR.createFromParcel(source));
+ }
+
+ @Override
+ public ChooserAction[] newArray(int size) {
+ return new ChooserAction[size];
+ }
+ };
+
+ /**
+ * Builder class for {@link ChooserAction} objects
+ */
+ public static final class Builder {
+ private final Icon mIcon;
+ private final CharSequence mLabel;
+ private final PendingIntent mAction;
+
+ /**
+ * Construct a new builder for {@link ChooserAction} object.
+ *
+ * @param icon an {@link Icon} representing this action, consisting of a white foreground
+ * atop a transparent background.
+ * @param label label the user-readable label for this action.
+ * @param action {@link PendingIntent} to be invoked when the action is selected.
+ */
+ public Builder(
+ @NonNull Icon icon,
+ @NonNull CharSequence label,
+ @NonNull PendingIntent action) {
+ Objects.requireNonNull(icon, "icon can not be null");
+ Objects.requireNonNull(label, "label can not be null");
+ Objects.requireNonNull(action, "pending intent can not be null");
+ mIcon = icon;
+ mLabel = label;
+ mAction = action;
+ }
+
+ /**
+ * Combine all of the options that have been set and return a new {@link ChooserAction}
+ * object.
+ * @return the built action
+ */
+ public ChooserAction build() {
+ return new ChooserAction(mIcon, mLabel, mAction);
+ }
+ }
+}
diff --git a/core/java/android/service/chooser/OWNERS b/core/java/android/service/chooser/OWNERS
index a5acba5..dcd4a7b 100644
--- a/core/java/android/service/chooser/OWNERS
+++ b/core/java/android/service/chooser/OWNERS
@@ -1,4 +1,3 @@
-asc@google.com
-mpietal@google.com
-dsandler@android.com
-dsandler@google.com
+mrcasey@google.com
+ayepin@google.com
+joshtrask@google.com
diff --git a/core/java/android/service/controls/ControlsProviderService.java b/core/java/android/service/controls/ControlsProviderService.java
index 950c8ac..b85cf6d 100644
--- a/core/java/android/service/controls/ControlsProviderService.java
+++ b/core/java/android/service/controls/ControlsProviderService.java
@@ -58,27 +58,28 @@
* Manifest metadata to show a custom embedded activity as part of device controls.
*
* The value of this metadata must be the {@link ComponentName} as a string of an activity in
- * the same package that will be launched as part of a TaskView.
+ * the same package that will be launched embedded in the device controls space.
*
* The activity must be exported, enabled and protected by
- * {@link Manifest.permission.BIND_CONTROLS}.
+ * {@link Manifest.permission#BIND_CONTROLS}.
*
- * @hide
+ * It is recommended that the activity is declared {@code android:resizeableActivity="true"}.
*/
public static final String META_DATA_PANEL_ACTIVITY =
"android.service.controls.META_DATA_PANEL_ACTIVITY";
/**
- * Boolean extra containing the value of
- * {@link android.provider.Settings.Secure#LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS}.
+ * Boolean extra containing the value of the setting allowing actions on a locked device.
+ *
+ * This corresponds to the setting that indicates whether the user has
+ * consented to allow actions on devices that declare {@link Control#isAuthRequired()} as
+ * {@code false} when the device is locked.
*
* This is passed with the intent when the panel specified by {@link #META_DATA_PANEL_ACTIVITY}
* is launched.
- *
- * @hide
*/
public static final String EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS =
- "android.service.controls.extra.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS";
+ "android.service.controls.extra.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS";
/**
* @hide
diff --git a/core/java/android/service/controls/OWNERS b/core/java/android/service/controls/OWNERS
new file mode 100644
index 0000000..4bb78c7
--- /dev/null
+++ b/core/java/android/service/controls/OWNERS
@@ -0,0 +1,4 @@
+# Bug component: 802726
+asc@google.com
+kozynski@google.com
+juliacr@google.com
\ No newline at end of file
diff --git a/core/java/android/service/notification/Adjustment.java b/core/java/android/service/notification/Adjustment.java
index 4b25c88..df185ee 100644
--- a/core/java/android/service/notification/Adjustment.java
+++ b/core/java/android/service/notification/Adjustment.java
@@ -52,7 +52,7 @@
/** @hide */
@StringDef (prefix = { "KEY_" }, value = {
KEY_CONTEXTUAL_ACTIONS, KEY_GROUP_KEY, KEY_IMPORTANCE, KEY_PEOPLE, KEY_SNOOZE_CRITERIA,
- KEY_TEXT_REPLIES, KEY_USER_SENTIMENT
+ KEY_TEXT_REPLIES, KEY_USER_SENTIMENT, KEY_IMPORTANCE_PROPOSAL
})
@Retention(RetentionPolicy.SOURCE)
public @interface Keys {}
@@ -122,6 +122,18 @@
public static final String KEY_IMPORTANCE = "key_importance";
/**
+ * Weaker than {@link #KEY_IMPORTANCE}, this adjustment suggests an importance rather than
+ * mandates an importance change.
+ *
+ * A notification listener can interpet this suggestion to show the user a prompt to change
+ * notification importance for the notification (or type, or app) moving forward.
+ *
+ * Data type: int, one of importance values e.g.
+ * {@link android.app.NotificationManager#IMPORTANCE_MIN}.
+ */
+ public static final String KEY_IMPORTANCE_PROPOSAL = "key_importance_proposal";
+
+ /**
* Data type: float, a ranking score from 0 (lowest) to 1 (highest).
* Used to rank notifications inside that fall under the same classification (i.e. alerting,
* silenced).
diff --git a/core/java/android/service/notification/NotificationListenerService.java b/core/java/android/service/notification/NotificationListenerService.java
index d113a3c..11e51ad 100644
--- a/core/java/android/service/notification/NotificationListenerService.java
+++ b/core/java/android/service/notification/NotificationListenerService.java
@@ -1730,6 +1730,8 @@
private ShortcutInfo mShortcutInfo;
private @RankingAdjustment int mRankingAdjustment;
private boolean mIsBubble;
+ // Notification assistant importance suggestion
+ private int mProposedImportance;
private static final int PARCEL_VERSION = 2;
@@ -1767,6 +1769,7 @@
out.writeParcelable(mShortcutInfo, flags);
out.writeInt(mRankingAdjustment);
out.writeBoolean(mIsBubble);
+ out.writeInt(mProposedImportance);
}
/** @hide */
@@ -1805,6 +1808,7 @@
mShortcutInfo = in.readParcelable(cl, android.content.pm.ShortcutInfo.class);
mRankingAdjustment = in.readInt();
mIsBubble = in.readBoolean();
+ mProposedImportance = in.readInt();
}
@@ -1897,6 +1901,23 @@
}
/**
+ * Returns the proposed importance provided by the {@link NotificationAssistantService}.
+ *
+ * This can be used to suggest that the user change the importance of this type of
+ * notification moving forward. A value of
+ * {@link NotificationManager#IMPORTANCE_UNSPECIFIED} means that the NAS has not recommended
+ * a change to the importance, and no UI should be shown to the user. See
+ * {@link Adjustment#KEY_IMPORTANCE_PROPOSAL}.
+ *
+ * @return the importance of the notification
+ * @hide
+ */
+ @SystemApi
+ public @NotificationManager.Importance int getProposedImportance() {
+ return mProposedImportance;
+ }
+
+ /**
* If the system has overridden the group key, then this will be non-null, and this
* key should be used to bundle notifications.
*/
@@ -2060,7 +2081,7 @@
boolean noisy, ArrayList<Notification.Action> smartActions,
ArrayList<CharSequence> smartReplies, boolean canBubble,
boolean isTextChanged, boolean isConversation, ShortcutInfo shortcutInfo,
- int rankingAdjustment, boolean isBubble) {
+ int rankingAdjustment, boolean isBubble, int proposedImportance) {
mKey = key;
mRank = rank;
mIsAmbient = importance < NotificationManager.IMPORTANCE_LOW;
@@ -2086,6 +2107,7 @@
mShortcutInfo = shortcutInfo;
mRankingAdjustment = rankingAdjustment;
mIsBubble = isBubble;
+ mProposedImportance = proposedImportance;
}
/**
@@ -2126,7 +2148,8 @@
other.mIsConversation,
other.mShortcutInfo,
other.mRankingAdjustment,
- other.mIsBubble);
+ other.mIsBubble,
+ other.mProposedImportance);
}
/**
@@ -2185,7 +2208,8 @@
&& Objects.equals((mShortcutInfo == null ? 0 : mShortcutInfo.getId()),
(other.mShortcutInfo == null ? 0 : other.mShortcutInfo.getId()))
&& Objects.equals(mRankingAdjustment, other.mRankingAdjustment)
- && Objects.equals(mIsBubble, other.mIsBubble);
+ && Objects.equals(mIsBubble, other.mIsBubble)
+ && Objects.equals(mProposedImportance, other.mProposedImportance);
}
}
diff --git a/core/java/android/service/notification/ZenPolicy.java b/core/java/android/service/notification/ZenPolicy.java
index a892570..bffa660 100644
--- a/core/java/android/service/notification/ZenPolicy.java
+++ b/core/java/android/service/notification/ZenPolicy.java
@@ -79,6 +79,12 @@
/** @hide */
public static final int PRIORITY_CATEGORY_CONVERSATIONS = 8;
+ /**
+ * Total number of priority categories. Keep updated with any updates to PriorityCategory enum.
+ * @hide
+ */
+ public static final int NUM_PRIORITY_CATEGORIES = 9;
+
/** @hide */
@IntDef(prefix = { "VISUAL_EFFECT_" }, value = {
VISUAL_EFFECT_FULL_SCREEN_INTENT,
@@ -107,6 +113,12 @@
/** @hide */
public static final int VISUAL_EFFECT_NOTIFICATION_LIST = 6;
+ /**
+ * Total number of visual effects. Keep updated with any updates to VisualEffect enum.
+ * @hide
+ */
+ public static final int NUM_VISUAL_EFFECTS = 7;
+
/** @hide */
@IntDef(prefix = { "PEOPLE_TYPE_" }, value = {
PEOPLE_TYPE_UNSET,
@@ -202,8 +214,8 @@
/** @hide */
public ZenPolicy() {
- mPriorityCategories = new ArrayList<>(Collections.nCopies(9, 0));
- mVisualEffects = new ArrayList<>(Collections.nCopies(7, 0));
+ mPriorityCategories = new ArrayList<>(Collections.nCopies(NUM_PRIORITY_CATEGORIES, 0));
+ mVisualEffects = new ArrayList<>(Collections.nCopies(NUM_VISUAL_EFFECTS, 0));
}
/**
@@ -804,8 +816,12 @@
@Override
public ZenPolicy createFromParcel(Parcel source) {
ZenPolicy policy = new ZenPolicy();
- policy.mPriorityCategories = source.readArrayList(Integer.class.getClassLoader(), java.lang.Integer.class);
- policy.mVisualEffects = source.readArrayList(Integer.class.getClassLoader(), java.lang.Integer.class);
+ policy.mPriorityCategories = trimList(
+ source.readArrayList(Integer.class.getClassLoader(), java.lang.Integer.class),
+ NUM_PRIORITY_CATEGORIES);
+ policy.mVisualEffects = trimList(
+ source.readArrayList(Integer.class.getClassLoader(), java.lang.Integer.class),
+ NUM_VISUAL_EFFECTS);
policy.mPriorityCalls = source.readInt();
policy.mPriorityMessages = source.readInt();
policy.mConversationSenders = source.readInt();
@@ -832,6 +848,15 @@
.toString();
}
+ // Returns a list containing the first maxLength elements of the input list if the list is
+ // longer than that size. For the lists in ZenPolicy, this should not happen unless the input
+ // is corrupt.
+ private static ArrayList<Integer> trimList(ArrayList<Integer> list, int maxLength) {
+ if (list == null || list.size() <= maxLength) {
+ return list;
+ }
+ return new ArrayList<>(list.subList(0, maxLength));
+ }
private String priorityCategoriesToString() {
StringBuilder builder = new StringBuilder();
diff --git a/core/java/android/service/wearable/IWearableSensingService.aidl b/core/java/android/service/wearable/IWearableSensingService.aidl
index ba71174..44a13c4 100644
--- a/core/java/android/service/wearable/IWearableSensingService.aidl
+++ b/core/java/android/service/wearable/IWearableSensingService.aidl
@@ -16,6 +16,7 @@
package android.service.wearable;
+import android.app.ambientcontext.AmbientContextEventRequest;
import android.os.PersistableBundle;
import android.os.RemoteCallback;
import android.os.SharedMemory;
@@ -29,4 +30,8 @@
oneway interface IWearableSensingService {
void provideDataStream(in ParcelFileDescriptor parcelFileDescriptor, in RemoteCallback callback);
void provideData(in PersistableBundle data, in SharedMemory sharedMemory, in RemoteCallback callback);
+ void startDetection(in AmbientContextEventRequest request, in String packageName,
+ in RemoteCallback detectionResultCallback, in RemoteCallback statusCallback);
+ void stopDetection(in String packageName);
+ void queryServiceStatus(in int[] eventTypes, in String packageName, in RemoteCallback callback);
}
\ No newline at end of file
diff --git a/core/java/android/service/wearable/WearableSensingService.java b/core/java/android/service/wearable/WearableSensingService.java
index a1c7658..8f49bcb 100644
--- a/core/java/android/service/wearable/WearableSensingService.java
+++ b/core/java/android/service/wearable/WearableSensingService.java
@@ -22,6 +22,7 @@
import android.annotation.SystemApi;
import android.app.Service;
import android.app.ambientcontext.AmbientContextEvent;
+import android.app.ambientcontext.AmbientContextEventRequest;
import android.app.wearable.WearableSensingManager;
import android.content.Intent;
import android.os.Bundle;
@@ -30,9 +31,14 @@
import android.os.PersistableBundle;
import android.os.RemoteCallback;
import android.os.SharedMemory;
+import android.service.ambientcontext.AmbientContextDetectionResult;
+import android.service.ambientcontext.AmbientContextDetectionServiceStatus;
import android.util.Slog;
+import java.util.Arrays;
+import java.util.HashSet;
import java.util.Objects;
+import java.util.Set;
import java.util.function.Consumer;
/**
@@ -116,6 +122,60 @@
};
WearableSensingService.this.onDataProvided(data, sharedMemory, consumer);
}
+
+ /** {@inheritDoc} */
+ @Override
+ public void startDetection(@NonNull AmbientContextEventRequest request,
+ String packageName, RemoteCallback detectionResultCallback,
+ RemoteCallback statusCallback) {
+ Objects.requireNonNull(request);
+ Objects.requireNonNull(packageName);
+ Objects.requireNonNull(detectionResultCallback);
+ Objects.requireNonNull(statusCallback);
+ Consumer<AmbientContextDetectionResult> detectionResultConsumer = result -> {
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(
+ AmbientContextDetectionResult.RESULT_RESPONSE_BUNDLE_KEY, result);
+ detectionResultCallback.sendResult(bundle);
+ };
+ Consumer<AmbientContextDetectionServiceStatus> statusConsumer = status -> {
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(
+ AmbientContextDetectionServiceStatus.STATUS_RESPONSE_BUNDLE_KEY,
+ status);
+ statusCallback.sendResult(bundle);
+ };
+ WearableSensingService.this.onStartDetection(
+ request, packageName, statusConsumer, detectionResultConsumer);
+ Slog.d(TAG, "startDetection " + request);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void stopDetection(String packageName) {
+ Objects.requireNonNull(packageName);
+ WearableSensingService.this.onStopDetection(packageName);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void queryServiceStatus(@AmbientContextEvent.EventCode int[] eventTypes,
+ String packageName, RemoteCallback callback) {
+ Objects.requireNonNull(eventTypes);
+ Objects.requireNonNull(packageName);
+ Objects.requireNonNull(callback);
+ Consumer<AmbientContextDetectionServiceStatus> consumer = response -> {
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(
+ AmbientContextDetectionServiceStatus.STATUS_RESPONSE_BUNDLE_KEY,
+ response);
+ callback.sendResult(bundle);
+ };
+ Integer[] events = intArrayToIntegerArray(eventTypes);
+ WearableSensingService.this.onQueryServiceStatus(
+ new HashSet<>(Arrays.asList(events)), packageName, consumer);
+ }
+
};
}
Slog.w(TAG, "Incorrect service interface, returning null.");
@@ -155,4 +215,61 @@
@NonNull PersistableBundle data,
@Nullable SharedMemory sharedMemory,
@NonNull Consumer<Integer> statusConsumer);
+
+ /**
+ * Called when a client app requests starting detection of the events in the request. The
+ * implementation should keep track of whether the user has explicitly consented to detecting
+ * the events using on-going ambient sensor (e.g. microphone), and agreed to share the
+ * detection results with this client app. If the user has not consented, the detection
+ * should not start, and the statusConsumer should get a response with STATUS_ACCESS_DENIED.
+ * If the user has made the consent and the underlying services are available, the
+ * implementation should start detection and provide detected events to the
+ * detectionResultConsumer. If the type of event needs immediate attention, the implementation
+ * should send result as soon as detected. Otherwise, the implementation can batch response.
+ * The ongoing detection will keep running, until onStopDetection is called. If there were
+ * previously requested detections from the same package, regardless of the type of events in
+ * the request, the previous request will be replaced with the new request and pending events
+ * are discarded.
+ *
+ * @param request The request with events to detect.
+ * @param packageName the requesting app's package name
+ * @param statusConsumer the consumer for the service status.
+ * @param detectionResultConsumer the consumer for the detected event
+ */
+ @BinderThread
+ public abstract void onStartDetection(@NonNull AmbientContextEventRequest request,
+ @NonNull String packageName,
+ @NonNull Consumer<AmbientContextDetectionServiceStatus> statusConsumer,
+ @NonNull Consumer<AmbientContextDetectionResult> detectionResultConsumer);
+
+ /**
+ * Stops detection of the events. Events that are not being detected will be ignored.
+ *
+ * @param packageName stops detection for the given package.
+ */
+ public abstract void onStopDetection(@NonNull String packageName);
+
+ /**
+ * Called when a query for the detection status occurs. The implementation should check
+ * the detection status of the requested events for the package, and provide results in a
+ * {@link AmbientContextDetectionServiceStatus} for the consumer.
+ *
+ * @param eventTypes The events to check for status.
+ * @param packageName the requesting app's package name
+ * @param consumer the consumer for the query results
+ */
+ @BinderThread
+ public abstract void onQueryServiceStatus(@NonNull Set<Integer> eventTypes,
+ @NonNull String packageName,
+ @NonNull Consumer<AmbientContextDetectionServiceStatus> consumer);
+
+ @NonNull
+ private static Integer[] intArrayToIntegerArray(@NonNull int[] integerSet) {
+ Integer[] intArray = new Integer[integerSet.length];
+ int i = 0;
+ for (Integer type : integerSet) {
+ intArray[i++] = type;
+ }
+ return intArray;
+ }
}
diff --git a/core/java/android/telephony/TelephonyCallback.java b/core/java/android/telephony/TelephonyCallback.java
index a50e6db..f8df668 100644
--- a/core/java/android/telephony/TelephonyCallback.java
+++ b/core/java/android/telephony/TelephonyCallback.java
@@ -106,6 +106,10 @@
/**
* Event for changes to the network service state (cellular).
*
+ * <p>Requires {@link Manifest.permission#ACCESS_FINE_LOCATION} or {@link
+ * Manifest.permission#ACCESS_COARSE_LOCATION} depending on the accuracy of the location info
+ * listeners want to get.
+ *
* @hide
* @see ServiceStateListener#onServiceStateChanged
* @see ServiceState
@@ -485,8 +489,9 @@
* <p>Requires permission {@link android.Manifest.permission#READ_PRECISE_PHONE_STATE} or
* the calling app has carrier privileges (see {@link TelephonyManager#hasCarrierPrivileges}).
*
- * <p>Also requires the {@link Manifest.permission#ACCESS_FINE_LOCATION} permission, regardless
- * of whether the calling app has carrier privileges.
+ * <p>Requires the {@link Manifest.permission#ACCESS_FINE_LOCATION} permission in case that
+ * listener want to get location info in {@link CellIdentity} regardless of whether the calling
+ * app has carrier privileges.
*
* @hide
* @see RegistrationFailedListener#onRegistrationFailed
@@ -504,8 +509,9 @@
* <p>Requires permission {@link android.Manifest.permission#READ_PRECISE_PHONE_STATE} or
* the calling app has carrier privileges (see {@link TelephonyManager#hasCarrierPrivileges}).
*
- * <p>Also requires the {@link Manifest.permission#ACCESS_FINE_LOCATION} permission, regardless
- * of whether the calling app has carrier privileges.
+ * <p>Requires the {@link Manifest.permission#ACCESS_FINE_LOCATION} permission in case that
+ * listener want to get {@link BarringInfo} which includes location info in {@link CellIdentity}
+ * regardless of whether the calling app has carrier privileges.
*
* @hide
* @see BarringInfoListener#onBarringInfoChanged
@@ -691,10 +697,8 @@
* Only apps holding the {@link Manifest.permission#ACCESS_FINE_LOCATION} permission will
* receive all the information in {@link ServiceState}, otherwise the cellIdentity
* will be null if apps only holding the {@link Manifest.permission#ACCESS_COARSE_LOCATION}
- * permission.
- * Network operator name in long/short alphanumeric format and numeric id will be null if
- * apps holding neither {@link android.Manifest.permission#ACCESS_FINE_LOCATION} nor
- * {@link android.Manifest.permission#ACCESS_COARSE_LOCATION}.
+ * permission. Network operator name in long/short alphanumeric format and numeric id will
+ * be null if apps holding neither {@link android.Manifest.permission#ACCESS_FINE_LOCATION}
*
* @see ServiceState#STATE_EMERGENCY_ONLY
* @see ServiceState#STATE_IN_SERVICE
@@ -1284,6 +1288,9 @@
* {@link android.Manifest.permission#READ_PRECISE_PHONE_STATE} and
* {@link android.Manifest.permission#ACCESS_FINE_LOCATION}.
*
+ * If the calling app doesn't have {@link android.Manifest.permission#ACCESS_FINE_LOCATION},
+ * it will receive {@link CellIdentity} without location-sensitive information included.
+ *
* @param cellIdentity the CellIdentity, which must include the globally unique
* identifier
* for the cell (for example, all components of the CGI or ECGI).
@@ -1462,6 +1469,10 @@
* {@link android.Manifest.permission#READ_PRECISE_PHONE_STATE} and
* {@link android.Manifest.permission#ACCESS_FINE_LOCATION}.
*
+ * If the calling app doesn't have {@link android.Manifest.permission#ACCESS_FINE_LOCATION},
+ * it will receive {@link BarringInfo} including {@link CellIdentity} without
+ * location-sensitive information included.
+ *
* @param barringInfo for all services on the current cell.
* @see android.telephony.BarringInfo
*/
diff --git a/core/java/android/util/FeatureFlagUtils.java b/core/java/android/util/FeatureFlagUtils.java
index 65bc81b..f6a8697 100644
--- a/core/java/android/util/FeatureFlagUtils.java
+++ b/core/java/android/util/FeatureFlagUtils.java
@@ -153,6 +153,11 @@
*/
public static final String SETTINGS_AUDIO_ROUTING = "settings_audio_routing";
+ /** Flag to enable/disable flash alerts
+ * @hide
+ */
+ public static final String SETTINGS_FLASH_ALERTS = "settings_flash_alerts";
+
private static final Map<String, String> DEFAULT_FLAGS;
static {
@@ -192,6 +197,7 @@
DEFAULT_FLAGS.put(SETTINGS_BIOMETRICS2_ENROLLMENT, "false");
DEFAULT_FLAGS.put(SETTINGS_ACCESSIBILITY_HEARING_AID_PAGE, "false");
DEFAULT_FLAGS.put(SETTINGS_AUDIO_ROUTING, "false");
+ DEFAULT_FLAGS.put(SETTINGS_FLASH_ALERTS, "false");
}
private static final Set<String> PERSISTENT_FLAGS;
diff --git a/core/java/android/view/AttachedSurfaceControl.java b/core/java/android/view/AttachedSurfaceControl.java
index 3b082bc..787ffb7 100644
--- a/core/java/android/view/AttachedSurfaceControl.java
+++ b/core/java/android/view/AttachedSurfaceControl.java
@@ -18,6 +18,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UiThread;
+import android.graphics.Rect;
import android.graphics.Region;
import android.hardware.HardwareBuffer;
import android.window.SurfaceSyncGroup;
@@ -149,4 +150,21 @@
default SurfaceSyncGroup getOrCreateSurfaceSyncGroup() {
return null;
}
+
+ /**
+ * Set a crop region on all children parented to the layer represented by this
+ * AttachedSurfaceControl. This includes SurfaceView, and an example usage may
+ * be to ensure that SurfaceView with {@link android.view.SurfaceView#setZOrderOnTop}
+ * are cropped to a region not including the app bar.
+ *
+ * This cropped is expressed in terms of insets in window-space. Negative insets
+ * are considered invalid and will produce an exception. Insets of zero will produce
+ * the same result as if this function had never been called.
+ *
+ * @param insets The insets in each direction by which to bound the children
+ * expressed in window-space.
+ * @hide
+ */
+ default void setChildBoundingInsets(@NonNull Rect insets) {
+ }
}
diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java
index 71030bcc..689dce8 100644
--- a/core/java/android/view/Display.java
+++ b/core/java/android/view/Display.java
@@ -332,6 +332,34 @@
public static final int FLAG_OWN_FOCUS = 1 << 11;
/**
+ * Flag: Indicates that the display should not become the top focused display by stealing the
+ * top focus from another display.
+ *
+ * <p>The result is that only targeted input events (displayId of input event matches the
+ * displayId of the display) can reach this display. A display with this flag set can still
+ * become the top focused display, if the system consists of only one display or if all
+ * displays have this flag set. In both cases the default display becomes the top focused
+ * display.
+ *
+ * <p>Note: A display only has a focused window if either
+ * - the display is the top focused display or
+ * - the display manages its own focus (via {@link #FLAG_OWN_FOCUS})
+ * - or all the displays manage their own focus (via {@code config_perDisplayFocusEnabled} flag)
+ * If a display has no focused window, no input event is dispatched to it. Therefore this
+ * flag is only useful together with {@link #FLAG_OWN_FOCUS} and will be
+ * ignored if it is not set.
+ *
+ * <p>Note: The framework only supports IME on the top focused display (b/262520411). Therefore,
+ * Enabling this flag on a display implicitly disables showing any IME. This is not intended
+ * behavior but cannot be fixed until b/262520411 is implemented. If you need IME on display do
+ * not set this flag.
+ *
+ * @hide
+ * @see #getFlags()
+ */
+ public static final int FLAG_STEAL_TOP_FOCUS_DISABLED = 1 << 12;
+
+ /**
* Display flag: Indicates that the contents of the display should not be scaled
* to fit the physical screen dimensions. Used for development only to emulate
* devices with smaller physicals screens while preserving density.
@@ -1705,6 +1733,16 @@
return (mFlags & FLAG_TRUSTED) == FLAG_TRUSTED;
}
+ /**
+ * @return {@code true} if the display can steal the top focus from another display.
+ *
+ * @see #FLAG_STEAL_TOP_FOCUS_DISABLED
+ * @hide
+ */
+ public boolean canStealTopFocus() {
+ return (mFlags & FLAG_STEAL_TOP_FOCUS_DISABLED) == 0;
+ }
+
private void updateDisplayInfoLocked() {
// Note: The display manager caches display info objects on our behalf.
DisplayInfo newInfo = mGlobal.getDisplayInfo(mDisplayId);
diff --git a/core/java/android/view/EventLogTags.logtags b/core/java/android/view/EventLogTags.logtags
index 098f1af..1ad3472 100644
--- a/core/java/android/view/EventLogTags.logtags
+++ b/core/java/android/view/EventLogTags.logtags
@@ -37,6 +37,14 @@
#
# See system/core/logcat/event.logtags for the master copy of the tags.
+# 32000 - 32999 reserved for input method framework
+# IME animation is started.
+32006 imf_ime_anim_start (token|3),(animation type|1),(alpha|5),(current insets|3),(shown insets|3),(hidden insets|3)
+# IME animation is finished.
+32007 imf_ime_anim_finish (token|3),(animation type|1),(alpha|5),(shown|1),(insets|3)
+# IME animation is canceled.
+32008 imf_ime_anim_cancel (token|3),(animation type|1),(pending insets|3)
+
# 62000 - 62199 reserved for inputflinger
# ---------------------------
diff --git a/core/java/android/view/ImeInsetsSourceConsumer.java b/core/java/android/view/ImeInsetsSourceConsumer.java
index b8cd7b9..f87b746 100644
--- a/core/java/android/view/ImeInsetsSourceConsumer.java
+++ b/core/java/android/view/ImeInsetsSourceConsumer.java
@@ -16,6 +16,7 @@
package android.view;
+import static android.os.Trace.TRACE_TAG_VIEW;
import static android.view.ImeInsetsSourceConsumerProto.INSETS_SOURCE_CONSUMER;
import static android.view.ImeInsetsSourceConsumerProto.IS_HIDE_ANIMATION_RUNNING;
import static android.view.ImeInsetsSourceConsumerProto.IS_REQUESTED_VISIBLE_AWAITING_CONTROL;
@@ -23,11 +24,15 @@
import android.annotation.Nullable;
import android.os.IBinder;
+import android.os.Process;
+import android.os.Trace;
import android.util.proto.ProtoOutputStream;
import android.view.SurfaceControl.Transaction;
+import android.view.inputmethod.ImeTracker;
import android.view.inputmethod.InputMethodManager;
import com.android.internal.inputmethod.ImeTracing;
+import com.android.internal.inputmethod.SoftInputShowHideReason;
import java.util.function.Supplier;
@@ -48,8 +53,8 @@
/**
* Tracks whether {@link WindowInsetsController#show(int)} or
* {@link InputMethodManager#showSoftInput(View, int)} is called during IME hide animation.
- * If it was called, we should not call {@link InputMethodManager#notifyImeHidden(IBinder)},
- * because the IME is being shown.
+ * If it was called, we should not call {@link InputMethodManager#notifyImeHidden(IBinder,
+ * ImeTracker.Token)}, because the IME is being shown.
*/
private boolean mIsShowRequestedDuringHideAnimation;
@@ -76,7 +81,7 @@
// Remove IME surface as IME has finished hide animation, if there is no pending
// show request.
if (!mIsShowRequestedDuringHideAnimation) {
- notifyHidden();
+ notifyHidden(null /* statsToken */);
removeSurface();
}
}
@@ -120,7 +125,8 @@
* @return @see {@link android.view.InsetsSourceConsumer.ShowResult}.
*/
@Override
- public @ShowResult int requestShow(boolean fromIme) {
+ @ShowResult
+ public int requestShow(boolean fromIme, @Nullable ImeTracker.Token statsToken) {
if (fromIme) {
ImeTracing.getInstance().triggerClientDump(
"ImeInsetsSourceConsumer#requestShow",
@@ -129,6 +135,9 @@
// TODO: ResultReceiver for IME.
// TODO: Set mShowOnNextImeRender to automatically show IME and guard it with a flag.
+ ImeTracker.get().onProgress(statsToken,
+ ImeTracker.PHASE_CLIENT_INSETS_CONSUMER_REQUEST_SHOW);
+
if (getControl() == null) {
// If control is null, schedule to show IME when control is available.
mIsRequestedVisibleAwaitingControl = true;
@@ -140,16 +149,32 @@
return ShowResult.SHOW_IMMEDIATELY;
}
- return getImm().requestImeShow(mController.getHost().getWindowToken())
+ return getImm().requestImeShow(mController.getHost().getWindowToken(), statsToken)
? ShowResult.IME_SHOW_DELAYED : ShowResult.IME_SHOW_FAILED;
}
/**
* Notify {@link com.android.server.inputmethod.InputMethodManagerService} that
* IME insets are hidden.
+ *
+ * @param statsToken the token tracking the current IME hide request or {@code null} otherwise.
*/
- private void notifyHidden() {
- getImm().notifyImeHidden(mController.getHost().getWindowToken());
+ private void notifyHidden(@Nullable ImeTracker.Token statsToken) {
+ // Create a new stats token to track the hide request when:
+ // - we do not already have one, or
+ // - we do already have one, but we have control and use the passed in token
+ // for the insets animation already.
+ if (statsToken == null || getControl() != null) {
+ statsToken = ImeTracker.get().onRequestHide(null /* component */, Process.myUid(),
+ ImeTracker.ORIGIN_CLIENT_HIDE_SOFT_INPUT,
+ SoftInputShowHideReason.HIDE_SOFT_INPUT_BY_INSETS_API);
+ }
+
+ ImeTracker.get().onProgress(statsToken,
+ ImeTracker.PHASE_CLIENT_INSETS_CONSUMER_NOTIFY_HIDDEN);
+
+ getImm().notifyImeHidden(mController.getHost().getWindowToken(), statsToken);
+ Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.hideRequestFromApi", 0);
}
@Override
diff --git a/core/java/android/view/InsetsAnimationControlImpl.java b/core/java/android/view/InsetsAnimationControlImpl.java
index 1648659..309a94a 100644
--- a/core/java/android/view/InsetsAnimationControlImpl.java
+++ b/core/java/android/view/InsetsAnimationControlImpl.java
@@ -17,6 +17,9 @@
package android.view;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.view.EventLogTags.IMF_IME_ANIM_CANCEL;
+import static android.view.EventLogTags.IMF_IME_ANIM_FINISH;
+import static android.view.EventLogTags.IMF_IME_ANIM_START;
import static android.view.InsetsAnimationControlImplProto.CURRENT_ALPHA;
import static android.view.InsetsAnimationControlImplProto.IS_CANCELLED;
import static android.view.InsetsAnimationControlImplProto.IS_FINISHED;
@@ -36,7 +39,10 @@
import static android.view.InsetsState.ISIDE_RIGHT;
import static android.view.InsetsState.ISIDE_TOP;
import static android.view.InsetsState.ITYPE_IME;
+import static android.view.WindowInsets.Type.ime;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
+import static android.view.inputmethod.ImeTracker.DEBUG_IME_VISIBILITY;
+import static android.view.inputmethod.ImeTracker.TOKEN_NONE;
import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
@@ -47,6 +53,7 @@
import android.graphics.Point;
import android.graphics.Rect;
import android.util.ArraySet;
+import android.util.EventLog;
import android.util.Log;
import android.util.SparseArray;
import android.util.SparseIntArray;
@@ -156,6 +163,12 @@
mLayoutInsetsDuringAnimation = layoutInsetsDuringAnimation;
mTranslator = translator;
mStatsToken = statsToken;
+ if (DEBUG_IME_VISIBILITY && (types & ime()) != 0) {
+ EventLog.writeEvent(IMF_IME_ANIM_START,
+ mStatsToken != null ? mStatsToken.getTag() : TOKEN_NONE, mAnimationType,
+ mCurrentAlpha, "Current:" + mCurrentInsets, "Shown:" + mShownInsets,
+ "Hidden:" + mHiddenInsets);
+ }
mController.startAnimation(this, listener, types, mAnimation,
new Bounds(mHiddenInsets, mShownInsets));
}
@@ -314,11 +327,16 @@
}
mShownOnFinish = shown;
mFinished = true;
- setInsetsAndAlpha(shown ? mShownInsets : mHiddenInsets, mPendingAlpha, 1f /* fraction */,
- true /* allowWhenFinished */);
+ final Insets insets = shown ? mShownInsets : mHiddenInsets;
+ setInsetsAndAlpha(insets, mPendingAlpha, 1f /* fraction */, true /* allowWhenFinished */);
if (DEBUG) Log.d(TAG, "notify control request finished for types: " + mTypes);
mListener.onFinished(this);
+ if (DEBUG_IME_VISIBILITY && (mTypes & ime()) != 0) {
+ EventLog.writeEvent(IMF_IME_ANIM_FINISH,
+ mStatsToken != null ? mStatsToken.getTag() : TOKEN_NONE, mAnimationType,
+ mCurrentAlpha, shown ? 1 : 0, Objects.toString(insets));
+ }
}
@Override
@@ -339,7 +357,11 @@
mCancelled = true;
mListener.onCancelled(mReadyDispatched ? this : null);
if (DEBUG) Log.d(TAG, "notify Control request cancelled for types: " + mTypes);
-
+ if (DEBUG_IME_VISIBILITY && (mTypes & ime()) != 0) {
+ EventLog.writeEvent(IMF_IME_ANIM_CANCEL,
+ mStatsToken != null ? mStatsToken.getTag() : TOKEN_NONE, mAnimationType,
+ Objects.toString(mPendingInsets));
+ }
releaseLeashes();
}
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index c074e84..8abe66a 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -41,13 +41,13 @@
import android.os.CancellationSignal;
import android.os.Handler;
import android.os.IBinder;
+import android.os.Process;
import android.os.Trace;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
import android.util.Pair;
import android.util.SparseArray;
-import android.util.SparseIntArray;
import android.util.proto.ProtoOutputStream;
import android.view.InsetsSourceConsumer.ShowResult;
import android.view.InsetsState.InternalInsetsType;
@@ -65,6 +65,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
import com.android.internal.inputmethod.ImeTracing;
+import com.android.internal.inputmethod.SoftInputShowHideReason;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
@@ -978,7 +979,14 @@
@Override
public void show(@InsetsType int types) {
- show(types, false /* fromIme */, null /* statsToken */);
+ ImeTracker.Token statsToken = null;
+ if ((types & ime()) != 0) {
+ statsToken = ImeTracker.get().onRequestShow(null /* component */,
+ Process.myUid(), ImeTracker.ORIGIN_CLIENT_SHOW_SOFT_INPUT,
+ SoftInputShowHideReason.SHOW_SOFT_INPUT_BY_INSETS_API);
+ }
+
+ show(types, false /* fromIme */, statsToken);
}
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
@@ -1055,7 +1063,14 @@
@Override
public void hide(@InsetsType int types) {
- hide(types, false /* fromIme */, null /* statsToken */);
+ ImeTracker.Token statsToken = null;
+ if ((types & ime()) != 0) {
+ statsToken = ImeTracker.get().onRequestHide(null /* component */,
+ Process.myUid(), ImeTracker.ORIGIN_CLIENT_HIDE_SOFT_INPUT,
+ SoftInputShowHideReason.HIDE_SOFT_INPUT_BY_INSETS_API);
+ }
+
+ hide(types, false /* fromIme */, statsToken);
}
@VisibleForTesting
@@ -1165,13 +1180,17 @@
if (DEBUG) Log.d(TAG, "user animation disabled types: " + disabledTypes);
types &= ~mDisabledUserAnimationInsetsTypes;
- if (fromIme && (disabledTypes & ime()) != 0
- && !mState.getSource(mImeSourceConsumer.getId()).isVisible()) {
- // We've requested IMM to show IME, but the IME is not controllable. We need to
- // cancel the request.
- setRequestedVisibleTypes(0 /* visibleTypes */, ime());
- if (mImeSourceConsumer.onAnimationStateChanged(false /* running */)) {
- notifyVisibilityChanged();
+ if ((disabledTypes & ime()) != 0) {
+ ImeTracker.get().onFailed(statsToken,
+ ImeTracker.PHASE_CLIENT_DISABLED_USER_ANIMATION);
+
+ if (fromIme && !mState.getSource(mImeSourceConsumer.getId()).isVisible()) {
+ // We've requested IMM to show IME, but the IME is not controllable. We need to
+ // cancel the request.
+ setRequestedVisibleTypes(0 /* visibleTypes */, ime());
+ if (mImeSourceConsumer.onAnimationStateChanged(false /* running */)) {
+ notifyVisibilityChanged();
+ }
}
}
}
@@ -1179,8 +1198,12 @@
// nothing to animate.
listener.onCancelled(null);
if (DEBUG) Log.d(TAG, "no types to animate in controlAnimationUnchecked");
+ Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromApi", 0);
+ Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromApiToImeReady", 0);
return;
}
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_DISABLED_USER_ANIMATION);
+
cancelExistingControllers(types);
if (DEBUG) Log.d(TAG, "controlAnimation types: " + types);
mLastStartedAnimTypes |= types;
@@ -1188,7 +1211,7 @@
final SparseArray<InsetsSourceControl> controls = new SparseArray<>();
Pair<Integer, Boolean> typesReadyPair = collectSourceControls(
- fromIme, types, controls, animationType);
+ fromIme, types, controls, animationType, statsToken);
int typesReady = typesReadyPair.first;
boolean imeReady = typesReadyPair.second;
if (DEBUG) Log.d(TAG, String.format(
@@ -1218,12 +1241,20 @@
setRequestedVisibleTypes(mReportedRequestedVisibleTypes, types);
Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromApi", 0);
+ if (!fromIme) {
+ Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromApiToImeReady", 0);
+ }
return;
}
if (typesReady == 0) {
if (DEBUG) Log.d(TAG, "No types ready. onCancelled()");
listener.onCancelled(null);
+ reportRequestedVisibleTypes();
+ Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromApi", 0);
+ if (!fromIme) {
+ Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromApiToImeReady", 0);
+ }
return;
}
@@ -1279,7 +1310,10 @@
* @return Pair of (types ready to animate, IME ready to animate).
*/
private Pair<Integer, Boolean> collectSourceControls(boolean fromIme, @InsetsType int types,
- SparseArray<InsetsSourceControl> controls, @AnimationType int animationType) {
+ SparseArray<InsetsSourceControl> controls, @AnimationType int animationType,
+ @Nullable ImeTracker.Token statsToken) {
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_COLLECT_SOURCE_CONTROLS);
+
int typesReady = 0;
boolean imeReady = true;
for (int i = mSourceConsumers.size() - 1; i >= 0; i--) {
@@ -1292,7 +1326,7 @@
boolean canRun = true;
if (show) {
// Show request
- switch(consumer.requestShow(fromIme)) {
+ switch(consumer.requestShow(fromIme, statsToken)) {
case ShowResult.SHOW_IMMEDIATELY:
break;
case ShowResult.IME_SHOW_DELAYED:
@@ -1571,6 +1605,10 @@
if (types == 0) {
// nothing to animate.
if (DEBUG) Log.d(TAG, "applyAnimation, nothing to animate");
+ Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromApi", 0);
+ if (!fromIme) {
+ Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromApiToImeReady", 0);
+ }
return;
}
diff --git a/core/java/android/view/InsetsSourceConsumer.java b/core/java/android/view/InsetsSourceConsumer.java
index f46eb34..47672a3 100644
--- a/core/java/android/view/InsetsSourceConsumer.java
+++ b/core/java/android/view/InsetsSourceConsumer.java
@@ -35,6 +35,7 @@
import android.util.proto.ProtoOutputStream;
import android.view.SurfaceControl.Transaction;
import android.view.WindowInsets.Type.InsetsType;
+import android.view.inputmethod.ImeTracker;
import com.android.internal.annotations.VisibleForTesting;
@@ -50,10 +51,14 @@
public class InsetsSourceConsumer {
@Retention(RetentionPolicy.SOURCE)
- @IntDef(value = {ShowResult.SHOW_IMMEDIATELY, ShowResult.IME_SHOW_DELAYED, ShowResult.IME_SHOW_FAILED})
+ @IntDef(value = {
+ ShowResult.SHOW_IMMEDIATELY,
+ ShowResult.IME_SHOW_DELAYED,
+ ShowResult.IME_SHOW_FAILED
+ })
@interface ShowResult {
/**
- * Window type is ready to be shown, will be shown immidiately.
+ * Window type is ready to be shown, will be shown immediately.
*/
int SHOW_IMMEDIATELY = 0;
/**
@@ -71,11 +76,13 @@
protected final InsetsController mController;
protected final InsetsState mState;
private int mId;
- private final @InsetsType int mType;
+ @InsetsType
+ private final int mType;
private static final String TAG = "InsetsSourceConsumer";
private final Supplier<Transaction> mTransactionSupplier;
- private @Nullable InsetsSourceControl mSourceControl;
+ @Nullable
+ private InsetsSourceControl mSourceControl;
private boolean mHasWindowFocus;
/**
@@ -180,7 +187,7 @@
return true;
}
- @VisibleForTesting
+ @VisibleForTesting(visibility = PACKAGE)
public InsetsSourceControl getControl() {
return mSourceControl;
}
@@ -280,10 +287,16 @@
* @param fromController {@code true} if request is coming from controller.
* (e.g. in IME case, controller is
* {@link android.inputmethodservice.InputMethodService}).
+ * @param statsToken the token tracking the current IME show request or {@code null} otherwise.
+ *
+ * @implNote The {@code statsToken} is ignored here, and only handled in
+ * {@link ImeInsetsSourceConsumer} for IME animations only.
+ *
* @return @see {@link ShowResult}.
*/
- @VisibleForTesting
- public @ShowResult int requestShow(boolean fromController) {
+ @VisibleForTesting(visibility = PACKAGE)
+ @ShowResult
+ public int requestShow(boolean fromController, @Nullable ImeTracker.Token statsToken) {
return ShowResult.SHOW_IMMEDIATELY;
}
diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java
index 9db084e..e38376d 100644
--- a/core/java/android/view/SurfaceView.java
+++ b/core/java/android/view/SurfaceView.java
@@ -1062,6 +1062,15 @@
}
/**
+ * @hide
+ */
+ public String getName() {
+ ViewRootImpl viewRoot = getViewRootImpl();
+ String viewRootName = viewRoot == null ? "detached" : viewRoot.getTitle().toString();
+ return "SurfaceView[" + viewRootName + "]";
+ }
+
+ /**
* If SV is trying to be part of the VRI sync, we need to add SV to the VRI sync before
* invoking the redrawNeeded call to the owner. This is to ensure we can set up the SV in
* the sync before the SV owner knows it needs to draw a new frame.
@@ -1073,7 +1082,7 @@
private void handleSyncBufferCallback(SurfaceHolder.Callback[] callbacks,
SyncBufferTransactionCallback syncBufferTransactionCallback) {
- final SurfaceSyncGroup surfaceSyncGroup = new SurfaceSyncGroup();
+ final SurfaceSyncGroup surfaceSyncGroup = new SurfaceSyncGroup(getName());
getViewRootImpl().addToSync(surfaceSyncGroup);
redrawNeededAsync(callbacks, () -> {
Transaction t = null;
@@ -1088,7 +1097,7 @@
}
private void handleSyncNoBuffer(SurfaceHolder.Callback[] callbacks) {
- final SurfaceSyncGroup surfaceSyncGroup = new SurfaceSyncGroup();
+ final SurfaceSyncGroup surfaceSyncGroup = new SurfaceSyncGroup(getName());
synchronized (mSyncGroups) {
mSyncGroups.add(surfaceSyncGroup);
}
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index d709840..0198457 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -21602,6 +21602,17 @@
return Math.max(vis1, vis2);
}
+ private boolean mShouldFakeFocus = false;
+
+ /**
+ * Fake send a focus event after attaching to window.
+ * See {@link android.view.ViewRootImpl#dispatchCompatFakeFocus()} for details.
+ * @hide
+ */
+ public void fakeFocusAfterAttachingToWindow() {
+ mShouldFakeFocus = true;
+ }
+
/**
* @param info the {@link android.view.View.AttachInfo} to associated with
* this view
@@ -21670,6 +21681,11 @@
notifyEnterOrExitForAutoFillIfNeeded(true);
notifyAppearedOrDisappearedForContentCaptureIfNeeded(true);
+
+ if (mShouldFakeFocus) {
+ getViewRootImpl().dispatchCompatFakeFocus();
+ mShouldFakeFocus = false;
+ }
}
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index ea7a64e..c0f4731 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -261,6 +261,7 @@
private static final boolean DEBUG_CONTENT_CAPTURE = false || LOCAL_LOGV;
private static final boolean DEBUG_SCROLL_CAPTURE = false || LOCAL_LOGV;
private static final boolean DEBUG_BLAST = false || LOCAL_LOGV;
+ private static final int LOGTAG_INPUT_FOCUS = 62001;
/**
* Set to false if we do not want to use the multi threaded renderer even though
@@ -675,12 +676,6 @@
private BLASTBufferQueue mBlastBufferQueue;
/**
- * Transaction object that can be used to synchronize child SurfaceControl changes with
- * ViewRootImpl SurfaceControl changes by the server. The object is passed along with
- * the SurfaceChangedCallback.
- */
- private final Transaction mSurfaceChangedTransaction = new Transaction();
- /**
* Child container layer of {@code mSurface} with the same bounds as its parent, and cropped to
* the surface insets. This surface is created only if a client requests it via {@link
* #getBoundsLayer()}. By parenting to this bounds surface, child surfaces can ensure they do
@@ -919,6 +914,8 @@
}
}
};
+ private final Rect mChildBoundingInsets = new Rect();
+ private boolean mChildBoundingInsetsChanged = false;
private String mTag = TAG;
@@ -997,8 +994,7 @@
mFastScrollSoundEffectsEnabled = audioManager.areNavigationRepeatSoundEffectsEnabled();
mScrollCaptureRequestTimeout = SCROLL_CAPTURE_REQUEST_TIMEOUT_MILLIS;
- mOnBackInvokedDispatcher = new WindowOnBackInvokedDispatcher(
- context.getApplicationInfo().isOnBackInvokedCallbackEnabled());
+ mOnBackInvokedDispatcher = new WindowOnBackInvokedDispatcher(context);
}
public static void addFirstDrawHandler(Runnable callback) {
@@ -1304,14 +1300,6 @@
mTmpFrames);
setFrame(mTmpFrames.frame);
registerBackCallbackOnWindow();
- if (!WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(mContext)) {
- // For apps requesting legacy back behavior, we add a compat callback that
- // dispatches {@link KeyEvent#KEYCODE_BACK} to their root views.
- // This way from system point of view, these apps are providing custom
- // {@link OnBackInvokedCallback}s, and will not play system back animations
- // for them.
- registerCompatOnBackInvokedCallback();
- }
if (DEBUG_LAYOUT) Log.v(mTag, "Added window " + mWindow);
if (res < WindowManagerGlobal.ADD_OKAY) {
mAttachInfo.mRootView = null;
@@ -2139,9 +2127,9 @@
mSurfaceChangedCallbacks.remove(c);
}
- private void notifySurfaceCreated() {
+ private void notifySurfaceCreated(Transaction t) {
for (int i = 0; i < mSurfaceChangedCallbacks.size(); i++) {
- mSurfaceChangedCallbacks.get(i).surfaceCreated(mSurfaceChangedTransaction);
+ mSurfaceChangedCallbacks.get(i).surfaceCreated(t);
}
}
@@ -2150,9 +2138,9 @@
* called if a new surface is created, only if the valid surface has been replaced with another
* valid surface.
*/
- private void notifySurfaceReplaced() {
+ private void notifySurfaceReplaced(Transaction t) {
for (int i = 0; i < mSurfaceChangedCallbacks.size(); i++) {
- mSurfaceChangedCallbacks.get(i).surfaceReplaced(mSurfaceChangedTransaction);
+ mSurfaceChangedCallbacks.get(i).surfaceReplaced(t);
}
}
@@ -2222,6 +2210,8 @@
mTempRect.inset(mWindowAttributes.surfaceInsets.left,
mWindowAttributes.surfaceInsets.top,
mWindowAttributes.surfaceInsets.right, mWindowAttributes.surfaceInsets.bottom);
+ mTempRect.inset(mChildBoundingInsets.left, mChildBoundingInsets.top,
+ mChildBoundingInsets.right, mChildBoundingInsets.bottom);
t.setWindowCrop(mBoundsLayer, mTempRect);
}
@@ -2243,7 +2233,7 @@
if (!sc.isValid()) return;
if (updateBoundsLayer(t)) {
- mergeWithNextTransaction(t, mSurface.getNextFrameNumber());
+ applyTransactionOnDraw(t);
}
}
@@ -2329,7 +2319,6 @@
*/
void notifyRendererOfFramePending() {
if (mAttachInfo.mThreadedRenderer != null) {
- mAttachInfo.mThreadedRenderer.notifyCallbackPending();
mAttachInfo.mThreadedRenderer.notifyFramePending();
}
}
@@ -2903,6 +2892,14 @@
host.dispatchAttachedToWindow(mAttachInfo, 0);
mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
dispatchApplyInsets(host);
+ if (!mOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled()) {
+ // For apps requesting legacy back behavior, we add a compat callback that
+ // dispatches {@link KeyEvent#KEYCODE_BACK} to their root views.
+ // This way from system point of view, these apps are providing custom
+ // {@link OnBackInvokedCallback}s, and will not play system back animations
+ // for them.
+ registerCompatOnBackInvokedCallback();
+ }
} else {
desiredWindowWidth = frame.width();
desiredWindowHeight = frame.height();
@@ -3447,7 +3444,8 @@
}
}
- if (surfaceSizeChanged || surfaceReplaced || surfaceCreated || windowAttributesChanged) {
+ if (surfaceSizeChanged || surfaceReplaced || surfaceCreated ||
+ windowAttributesChanged || mChildBoundingInsetsChanged) {
// If the surface has been replaced, there's a chance the bounds layer is not parented
// to the new layer. When updating bounds layer, also reparent to the main VRI
// SurfaceControl to ensure it's correctly placed in the hierarchy.
@@ -3458,6 +3456,7 @@
// enough. WMS doesn't want to keep around old children since they will leak when the
// client creates new children.
prepareSurfaces();
+ mChildBoundingInsetsChanged = false;
}
final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
@@ -3505,17 +3504,24 @@
}
}
+ boolean didUseTransaction = false;
// These callbacks will trigger SurfaceView SurfaceHolder.Callbacks and must be invoked
// after the measure pass. If its invoked before the measure pass and the app modifies
// the view hierarchy in the callbacks, we could leave the views in a broken state.
if (surfaceCreated) {
- notifySurfaceCreated();
+ notifySurfaceCreated(mTransaction);
+ didUseTransaction = true;
} else if (surfaceReplaced) {
- notifySurfaceReplaced();
+ notifySurfaceReplaced(mTransaction);
+ didUseTransaction = true;
} else if (surfaceDestroyed) {
notifySurfaceDestroyed();
}
+ if (didUseTransaction) {
+ applyTransactionOnDraw(mTransaction);
+ }
+
if (triggerGlobalLayoutListener) {
mAttachInfo.mRecomputeGlobalAttributes = false;
mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();
@@ -3719,22 +3725,18 @@
final int seqId = mSyncSeqId;
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 (t != null) {
- mSurfaceChangedTransaction.merge(t);
- }
- mWmsRequestSyncGroupState = WMS_SYNC_MERGED;
- reportDrawFinished(seqId);
- });
+ mWmsRequestSyncGroup = new SurfaceSyncGroup("wmsSync-" + mTag, t -> {
+ mWmsRequestSyncGroupState = WMS_SYNC_MERGED;
+ reportDrawFinished(t, seqId);
});
+ Trace.traceBegin(Trace.TRACE_TAG_VIEW,
+ "create WMS Sync group=" + mWmsRequestSyncGroup.getName());
if (DEBUG_BLAST) {
- Log.d(mTag, "Setup new sync id=" + mWmsRequestSyncGroup);
+ Log.d(mTag, "Setup new sync=" + mWmsRequestSyncGroup.getName());
}
mWmsRequestSyncGroup.addToSync(this);
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
notifySurfaceSyncStarted();
}
@@ -3835,8 +3837,7 @@
}
if (mAdded) {
- dispatchFocusEvent(hasWindowFocus);
-
+ dispatchFocusEvent(hasWindowFocus, false /* fakeFocus */);
// Note: must be done after the focus change callbacks,
// so all of the view state is set up correctly.
mImeFocusController.onPostWindowFocus(
@@ -3873,7 +3874,33 @@
}
}
- private void dispatchFocusEvent(boolean hasWindowFocus) {
+ /**
+ * Send a fake focus event for unfocused apps in split screen as some game engines wait to
+ * get focus before drawing the content of the app. This will be used so that apps do not get
+ * blacked out when they are resumed and do not have focus yet.
+ *
+ * {@hide}
+ */
+ // TODO(b/263094829): Investigate dispatching this for onPause as well
+ public void dispatchCompatFakeFocus() {
+ boolean aboutToHaveFocus = false;
+ synchronized (this) {
+ aboutToHaveFocus = mWindowFocusChanged && mUpcomingWindowFocus;
+ }
+ final boolean alreadyHaveFocus = mAttachInfo.mHasWindowFocus;
+ if (aboutToHaveFocus || alreadyHaveFocus) {
+ // Do not need to toggle focus if app doesn't need it, or has focus.
+ return;
+ }
+ EventLog.writeEvent(LOGTAG_INPUT_FOCUS,
+ "Giving fake focus to " + mBasePackageName, "reason=unity bug workaround");
+ dispatchFocusEvent(true /* hasWindowFocus */, true /* fakeFocus */);
+ EventLog.writeEvent(LOGTAG_INPUT_FOCUS,
+ "Removing fake focus from " + mBasePackageName, "reason=timeout callback");
+ dispatchFocusEvent(false /* hasWindowFocus */, true /* fakeFocus */);
+ }
+
+ private void dispatchFocusEvent(boolean hasWindowFocus, boolean fakeFocus) {
profileRendering(hasWindowFocus);
if (hasWindowFocus && mAttachInfo.mThreadedRenderer != null && mSurface.isValid()) {
mFullRedrawNeeded = true;
@@ -3899,7 +3926,10 @@
}
mAttachInfo.mHasWindowFocus = hasWindowFocus;
- mImeFocusController.onPreWindowFocus(hasWindowFocus, mWindowAttributes);
+
+ if (!fakeFocus) {
+ mImeFocusController.onPreWindowFocus(hasWindowFocus, mWindowAttributes);
+ }
if (mView != null) {
mAttachInfo.mKeyDispatchState.reset();
@@ -4363,18 +4393,22 @@
}
}
- private void reportDrawFinished(int seqId) {
+ private void reportDrawFinished(@Nullable Transaction t, int seqId) {
if (DEBUG_BLAST) {
- Log.d(mTag, "reportDrawFinished " + Debug.getCallers(5));
+ Log.d(mTag, "reportDrawFinished");
}
try {
- mWindowSession.finishDrawing(mWindow, mSurfaceChangedTransaction, seqId);
+ mWindowSession.finishDrawing(mWindow, t, seqId);
} catch (RemoteException e) {
Log.e(mTag, "Unable to report draw finished", e);
- mSurfaceChangedTransaction.apply();
+ if (t != null) {
+ t.apply();
+ }
} finally {
- mSurfaceChangedTransaction.clear();
+ if (t != null) {
+ t.clear();
+ }
}
}
@@ -6328,7 +6362,7 @@
// view tree or IME, and invoke the appropriate {@link OnBackInvokedCallback}.
if (isBack(event)
&& mContext != null
- && WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(mContext)) {
+ && mOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled()) {
OnBackInvokedCallback topCallback =
getOnBackInvokedDispatcher().getTopCallback();
if (event.getAction() == KeyEvent.ACTION_UP) {
@@ -11078,7 +11112,7 @@
@Nullable public SurfaceControl.Transaction buildReparentTransaction(
@NonNull SurfaceControl child) {
if (mSurfaceControl.isValid()) {
- return new SurfaceControl.Transaction().reparent(child, mSurfaceControl);
+ return new SurfaceControl.Transaction().reparent(child, getBoundsLayer());
}
return null;
}
@@ -11296,13 +11330,28 @@
@Override
public SurfaceSyncGroup getOrCreateSurfaceSyncGroup() {
+ boolean newSyncGroup = false;
if (mActiveSurfaceSyncGroup == null) {
- mActiveSurfaceSyncGroup = new SurfaceSyncGroup();
+ mActiveSurfaceSyncGroup = new SurfaceSyncGroup(mTag);
updateSyncInProgressCount(mActiveSurfaceSyncGroup);
if (!mIsInTraversal && !mTraversalScheduled) {
scheduleTraversals();
}
+ newSyncGroup = true;
}
+
+ Trace.instant(Trace.TRACE_TAG_VIEW,
+ "getOrCreateSurfaceSyncGroup isNew=" + newSyncGroup + " " + mTag);
+
+ if (DEBUG_BLAST) {
+ if (newSyncGroup) {
+ Log.d(mTag, "Creating new active sync group " + mActiveSurfaceSyncGroup.getName());
+ } else {
+ Log.d(mTag, "Return already created active sync group "
+ + mActiveSurfaceSyncGroup.getName());
+ }
+ }
+
return mActiveSurfaceSyncGroup;
};
@@ -11334,4 +11383,14 @@
}
mActiveSurfaceSyncGroup.addToSync(syncable, false /* parentSyncGroupMerge */);
}
+
+ @Override
+ public void setChildBoundingInsets(@NonNull Rect insets) {
+ if (insets.left < 0 || insets.top < 0 || insets.right < 0 || insets.bottom < 0) {
+ throw new IllegalArgumentException("Negative insets passed to setChildBoundingInsets.");
+ }
+ mChildBoundingInsets.set(insets);
+ mChildBoundingInsetsChanged = true;
+ scheduleTraversals();
+ }
}
diff --git a/core/java/android/view/inputmethod/HandwritingGesture.java b/core/java/android/view/inputmethod/HandwritingGesture.java
index 07b1e1f..2516269 100644
--- a/core/java/android/view/inputmethod/HandwritingGesture.java
+++ b/core/java/android/view/inputmethod/HandwritingGesture.java
@@ -159,7 +159,7 @@
* @hide
*/
@TestApi
- public @GestureType int getGestureType() {
+ public final @GestureType int getGestureType() {
return mType;
}
@@ -173,7 +173,7 @@
* example 2: join can fail if the gesture is drawn over text but there is no whitespace.
*/
@Nullable
- public String getFallbackText() {
+ public final String getFallbackText() {
return mFallbackText;
}
}
diff --git a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
index 6eae63a..f08f61f 100644
--- a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
+++ b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
@@ -40,6 +40,7 @@
import com.android.internal.inputmethod.SoftInputShowHideReason;
import com.android.internal.inputmethod.StartInputFlags;
import com.android.internal.inputmethod.StartInputReason;
+import com.android.internal.view.IImeTracker;
import com.android.internal.view.IInputMethodManager;
import java.util.ArrayList;
@@ -61,6 +62,9 @@
@Nullable
private static volatile IInputMethodManager sServiceCache = null;
+ @Nullable
+ private static volatile IImeTracker sTrackerServiceCache = null;
+
/**
* @return {@code true} if {@link IInputMethodManager} is available.
*/
@@ -527,4 +531,137 @@
throw e.rethrowFromSystemServer();
}
}
+
+ @AnyThread
+ @Nullable
+ static IBinder onRequestShow(int uid, @ImeTracker.Origin int origin,
+ @SoftInputShowHideReason int reason) {
+ final IImeTracker service = getImeTrackerService();
+ if (service == null) {
+ return null;
+ }
+ try {
+ return service.onRequestShow(uid, origin, reason);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @AnyThread
+ @Nullable
+ static IBinder onRequestHide(int uid, @ImeTracker.Origin int origin,
+ @SoftInputShowHideReason int reason) {
+ final IImeTracker service = getImeTrackerService();
+ if (service == null) {
+ return null;
+ }
+ try {
+ return service.onRequestHide(uid, origin, reason);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @AnyThread
+ static void onProgress(@NonNull IBinder statsToken, @ImeTracker.Phase int phase) {
+ final IImeTracker service = getImeTrackerService();
+ if (service == null) {
+ return;
+ }
+ try {
+ service.onProgress(statsToken, phase);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @AnyThread
+ static void onFailed(@NonNull IBinder statsToken, @ImeTracker.Phase int phase) {
+ final IImeTracker service = getImeTrackerService();
+ if (service == null) {
+ return;
+ }
+ try {
+ service.onFailed(statsToken, phase);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @AnyThread
+ static void onCancelled(@NonNull IBinder statsToken, @ImeTracker.Phase int phase) {
+ final IImeTracker service = getImeTrackerService();
+ if (service == null) {
+ return;
+ }
+ try {
+ service.onCancelled(statsToken, phase);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @AnyThread
+ static void onShown(@NonNull IBinder statsToken) {
+ final IImeTracker service = getImeTrackerService();
+ if (service == null) {
+ return;
+ }
+ try {
+ service.onShown(statsToken);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @AnyThread
+ static void onHidden(@NonNull IBinder statsToken) {
+ final IImeTracker service = getImeTrackerService();
+ if (service == null) {
+ return;
+ }
+ try {
+ service.onHidden(statsToken);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @AnyThread
+ @RequiresPermission(Manifest.permission.TEST_INPUT_METHOD)
+ static boolean hasPendingImeVisibilityRequests() {
+ final var service = getImeTrackerService();
+ if (service == null) {
+ return true;
+ }
+ try {
+ return service.hasPendingImeVisibilityRequests();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @AnyThread
+ @Nullable
+ private static IImeTracker getImeTrackerService() {
+ var trackerService = sTrackerServiceCache;
+ if (trackerService == null) {
+ final var service = getService();
+ if (service == null) {
+ return null;
+ }
+
+ try {
+ trackerService = service.getImeTrackerService();
+ if (trackerService == null) {
+ return null;
+ }
+
+ sTrackerServiceCache = trackerService;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return trackerService;
+ }
}
diff --git a/core/java/android/view/inputmethod/ImeTracker.java b/core/java/android/view/inputmethod/ImeTracker.java
index 927d769..9ed5c29 100644
--- a/core/java/android/view/inputmethod/ImeTracker.java
+++ b/core/java/android/view/inputmethod/ImeTracker.java
@@ -16,9 +16,6 @@
package android.view.inputmethod;
-import static android.view.inputmethod.ImeTracker.Debug.originToString;
-import static android.view.inputmethod.ImeTracker.Debug.phaseToString;
-
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -46,6 +43,53 @@
String TAG = "ImeTracker";
+ /** The debug flag for IME visibility event log. */
+ // TODO(b/239501597) : Have a system property to control this flag.
+ boolean DEBUG_IME_VISIBILITY = false;
+
+ /** The message to indicate if there is no valid {@link Token}. */
+ String TOKEN_NONE = "TOKEN_NONE";
+
+ /** The type of the IME request. */
+ @IntDef(prefix = { "TYPE_" }, value = {
+ TYPE_SHOW,
+ TYPE_HIDE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface Type {}
+
+ /** IME show request type. */
+ int TYPE_SHOW = ImeProtoEnums.TYPE_SHOW;
+
+ /** IME hide request type. */
+ int TYPE_HIDE = ImeProtoEnums.TYPE_HIDE;
+
+ /** The status of the IME request. */
+ @IntDef(prefix = { "STATUS_" }, value = {
+ STATUS_RUN,
+ STATUS_CANCEL,
+ STATUS_FAIL,
+ STATUS_SUCCESS,
+ STATUS_TIMEOUT
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface Status {}
+
+ /** IME request running. */
+ int STATUS_RUN = ImeProtoEnums.STATUS_RUN;
+
+ /** IME request cancelled. */
+ int STATUS_CANCEL = ImeProtoEnums.STATUS_CANCEL;
+
+ /** IME request failed. */
+ int STATUS_FAIL = ImeProtoEnums.STATUS_FAIL;
+
+ /** IME request succeeded. */
+ int STATUS_SUCCESS = ImeProtoEnums.STATUS_SUCCESS;
+
+ /** IME request timed out. */
+ int STATUS_TIMEOUT = ImeProtoEnums.STATUS_TIMEOUT;
+
/**
* The origin of the IME request
*
@@ -61,25 +105,17 @@
@Retention(RetentionPolicy.SOURCE)
@interface Origin {}
- /**
- * The IME show request originated in the client.
- */
- int ORIGIN_CLIENT_SHOW_SOFT_INPUT = 0;
+ /** The IME show request originated in the client. */
+ int ORIGIN_CLIENT_SHOW_SOFT_INPUT = ImeProtoEnums.ORIGIN_CLIENT_SHOW_SOFT_INPUT;
- /**
- * The IME hide request originated in the client.
- */
- int ORIGIN_CLIENT_HIDE_SOFT_INPUT = 1;
+ /** The IME hide request originated in the client. */
+ int ORIGIN_CLIENT_HIDE_SOFT_INPUT = ImeProtoEnums.ORIGIN_CLIENT_HIDE_SOFT_INPUT;
- /**
- * The IME show request originated in the server.
- */
- int ORIGIN_SERVER_START_INPUT = 2;
+ /** The IME show request originated in the server. */
+ int ORIGIN_SERVER_START_INPUT = ImeProtoEnums.ORIGIN_SERVER_START_INPUT;
- /**
- * The IME hide request originated in the server.
- */
- int ORIGIN_SERVER_HIDE_INPUT = 3;
+ /** The IME hide request originated in the server. */
+ int ORIGIN_SERVER_HIDE_INPUT = ImeProtoEnums.ORIGIN_SERVER_HIDE_INPUT;
/**
* The current phase of the IME request.
@@ -88,6 +124,7 @@
* where the phase is (i.e. {@code PHASE_SERVER_...} occurs in the server).
*/
@IntDef(prefix = { "PHASE_" }, value = {
+ PHASE_NOT_SET,
PHASE_CLIENT_VIEW_SERVED,
PHASE_SERVER_CLIENT_KNOWN,
PHASE_SERVER_CLIENT_FOCUSED,
@@ -121,6 +158,11 @@
PHASE_CLIENT_HANDLE_HIDE_INSETS,
PHASE_CLIENT_APPLY_ANIMATION,
PHASE_CLIENT_CONTROL_ANIMATION,
+ PHASE_CLIENT_DISABLED_USER_ANIMATION,
+ PHASE_CLIENT_COLLECT_SOURCE_CONTROLS,
+ PHASE_CLIENT_INSETS_CONSUMER_REQUEST_SHOW,
+ PHASE_CLIENT_REQUEST_IME_SHOW,
+ PHASE_CLIENT_INSETS_CONSUMER_NOTIFY_HIDDEN,
PHASE_CLIENT_ANIMATION_RUNNING,
PHASE_CLIENT_ANIMATION_CANCEL,
PHASE_CLIENT_ANIMATION_FINISHED_SHOW,
@@ -129,135 +171,172 @@
@Retention(RetentionPolicy.SOURCE)
@interface Phase {}
+ int PHASE_NOT_SET = ImeProtoEnums.PHASE_NOT_SET;
+
/** The view that requested the IME has been served by the IMM. */
- int PHASE_CLIENT_VIEW_SERVED = 0;
+ int PHASE_CLIENT_VIEW_SERVED = ImeProtoEnums.PHASE_CLIENT_VIEW_SERVED;
/** The IME client that requested the IME has window manager focus. */
- int PHASE_SERVER_CLIENT_KNOWN = 1;
+ int PHASE_SERVER_CLIENT_KNOWN = ImeProtoEnums.PHASE_SERVER_CLIENT_KNOWN;
/** The IME client that requested the IME has IME focus. */
- int PHASE_SERVER_CLIENT_FOCUSED = 2;
+ int PHASE_SERVER_CLIENT_FOCUSED = ImeProtoEnums.PHASE_SERVER_CLIENT_FOCUSED;
/** The IME request complies with the current accessibility settings. */
- int PHASE_SERVER_ACCESSIBILITY = 3;
+ int PHASE_SERVER_ACCESSIBILITY = ImeProtoEnums.PHASE_SERVER_ACCESSIBILITY;
/** The server is ready to run third party code. */
- int PHASE_SERVER_SYSTEM_READY = 4;
+ int PHASE_SERVER_SYSTEM_READY = ImeProtoEnums.PHASE_SERVER_SYSTEM_READY;
/** Checked the implicit hide request against any explicit show requests. */
- int PHASE_SERVER_HIDE_IMPLICIT = 5;
+ int PHASE_SERVER_HIDE_IMPLICIT = ImeProtoEnums.PHASE_SERVER_HIDE_IMPLICIT;
/** Checked the not-always hide request against any forced show requests. */
- int PHASE_SERVER_HIDE_NOT_ALWAYS = 6;
+ int PHASE_SERVER_HIDE_NOT_ALWAYS = ImeProtoEnums.PHASE_SERVER_HIDE_NOT_ALWAYS;
/** The server is waiting for a connection to the IME. */
- int PHASE_SERVER_WAIT_IME = 7;
+ int PHASE_SERVER_WAIT_IME = ImeProtoEnums.PHASE_SERVER_WAIT_IME;
/** The server has a connection to the IME. */
- int PHASE_SERVER_HAS_IME = 8;
+ int PHASE_SERVER_HAS_IME = ImeProtoEnums.PHASE_SERVER_HAS_IME;
/** The server decided the IME should be hidden. */
- int PHASE_SERVER_SHOULD_HIDE = 9;
+ int PHASE_SERVER_SHOULD_HIDE = ImeProtoEnums.PHASE_SERVER_SHOULD_HIDE;
/** Reached the IME wrapper. */
- int PHASE_IME_WRAPPER = 10;
+ int PHASE_IME_WRAPPER = ImeProtoEnums.PHASE_IME_WRAPPER;
/** Dispatched from the IME wrapper to the IME. */
- int PHASE_IME_WRAPPER_DISPATCH = 11;
+ int PHASE_IME_WRAPPER_DISPATCH = ImeProtoEnums.PHASE_IME_WRAPPER_DISPATCH;
/** Reached the IME' showSoftInput method. */
- int PHASE_IME_SHOW_SOFT_INPUT = 12;
+ int PHASE_IME_SHOW_SOFT_INPUT = ImeProtoEnums.PHASE_IME_SHOW_SOFT_INPUT;
/** Reached the IME' hideSoftInput method. */
- int PHASE_IME_HIDE_SOFT_INPUT = 13;
+ int PHASE_IME_HIDE_SOFT_INPUT = ImeProtoEnums.PHASE_IME_HIDE_SOFT_INPUT;
/** The server decided the IME should be shown. */
- int PHASE_IME_ON_SHOW_SOFT_INPUT_TRUE = 14;
+ int PHASE_IME_ON_SHOW_SOFT_INPUT_TRUE = ImeProtoEnums.PHASE_IME_ON_SHOW_SOFT_INPUT_TRUE;
/** Requested applying the IME visibility in the insets source consumer. */
- int PHASE_IME_APPLY_VISIBILITY_INSETS_CONSUMER = 15;
+ int PHASE_IME_APPLY_VISIBILITY_INSETS_CONSUMER =
+ ImeProtoEnums.PHASE_IME_APPLY_VISIBILITY_INSETS_CONSUMER;
/** Applied the IME visibility. */
- int PHASE_SERVER_APPLY_IME_VISIBILITY = 16;
+ int PHASE_SERVER_APPLY_IME_VISIBILITY = ImeProtoEnums.PHASE_SERVER_APPLY_IME_VISIBILITY;
/** Created the show IME runner. */
- int PHASE_WM_SHOW_IME_RUNNER = 17;
+ int PHASE_WM_SHOW_IME_RUNNER = ImeProtoEnums.PHASE_WM_SHOW_IME_RUNNER;
/** Ready to show IME. */
- int PHASE_WM_SHOW_IME_READY = 18;
+ int PHASE_WM_SHOW_IME_READY = ImeProtoEnums.PHASE_WM_SHOW_IME_READY;
/** The Window Manager has a connection to the IME insets control target. */
- int PHASE_WM_HAS_IME_INSETS_CONTROL_TARGET = 19;
+ int PHASE_WM_HAS_IME_INSETS_CONTROL_TARGET =
+ ImeProtoEnums.PHASE_WM_HAS_IME_INSETS_CONTROL_TARGET;
/** Reached the window insets control target's show insets method. */
- int PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_SHOW_INSETS = 20;
+ int PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_SHOW_INSETS =
+ ImeProtoEnums.PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_SHOW_INSETS;
/** Reached the window insets control target's hide insets method. */
- int PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_HIDE_INSETS = 21;
+ int PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_HIDE_INSETS =
+ ImeProtoEnums.PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_HIDE_INSETS;
/** Reached the remote insets control target's show insets method. */
- int PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_SHOW_INSETS = 22;
+ int PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_SHOW_INSETS =
+ ImeProtoEnums.PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_SHOW_INSETS;
/** Reached the remote insets control target's hide insets method. */
- int PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_HIDE_INSETS = 23;
+ int PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_HIDE_INSETS =
+ ImeProtoEnums.PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_HIDE_INSETS;
/** Reached the remote insets controller. */
- int PHASE_WM_REMOTE_INSETS_CONTROLLER = 24;
+ int PHASE_WM_REMOTE_INSETS_CONTROLLER = ImeProtoEnums.PHASE_WM_REMOTE_INSETS_CONTROLLER;
/** Created the IME window insets show animation. */
- int PHASE_WM_ANIMATION_CREATE = 25;
+ int PHASE_WM_ANIMATION_CREATE = ImeProtoEnums.PHASE_WM_ANIMATION_CREATE;
/** Started the IME window insets show animation. */
- int PHASE_WM_ANIMATION_RUNNING = 26;
+ int PHASE_WM_ANIMATION_RUNNING = ImeProtoEnums.PHASE_WM_ANIMATION_RUNNING;
/** Reached the client's show insets method. */
- int PHASE_CLIENT_SHOW_INSETS = 27;
+ int PHASE_CLIENT_SHOW_INSETS = ImeProtoEnums.PHASE_CLIENT_SHOW_INSETS;
/** Reached the client's hide insets method. */
- int PHASE_CLIENT_HIDE_INSETS = 28;
+ int PHASE_CLIENT_HIDE_INSETS = ImeProtoEnums.PHASE_CLIENT_HIDE_INSETS;
/** Handling the IME window insets show request. */
- int PHASE_CLIENT_HANDLE_SHOW_INSETS = 29;
+ int PHASE_CLIENT_HANDLE_SHOW_INSETS = ImeProtoEnums.PHASE_CLIENT_HANDLE_SHOW_INSETS;
/** Handling the IME window insets hide request. */
- int PHASE_CLIENT_HANDLE_HIDE_INSETS = 30;
+ int PHASE_CLIENT_HANDLE_HIDE_INSETS = ImeProtoEnums.PHASE_CLIENT_HANDLE_HIDE_INSETS;
/** Applied the IME window insets show animation. */
- int PHASE_CLIENT_APPLY_ANIMATION = 31;
+ int PHASE_CLIENT_APPLY_ANIMATION = ImeProtoEnums.PHASE_CLIENT_APPLY_ANIMATION;
/** Started the IME window insets show animation. */
- int PHASE_CLIENT_CONTROL_ANIMATION = 32;
+ int PHASE_CLIENT_CONTROL_ANIMATION = ImeProtoEnums.PHASE_CLIENT_CONTROL_ANIMATION;
+
+ /** Checked that the IME is controllable. */
+ int PHASE_CLIENT_DISABLED_USER_ANIMATION = ImeProtoEnums.PHASE_CLIENT_DISABLED_USER_ANIMATION;
+
+ /** Collecting insets source controls. */
+ int PHASE_CLIENT_COLLECT_SOURCE_CONTROLS = ImeProtoEnums.PHASE_CLIENT_COLLECT_SOURCE_CONTROLS;
+
+ /** Reached the insets source consumer's show request method. */
+ int PHASE_CLIENT_INSETS_CONSUMER_REQUEST_SHOW =
+ ImeProtoEnums.PHASE_CLIENT_INSETS_CONSUMER_REQUEST_SHOW;
+
+ /** Reached input method manager's request IME show method. */
+ int PHASE_CLIENT_REQUEST_IME_SHOW = ImeProtoEnums.PHASE_CLIENT_REQUEST_IME_SHOW;
+
+ /** Reached the insets source consumer's notify hidden method. */
+ int PHASE_CLIENT_INSETS_CONSUMER_NOTIFY_HIDDEN =
+ ImeProtoEnums.PHASE_CLIENT_INSETS_CONSUMER_NOTIFY_HIDDEN;
/** Queued the IME window insets show animation. */
- int PHASE_CLIENT_ANIMATION_RUNNING = 33;
+ int PHASE_CLIENT_ANIMATION_RUNNING = ImeProtoEnums.PHASE_CLIENT_ANIMATION_RUNNING;
/** Cancelled the IME window insets show animation. */
- int PHASE_CLIENT_ANIMATION_CANCEL = 34;
+ int PHASE_CLIENT_ANIMATION_CANCEL = ImeProtoEnums.PHASE_CLIENT_ANIMATION_CANCEL;
/** Finished the IME window insets show animation. */
- int PHASE_CLIENT_ANIMATION_FINISHED_SHOW = 35;
+ int PHASE_CLIENT_ANIMATION_FINISHED_SHOW = ImeProtoEnums.PHASE_CLIENT_ANIMATION_FINISHED_SHOW;
/** Finished the IME window insets hide animation. */
- int PHASE_CLIENT_ANIMATION_FINISHED_HIDE = 36;
+ int PHASE_CLIENT_ANIMATION_FINISHED_HIDE = ImeProtoEnums.PHASE_CLIENT_ANIMATION_FINISHED_HIDE;
/**
- * Called when an IME show request is created.
+ * Creates an IME show request tracking token.
*
- * @param token the token tracking the current IME show request or {@code null} otherwise.
+ * @param component the component name where the IME show request was created,
+ * or {@code null} otherwise
+ * (defaulting to {@link ActivityThread#currentProcessName()}).
+ * @param uid the uid of the client that requested the IME.
* @param origin the origin of the IME show request.
* @param reason the reason why the IME show request was created.
+ *
+ * @return An IME tracking token.
*/
- void onRequestShow(@Nullable Token token, @Origin int origin,
+ @NonNull
+ Token onRequestShow(@Nullable String component, int uid, @Origin int origin,
@SoftInputShowHideReason int reason);
/**
- * Called when an IME hide request is created.
+ * Creates an IME hide request tracking token.
*
- * @param token the token tracking the current IME hide request or {@code null} otherwise.
+ * @param component the component name where the IME hide request was created,
+ * or {@code null} otherwise
+ * (defaulting to {@link ActivityThread#currentProcessName()}).
+ * @param uid the uid of the client that requested the IME.
* @param origin the origin of the IME hide request.
* @param reason the reason why the IME hide request was created.
+ *
+ * @return An IME tracking token.
*/
- void onRequestHide(@Nullable Token token, @Origin int origin,
+ @NonNull
+ Token onRequestHide(@Nullable String component, int uid, @Origin int origin,
@SoftInputShowHideReason int reason);
/**
@@ -313,112 +392,122 @@
*/
@NonNull
static ImeTracker get() {
- return SystemProperties.getBoolean("persist.debug.imetracker", false)
- ? LOGGER
- : NOOP_LOGGER;
+ return LOGGER;
}
/** The singleton IME tracker instance. */
+ @NonNull
ImeTracker LOGGER = new ImeTracker() {
+ {
+ // Set logging flag initial value.
+ mLogProgress = SystemProperties.getBoolean("persist.debug.imetracker", false);
+ // Update logging flag dynamically.
+ SystemProperties.addChangeCallback(() ->
+ mLogProgress =
+ SystemProperties.getBoolean("persist.debug.imetracker", false));
+ }
+
+ /** Whether progress should be logged. */
+ private boolean mLogProgress;
+
+ @NonNull
@Override
- public void onRequestShow(@Nullable Token token, int origin,
+ public Token onRequestShow(@Nullable String component, int uid, @Origin int origin,
@SoftInputShowHideReason int reason) {
- if (token == null) return;
- Log.i(TAG, token.mTag + ": onRequestShow at " + originToString(origin)
+ IBinder binder = IInputMethodManagerGlobalInvoker.onRequestShow(uid, origin, reason);
+ if (binder == null) binder = new Binder();
+
+ final Token token = Token.build(binder, component);
+
+ Log.i(TAG, token.mTag + ": onRequestShow at " + Debug.originToString(origin)
+ " reason " + InputMethodDebug.softInputDisplayReasonToString(reason));
+
+ return token;
}
+ @NonNull
@Override
- public void onRequestHide(@Nullable Token token, int origin,
+ public Token onRequestHide(@Nullable String component, int uid, @Origin int origin,
@SoftInputShowHideReason int reason) {
- if (token == null) return;
- Log.i(TAG, token.mTag + ": onRequestHide at " + originToString(origin)
+ IBinder binder = IInputMethodManagerGlobalInvoker.onRequestHide(uid, origin, reason);
+ if (binder == null) binder = new Binder();
+
+ final Token token = Token.build(binder, component);
+
+ Log.i(TAG, token.mTag + ": onRequestHide at " + Debug.originToString(origin)
+ " reason " + InputMethodDebug.softInputDisplayReasonToString(reason));
+
+ return token;
}
@Override
- public void onProgress(@Nullable Token token, int phase) {
+ public void onProgress(@Nullable Token token, @Phase int phase) {
if (token == null) return;
- Log.i(TAG, token.mTag + ": onProgress at " + phaseToString(phase));
+ IInputMethodManagerGlobalInvoker.onProgress(token.mBinder, phase);
+
+ if (mLogProgress) {
+ Log.i(TAG, token.mTag + ": onProgress at " + Debug.phaseToString(phase));
+ }
}
@Override
- public void onFailed(@Nullable Token token, int phase) {
+ public void onFailed(@Nullable Token token, @Phase int phase) {
if (token == null) return;
- Log.i(TAG, token.mTag + ": onFailed at " + phaseToString(phase));
+ IInputMethodManagerGlobalInvoker.onFailed(token.mBinder, phase);
+
+ Log.i(TAG, token.mTag + ": onFailed at " + Debug.phaseToString(phase));
}
@Override
- public void onTodo(@Nullable Token token, int phase) {
+ public void onTodo(@Nullable Token token, @Phase int phase) {
if (token == null) return;
- Log.i(TAG, token.mTag + ": onTodo at " + phaseToString(phase));
+ Log.i(TAG, token.mTag + ": onTodo at " + Debug.phaseToString(phase));
}
@Override
- public void onCancelled(@Nullable Token token, int phase) {
+ public void onCancelled(@Nullable Token token, @Phase int phase) {
if (token == null) return;
- Log.i(TAG, token.mTag + ": onCancelled at " + phaseToString(phase));
+ IInputMethodManagerGlobalInvoker.onCancelled(token.mBinder, phase);
+
+ Log.i(TAG, token.mTag + ": onCancelled at " + Debug.phaseToString(phase));
}
@Override
public void onShown(@Nullable Token token) {
if (token == null) return;
+ IInputMethodManagerGlobalInvoker.onShown(token.mBinder);
+
Log.i(TAG, token.mTag + ": onShown");
}
@Override
public void onHidden(@Nullable Token token) {
if (token == null) return;
+ IInputMethodManagerGlobalInvoker.onHidden(token.mBinder);
+
Log.i(TAG, token.mTag + ": onHidden");
}
};
- /** The singleton no-op IME tracker instance. */
- ImeTracker NOOP_LOGGER = new ImeTracker() {
-
- @Override
- public void onRequestShow(@Nullable Token token, int origin,
- @SoftInputShowHideReason int reason) {}
-
- @Override
- public void onRequestHide(@Nullable Token token, int origin,
- @SoftInputShowHideReason int reason) {}
-
- @Override
- public void onProgress(@Nullable Token token, int phase) {}
-
- @Override
- public void onFailed(@Nullable Token token, int phase) {}
-
- @Override
- public void onTodo(@Nullable Token token, int phase) {}
-
- @Override
- public void onCancelled(@Nullable Token token, int phase) {}
-
- @Override
- public void onShown(@Nullable Token token) {}
-
- @Override
- public void onHidden(@Nullable Token token) {}
- };
-
/** A token that tracks the progress of an IME request. */
class Token implements Parcelable {
- private final IBinder mBinder;
+ @NonNull
+ public final IBinder mBinder;
+
+ @NonNull
private final String mTag;
- public Token() {
- this(ActivityThread.currentProcessName());
+ @NonNull
+ private static Token build(@NonNull IBinder binder, @Nullable String component) {
+ if (component == null) component = ActivityThread.currentProcessName();
+ final String tag = component + ":" + Integer.toHexString((new Random().nextInt()));
+
+ return new Token(binder, tag);
}
- public Token(String component) {
- this(new Binder(), component + ":" + Integer.toHexString((new Random().nextInt())));
- }
-
- private Token(IBinder binder, String tag) {
+ private Token(@NonNull IBinder binder, @NonNull String tag) {
mBinder = binder;
mTag = tag;
}
@@ -443,10 +532,11 @@
@NonNull
public static final Creator<Token> CREATOR = new Creator<>() {
+ @NonNull
@Override
public Token createFromParcel(Parcel source) {
- IBinder binder = source.readStrongBinder();
- String tag = source.readString8();
+ final IBinder binder = source.readStrongBinder();
+ final String tag = source.readString8();
return new Token(binder, tag);
}
@@ -458,22 +548,34 @@
}
/**
- * Utilities for mapping phases and origins IntDef values to their names.
+ * Utilities for mapping IntDef values to their names.
*
* Note: This is held in a separate class so that it only gets initialized when actually needed.
*/
class Debug {
+ private static final Map<Integer, String> sTypes =
+ getFieldMapping(ImeTracker.class, "TYPE_");
+ private static final Map<Integer, String> sStatus =
+ getFieldMapping(ImeTracker.class, "STATUS_");
private static final Map<Integer, String> sOrigins =
getFieldMapping(ImeTracker.class, "ORIGIN_");
private static final Map<Integer, String> sPhases =
getFieldMapping(ImeTracker.class, "PHASE_");
- public static String originToString(int origin) {
+ public static String typeToString(@Type int type) {
+ return sTypes.getOrDefault(type, "TYPE_" + type);
+ }
+
+ public static String statusToString(@Status int status) {
+ return sStatus.getOrDefault(status, "STATUS_" + status);
+ }
+
+ public static String originToString(@Origin int origin) {
return sOrigins.getOrDefault(origin, "ORIGIN_" + origin);
}
- public static String phaseToString(int phase) {
+ public static String phaseToString(@Phase int phase) {
return sPhases.getOrDefault(phase, "PHASE_" + phase);
}
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index ee31fd5..e9f0d29 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -2002,14 +2002,16 @@
* {@link #RESULT_HIDDEN}.
*/
public boolean showSoftInput(View view, int flags, ResultReceiver resultReceiver) {
- return showSoftInput(view, flags, resultReceiver, SoftInputShowHideReason.SHOW_SOFT_INPUT);
+ return showSoftInput(view, null /* statsToken */, flags, resultReceiver,
+ SoftInputShowHideReason.SHOW_SOFT_INPUT);
}
- private boolean showSoftInput(View view, int flags, ResultReceiver resultReceiver,
- @SoftInputShowHideReason int reason) {
- final ImeTracker.Token statsToken = new ImeTracker.Token();
- ImeTracker.get().onRequestShow(statsToken, ImeTracker.ORIGIN_CLIENT_SHOW_SOFT_INPUT,
- reason);
+ private boolean showSoftInput(View view, @Nullable ImeTracker.Token statsToken, int flags,
+ ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+ if (statsToken == null) {
+ statsToken = ImeTracker.get().onRequestShow(null /* component */,
+ Process.myUid(), ImeTracker.ORIGIN_CLIENT_SHOW_SOFT_INPUT, reason);
+ }
ImeTracing.getInstance().triggerClientDump("InputMethodManager#showSoftInput", this,
null /* icProto */);
@@ -2057,8 +2059,8 @@
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123768499)
public void showSoftInputUnchecked(int flags, ResultReceiver resultReceiver) {
synchronized (mH) {
- final ImeTracker.Token statsToken = new ImeTracker.Token();
- ImeTracker.get().onRequestShow(statsToken, ImeTracker.ORIGIN_CLIENT_SHOW_SOFT_INPUT,
+ final ImeTracker.Token statsToken = ImeTracker.get().onRequestShow(null /* component */,
+ Process.myUid(), ImeTracker.ORIGIN_CLIENT_SHOW_SOFT_INPUT,
SoftInputShowHideReason.SHOW_SOFT_INPUT);
Log.w(TAG, "showSoftInputUnchecked() is a hidden method, which will be"
@@ -2148,9 +2150,8 @@
private boolean hideSoftInputFromWindow(IBinder windowToken, int flags,
ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
- final ImeTracker.Token statsToken = new ImeTracker.Token();
- ImeTracker.get().onRequestHide(statsToken, ImeTracker.ORIGIN_CLIENT_HIDE_SOFT_INPUT,
- reason);
+ final ImeTracker.Token statsToken = ImeTracker.get().onRequestHide(null /* component */,
+ Process.myUid(), ImeTracker.ORIGIN_CLIENT_HIDE_SOFT_INPUT, reason);
ImeTracing.getInstance().triggerClientDump("InputMethodManager#hideSoftInputFromWindow",
this, null /* icProto */);
@@ -2283,7 +2284,7 @@
hideSoftInputFromWindow(view.getWindowToken(), hideFlags, null,
SoftInputShowHideReason.HIDE_TOGGLE_SOFT_INPUT);
} else {
- showSoftInput(view, showFlags, null,
+ showSoftInput(view, null /* statsToken */, showFlags, null /* resultReceiver */,
SoftInputShowHideReason.SHOW_TOGGLE_SOFT_INPUT);
}
}
@@ -2460,6 +2461,7 @@
final EditorInfo editorInfo = connectionPair.second;
final Handler icHandler;
InputBindResult res = null;
+ final boolean hasServedView;
synchronized (mH) {
// Now that we are locked again, validate that our state hasn't
// changed.
@@ -2591,6 +2593,7 @@
switch (res.result) {
case InputBindResult.ResultCode.ERROR_NOT_IME_TARGET_WINDOW:
mRestartOnNextWindowFocus = true;
+ mServedView = null;
break;
}
if (mCompletions != null) {
@@ -2598,10 +2601,11 @@
mCurBindState.mImeSession.displayCompletions(mCompletions);
}
}
+ hasServedView = mServedView != null;
}
// Notify the app that the InputConnection is initialized and ready for use.
- if (ic != null && res != null && res.method != null) {
+ if (ic != null && res != null && res.method != null && hasServedView) {
if (DEBUG) {
Log.v(TAG, "Calling View.onInputConnectionOpened: view= " + view
+ ", ic=" + ic + ", editorInfo=" + editorInfo + ", handler=" + icHandler);
@@ -2793,8 +2797,8 @@
@UnsupportedAppUsage
void closeCurrentInput() {
- final ImeTracker.Token statsToken = new ImeTracker.Token();
- ImeTracker.get().onRequestHide(statsToken, ImeTracker.ORIGIN_CLIENT_HIDE_SOFT_INPUT,
+ final ImeTracker.Token statsToken = ImeTracker.get().onRequestHide(null /* component */,
+ Process.myUid(), ImeTracker.ORIGIN_CLIENT_HIDE_SOFT_INPUT,
SoftInputShowHideReason.HIDE_SOFT_INPUT);
synchronized (mH) {
@@ -2853,18 +2857,23 @@
*
* @param windowToken the window from which this request originates. If this doesn't match the
* currently served view, the request is ignored and returns {@code false}.
+ * @param statsToken the token tracking the current IME show request or {@code null} otherwise.
*
* @return {@code true} if IME can (eventually) be shown, {@code false} otherwise.
* @hide
*/
- public boolean requestImeShow(IBinder windowToken) {
+ public boolean requestImeShow(IBinder windowToken, @Nullable ImeTracker.Token statsToken) {
checkFocus();
synchronized (mH) {
final View servedView = getServedViewLocked();
if (servedView == null || servedView.getWindowToken() != windowToken) {
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_CLIENT_REQUEST_IME_SHOW);
return false;
}
- showSoftInput(servedView, 0 /* flags */, null /* resultReceiver */,
+
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_REQUEST_IME_SHOW);
+
+ showSoftInput(servedView, statsToken, 0 /* flags */, null /* resultReceiver */,
SoftInputShowHideReason.SHOW_SOFT_INPUT_BY_INSETS_API);
return true;
}
@@ -2875,12 +2884,15 @@
*
* @param windowToken the window from which this request originates. If this doesn't match the
* currently served view, the request is ignored.
+ * @param statsToken the token tracking the current IME show request or {@code null} otherwise.
* @hide
*/
- public void notifyImeHidden(IBinder windowToken) {
- final ImeTracker.Token statsToken = new ImeTracker.Token();
- ImeTracker.get().onRequestHide(statsToken, ImeTracker.ORIGIN_CLIENT_HIDE_SOFT_INPUT,
- SoftInputShowHideReason.HIDE_SOFT_INPUT_BY_INSETS_API);
+ public void notifyImeHidden(IBinder windowToken, @Nullable ImeTracker.Token statsToken) {
+ if (statsToken == null) {
+ statsToken = ImeTracker.get().onRequestHide(null /* component */,
+ Process.myUid(), ImeTracker.ORIGIN_CLIENT_HIDE_SOFT_INPUT,
+ SoftInputShowHideReason.HIDE_SOFT_INPUT_BY_INSETS_API);
+ }
ImeTracing.getInstance().triggerClientDump("InputMethodManager#notifyImeHidden", this,
null /* icProto */);
@@ -3546,6 +3558,18 @@
}
/**
+ * A test API for CTS to check whether there are any pending IME visibility requests.
+ *
+ * @return {@code true} iff there are pending IME visibility requests.
+ * @hide
+ */
+ @TestApi
+ @RequiresPermission(Manifest.permission.TEST_INPUT_METHOD)
+ public boolean hasPendingImeVisibilityRequests() {
+ return IInputMethodManagerGlobalInvoker.hasPendingImeVisibilityRequests();
+ }
+
+ /**
* Show the settings for enabling subtypes of the specified input method.
*
* @param imiId An input method, whose subtypes settings will be shown. If imiId is null,
diff --git a/core/java/android/view/inputmethod/TextAppearanceInfo.java b/core/java/android/view/inputmethod/TextAppearanceInfo.java
index 500c41c..05717dd 100644
--- a/core/java/android/view/inputmethod/TextAppearanceInfo.java
+++ b/core/java/android/view/inputmethod/TextAppearanceInfo.java
@@ -31,7 +31,10 @@
import android.os.LocaleList;
import android.os.Parcel;
import android.os.Parcelable;
+import android.text.Spanned;
+import android.text.TextPaint;
import android.text.method.TransformationMethod;
+import android.text.style.CharacterStyle;
import android.widget.TextView;
import java.util.Objects;
@@ -182,6 +185,70 @@
mLinkTextColor = builder.mLinkTextColor;
}
+ /**
+ * Creates a new instance of {@link TextAppearanceInfo} by extracting text appearance from the
+ * character before cursor in the target {@link TextView}.
+ * @param textView the target {@link TextView}.
+ * @return the new instance of {@link TextAppearanceInfo}.
+ * @hide
+ */
+ @NonNull
+ public static TextAppearanceInfo createFromTextView(@NonNull TextView textView) {
+ final int selectionStart = textView.getSelectionStart();
+ final CharSequence text = textView.getText();
+ TextPaint textPaint = new TextPaint();
+ textPaint.set(textView.getPaint()); // Copy from textView
+ if (text instanceof Spanned && text.length() > 0 && selectionStart > 0) {
+ // Extract the CharacterStyle spans that changes text appearance in the character before
+ // cursor.
+ Spanned spannedText = (Spanned) text;
+ int lastCh = selectionStart - 1;
+ CharacterStyle[] spans = spannedText.getSpans(lastCh, lastCh, CharacterStyle.class);
+ if (spans != null) {
+ for (CharacterStyle span: spans) {
+ // Exclude spans that end at lastCh
+ if (spannedText.getSpanStart(span) <= lastCh
+ && lastCh < spannedText.getSpanEnd(span)) {
+ span.updateDrawState(textPaint); // Override the TextPaint
+ }
+ }
+ }
+ }
+ Typeface typeface = textPaint.getTypeface();
+ String systemFontFamilyName = null;
+ int textWeight = FontStyle.FONT_WEIGHT_UNSPECIFIED;
+ int textStyle = Typeface.NORMAL;
+ if (typeface != null) {
+ systemFontFamilyName = typeface.getSystemFontFamilyName();
+ textWeight = typeface.getWeight();
+ textStyle = typeface.getStyle();
+ }
+ TextAppearanceInfo.Builder builder = new TextAppearanceInfo.Builder();
+ builder.setTextSize(textPaint.getTextSize())
+ .setTextLocales(textPaint.getTextLocales())
+ .setSystemFontFamilyName(systemFontFamilyName)
+ .setTextFontWeight(textWeight)
+ .setTextStyle(textStyle)
+ .setShadowDx(textPaint.getShadowLayerDx())
+ .setShadowDy(textPaint.getShadowLayerDy())
+ .setShadowRadius(textPaint.getShadowLayerRadius())
+ .setShadowColor(textPaint.getShadowLayerColor())
+ .setElegantTextHeight(textPaint.isElegantTextHeight())
+ .setLetterSpacing(textPaint.getLetterSpacing())
+ .setFontFeatureSettings(textPaint.getFontFeatureSettings())
+ .setFontVariationSettings(textPaint.getFontVariationSettings())
+ .setTextScaleX(textPaint.getTextScaleX())
+ .setTextColor(textPaint.getColor())
+ .setLinkTextColor(textPaint.linkColor)
+ .setAllCaps(textView.isAllCaps())
+ .setFallbackLineSpacing(textView.isFallbackLineSpacing())
+ .setLineBreakStyle(textView.getLineBreakStyle())
+ .setLineBreakWordStyle(textView.getLineBreakWordStyle())
+ .setHighlightTextColor(textView.getHighlightColor())
+ .setHintTextColor(textView.getCurrentHintTextColor());
+ return builder.build();
+ }
+
@Override
public int describeContents() {
return 0;
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index 0a3ea8a..b33afa5 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -21,7 +21,6 @@
import android.R;
import android.animation.ValueAnimator;
-import android.annotation.ColorInt;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -38,7 +37,6 @@
import android.content.UndoOwner;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
-import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
@@ -51,10 +49,8 @@
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.RenderNode;
-import android.graphics.Typeface;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
-import android.graphics.fonts.FontStyle;
import android.os.Build;
import android.os.Bundle;
import android.os.LocaleList;
@@ -137,7 +133,6 @@
import android.widget.TextView.OnEditorActionListener;
import android.window.OnBackInvokedCallback;
import android.window.OnBackInvokedDispatcher;
-import android.window.WindowOnBackInvokedDispatcher;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.inputmethod.EditableInputConnection;
@@ -774,8 +769,7 @@
}
ViewRootImpl viewRootImpl = getTextView().getViewRootImpl();
if (viewRootImpl != null
- && WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(
- viewRootImpl.mContext)) {
+ && viewRootImpl.getOnBackInvokedDispatcher().isOnBackInvokedCallbackEnabled()) {
viewRootImpl.getOnBackInvokedDispatcher()
.unregisterOnBackInvokedCallback(mBackCallback);
mBackCallbackRegistered = false;
@@ -788,8 +782,7 @@
}
ViewRootImpl viewRootImpl = mTextView.getViewRootImpl();
if (viewRootImpl != null
- && WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(
- viewRootImpl.mContext)) {
+ && viewRootImpl.getOnBackInvokedDispatcher().isOnBackInvokedCallbackEnabled()) {
viewRootImpl.getOnBackInvokedDispatcher().registerOnBackInvokedCallback(
OnBackInvokedDispatcher.PRIORITY_DEFAULT, mBackCallback);
mBackCallbackRegistered = true;
@@ -4774,41 +4767,7 @@
}
if (includeTextAppearance) {
- Typeface typeface = mTextView.getPaint().getTypeface();
- String systemFontFamilyName = null;
- int textFontWeight = FontStyle.FONT_WEIGHT_UNSPECIFIED;
- if (typeface != null) {
- systemFontFamilyName = typeface.getSystemFontFamilyName();
- textFontWeight = typeface.getWeight();
- }
- ColorStateList linkTextColors = mTextView.getLinkTextColors();
- @ColorInt int linkTextColor = linkTextColors != null
- ? linkTextColors.getDefaultColor() : 0;
-
- TextAppearanceInfo.Builder appearanceBuilder = new TextAppearanceInfo.Builder();
- appearanceBuilder.setTextSize(mTextView.getTextSize())
- .setTextLocales(mTextView.getTextLocales())
- .setSystemFontFamilyName(systemFontFamilyName)
- .setTextFontWeight(textFontWeight)
- .setTextStyle(mTextView.getTypefaceStyle())
- .setAllCaps(mTextView.isAllCaps())
- .setShadowDx(mTextView.getShadowDx())
- .setShadowDy(mTextView.getShadowDy())
- .setShadowRadius(mTextView.getShadowRadius())
- .setShadowColor(mTextView.getShadowColor())
- .setElegantTextHeight(mTextView.isElegantTextHeight())
- .setFallbackLineSpacing(mTextView.isFallbackLineSpacing())
- .setLetterSpacing(mTextView.getLetterSpacing())
- .setFontFeatureSettings(mTextView.getFontFeatureSettings())
- .setFontVariationSettings(mTextView.getFontVariationSettings())
- .setLineBreakStyle(mTextView.getLineBreakStyle())
- .setLineBreakWordStyle(mTextView.getLineBreakWordStyle())
- .setTextScaleX(mTextView.getTextScaleX())
- .setHighlightTextColor(mTextView.getHighlightColor())
- .setTextColor(mTextView.getCurrentTextColor())
- .setHintTextColor(mTextView.getCurrentHintTextColor())
- .setLinkTextColor(linkTextColor);
- builder.setTextAppearanceInfo(appearanceBuilder.build());
+ builder.setTextAppearanceInfo(TextAppearanceInfo.createFromTextView(mTextView));
}
imm.updateCursorAnchorInfo(mTextView, builder.build());
diff --git a/core/java/android/widget/MediaController.java b/core/java/android/widget/MediaController.java
index ab2d005..ff2e175 100644
--- a/core/java/android/widget/MediaController.java
+++ b/core/java/android/widget/MediaController.java
@@ -38,7 +38,6 @@
import android.widget.SeekBar.OnSeekBarChangeListener;
import android.window.OnBackInvokedCallback;
import android.window.OnBackInvokedDispatcher;
-import android.window.WindowOnBackInvokedDispatcher;
import com.android.internal.policy.PhoneWindow;
@@ -748,8 +747,7 @@
}
ViewRootImpl viewRootImpl = mDecor.getViewRootImpl();
if (viewRootImpl != null
- && WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(
- viewRootImpl.mContext)) {
+ && viewRootImpl.getOnBackInvokedDispatcher().isOnBackInvokedCallbackEnabled()) {
viewRootImpl.getOnBackInvokedDispatcher()
.unregisterOnBackInvokedCallback(mBackCallback);
}
@@ -763,8 +761,7 @@
ViewRootImpl viewRootImpl = mDecor.getViewRootImpl();
if (viewRootImpl != null
- && WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(
- viewRootImpl.mContext)) {
+ && viewRootImpl.getOnBackInvokedDispatcher().isOnBackInvokedCallbackEnabled()) {
viewRootImpl.getOnBackInvokedDispatcher().registerOnBackInvokedCallback(
OnBackInvokedDispatcher.PRIORITY_DEFAULT, mBackCallback);
mBackCallbackRegistered = true;
diff --git a/core/java/android/window/ImeOnBackInvokedDispatcher.java b/core/java/android/window/ImeOnBackInvokedDispatcher.java
index f74d294..be9cbff 100644
--- a/core/java/android/window/ImeOnBackInvokedDispatcher.java
+++ b/core/java/android/window/ImeOnBackInvokedDispatcher.java
@@ -123,7 +123,7 @@
private void receive(
int resultCode, Bundle resultData,
- @NonNull OnBackInvokedDispatcher receivingDispatcher) {
+ @NonNull WindowOnBackInvokedDispatcher receivingDispatcher) {
final int callbackId = resultData.getInt(RESULT_KEY_ID);
if (resultCode == RESULT_CODE_REGISTER) {
int priority = resultData.getInt(RESULT_KEY_PRIORITY);
@@ -140,11 +140,11 @@
@NonNull IOnBackInvokedCallback iCallback,
@OnBackInvokedDispatcher.Priority int priority,
int callbackId,
- @NonNull OnBackInvokedDispatcher receivingDispatcher) {
+ @NonNull WindowOnBackInvokedDispatcher receivingDispatcher) {
final ImeOnBackInvokedCallback imeCallback =
new ImeOnBackInvokedCallback(iCallback, callbackId, priority);
mImeCallbacks.add(imeCallback);
- receivingDispatcher.registerOnBackInvokedCallback(priority, imeCallback);
+ receivingDispatcher.registerOnBackInvokedCallbackUnchecked(imeCallback, priority);
}
private void unregisterReceivedCallback(
diff --git a/core/java/android/window/ProxyOnBackInvokedDispatcher.java b/core/java/android/window/ProxyOnBackInvokedDispatcher.java
index 49acde9..09d8b0f 100644
--- a/core/java/android/window/ProxyOnBackInvokedDispatcher.java
+++ b/core/java/android/window/ProxyOnBackInvokedDispatcher.java
@@ -18,6 +18,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.content.Context;
import android.util.Log;
import android.util.Pair;
import android.window.WindowOnBackInvokedDispatcher.Checker;
@@ -53,8 +54,8 @@
private ImeOnBackInvokedDispatcher mImeDispatcher;
private final Checker mChecker;
- public ProxyOnBackInvokedDispatcher(boolean applicationCallBackEnabled) {
- mChecker = new Checker(applicationCallBackEnabled);
+ public ProxyOnBackInvokedDispatcher(@NonNull Context context) {
+ mChecker = new Checker(context);
}
@Override
diff --git a/core/java/android/window/SurfaceSyncGroup.java b/core/java/android/window/SurfaceSyncGroup.java
index c0686fa..e1a9a48 100644
--- a/core/java/android/window/SurfaceSyncGroup.java
+++ b/core/java/android/window/SurfaceSyncGroup.java
@@ -19,6 +19,7 @@
import android.annotation.Nullable;
import android.annotation.UiThread;
import android.os.Debug;
+import android.os.Trace;
import android.util.ArraySet;
import android.util.Log;
import android.util.Pair;
@@ -30,6 +31,7 @@
import java.util.Set;
import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.function.Supplier;
@@ -75,6 +77,10 @@
private static final String TAG = "SurfaceSyncGroup";
private static final boolean DEBUG = false;
+ private static final int MAX_COUNT = 100;
+
+ private static final AtomicInteger sCounter = new AtomicInteger(0);
+
private static Supplier<Transaction> sTransactionFactory = Transaction::new;
/**
@@ -83,6 +89,8 @@
*/
private final Object mLock = new Object();
+ private final String mName;
+
@GuardedBy("mLock")
private final Set<TransactionReadyCallback> mPendingSyncs = new ArraySet<>();
@GuardedBy("mLock")
@@ -112,9 +120,12 @@
/**
* Starts a sync and will automatically apply the final, merged transaction.
*/
- public SurfaceSyncGroup() {
- this(transaction -> {
+ public SurfaceSyncGroup(String name) {
+ this(name, transaction -> {
if (transaction != null) {
+ if (DEBUG) {
+ Log.d(TAG, "Applying transaction " + transaction);
+ }
transaction.apply();
}
});
@@ -128,11 +139,27 @@
* transaction with all the sync data merged. The Transaction
* passed back can be null.
*
- * NOTE: Only should be used by ViewRootImpl
+ * NOTE: Only should be used by ViewRootImpl
* @hide
*/
- public SurfaceSyncGroup(Consumer<Transaction> transactionReadyCallback) {
+ public SurfaceSyncGroup(String name, Consumer<Transaction> transactionReadyCallback) {
+ // sCounter is a way to give the SurfaceSyncGroup a unique name even if the name passed in
+ // is not.
+ // Avoid letting the count get too big so just reset to 0. It's unlikely that we'll have
+ // more than MAX_COUNT active syncs that have overlapping names
+ if (sCounter.get() >= MAX_COUNT) {
+ sCounter.set(0);
+ }
+
+ mName = name + "#" + sCounter.getAndIncrement();
+
mTransactionReadyCallback = transaction -> {
+ if (DEBUG && transaction != null) {
+ Log.d(TAG, "Sending non null transaction " + transaction + " to callback for "
+ + mName);
+ }
+ Trace.instant(Trace.TRACE_TAG_VIEW,
+ "Final TransactionCallback with " + transaction + " for " + mName);
transactionReadyCallback.accept(transaction);
synchronized (mLock) {
for (Pair<Executor, Runnable> callback : mSyncCompleteCallbacks) {
@@ -141,8 +168,10 @@
}
};
+ Trace.instant(Trace.TRACE_TAG_VIEW, "new SurfaceSyncGroup " + mName);
+
if (DEBUG) {
- Log.d(TAG, "setupSync " + this + " " + Debug.getCallers(2));
+ Log.d(TAG, "setupSync " + mName + " " + Debug.getCallers(2));
}
}
@@ -171,9 +200,11 @@
/**
* 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) {
+ Trace.traceBegin(Trace.TRACE_TAG_VIEW, "markSyncReady " + mName);
synchronized (mLock) {
mSyncReady = true;
if (t != null) {
@@ -181,6 +212,7 @@
}
checkIfSyncIsComplete();
}
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
/**
@@ -198,7 +230,7 @@
@UiThread
public boolean addToSync(SurfaceView surfaceView,
Consumer<SurfaceViewFrameCallback> frameCallbackConsumer) {
- SurfaceSyncGroup surfaceSyncGroup = new SurfaceSyncGroup();
+ SurfaceSyncGroup surfaceSyncGroup = new SurfaceSyncGroup(surfaceView.getName());
if (addToSync(surfaceSyncGroup, false /* parentSyncGroupMerge */)) {
frameCallbackConsumer.accept(
() -> surfaceView.syncNextFrame(surfaceSyncGroup::onTransactionReady));
@@ -235,9 +267,15 @@
* otherwise.
*/
public boolean addToSync(SurfaceSyncGroup surfaceSyncGroup, boolean parentSyncGroupMerge) {
+ Trace.traceBegin(Trace.TRACE_TAG_VIEW,
+ "addToSync child=" + surfaceSyncGroup.mName + " parent=" + mName);
TransactionReadyCallback transactionReadyCallback = new TransactionReadyCallback() {
@Override
public void onTransactionReady(Transaction t) {
+ if (DEBUG) {
+ Log.d(TAG, "onTransactionReady called for" + surfaceSyncGroup.mName
+ + " and sent to " + mName);
+ }
synchronized (mLock) {
if (t != null) {
// When an older parent sync group is added due to a child syncGroup getting
@@ -250,6 +288,9 @@
mTransaction.merge(t);
}
mPendingSyncs.remove(this);
+ Trace.instant(Trace.TRACE_TAG_VIEW,
+ "onTransactionReady child=" + surfaceSyncGroup.mName + " parent="
+ + mName);
checkIfSyncIsComplete();
}
}
@@ -257,13 +298,20 @@
synchronized (mLock) {
if (mSyncReady) {
- Log.e(TAG, "Sync " + this + " was already marked as ready. No more "
+ Log.e(TAG, "Sync " + mName + " was already marked as ready. No more "
+ "SurfaceSyncGroups can be added.");
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
return false;
}
mPendingSyncs.add(transactionReadyCallback);
+ if (DEBUG) {
+ Log.d(TAG, "addToSync " + surfaceSyncGroup.mName + " to " + mName + " mSyncReady="
+ + mSyncReady + " mPendingSyncs=" + mPendingSyncs.size());
+ }
}
+
surfaceSyncGroup.onAddedToSyncGroup(this, transactionReadyCallback);
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
return true;
}
@@ -281,21 +329,25 @@
private void checkIfSyncIsComplete() {
if (mFinished) {
if (DEBUG) {
- Log.d(TAG, "SurfaceSyncGroup=" + this + " is already complete");
+ Log.d(TAG, "SurfaceSyncGroup=" + mName + " is already complete");
}
return;
}
+ Trace.instant(Trace.TRACE_TAG_VIEW,
+ "checkIfSyncIsComplete " + mName + " mSyncReady=" + mSyncReady + " mPendingSyncs="
+ + mPendingSyncs.size());
if (!mSyncReady || !mPendingSyncs.isEmpty()) {
if (DEBUG) {
- Log.d(TAG, "SurfaceSyncGroup=" + this + " is not complete. mSyncReady="
- + mSyncReady + " mPendingSyncs=" + mPendingSyncs.size());
+ Log.d(TAG,
+ "SurfaceSyncGroup=" + mName + " is not complete. mSyncReady=" + mSyncReady
+ + " mPendingSyncs=" + mPendingSyncs.size());
}
return;
}
if (DEBUG) {
- Log.d(TAG, "Successfully finished sync id=" + this);
+ Log.d(TAG, "Successfully finished sync id=" + mName);
}
mTransactionReadyCallback.onTransactionReady(mTransaction);
mFinished = true;
@@ -303,6 +355,8 @@
private void onAddedToSyncGroup(SurfaceSyncGroup parentSyncGroup,
TransactionReadyCallback transactionReadyCallback) {
+ Trace.traceBegin(Trace.TRACE_TAG_VIEW,
+ "onAddedToSyncGroup child=" + mName + " parent=" + parentSyncGroup.mName);
boolean finished = false;
synchronized (mLock) {
if (mFinished) {
@@ -316,7 +370,9 @@
// from the original parent are also combined with the new parent SurfaceSyncGroup.
if (mParentSyncGroup != null && mParentSyncGroup != parentSyncGroup) {
if (DEBUG) {
- Log.d(TAG, "Already part of sync group " + mParentSyncGroup + " " + this);
+ Log.d(TAG, "Trying to add to " + parentSyncGroup.mName
+ + " but already part of sync group " + mParentSyncGroup.mName + " "
+ + mName);
}
parentSyncGroup.addToSync(mParentSyncGroup, true /* parentSyncGroupMerge */);
}
@@ -329,8 +385,12 @@
mParentSyncGroup = parentSyncGroup;
final TransactionReadyCallback lastCallback = mTransactionReadyCallback;
mTransactionReadyCallback = t -> {
+ Trace.traceBegin(Trace.TRACE_TAG_VIEW,
+ "transactionReadyCallback " + mName + " parent="
+ + parentSyncGroup.mName);
lastCallback.onTransactionReady(null);
transactionReadyCallback.onTransactionReady(t);
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
};
}
}
@@ -340,7 +400,13 @@
if (finished) {
transactionReadyCallback.onTransactionReady(null);
}
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
+
+ public String getName() {
+ return mName;
+ }
+
/**
* 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
diff --git a/core/java/android/window/SurfaceSyncGroup.md b/core/java/android/window/SurfaceSyncGroup.md
index 659aade..b4faeac 100644
--- a/core/java/android/window/SurfaceSyncGroup.md
+++ b/core/java/android/window/SurfaceSyncGroup.md
@@ -59,7 +59,7 @@
A simple example where you want to sync two windows and also include a transaction in the sync
```java
-SurfaceSyncGroup syncGroup = new SurfaceSyncGroup();
+SurfaceSyncGroup syncGroup = new SurfaceSyncGroup(NAME);
SyncGroup.addSyncCompleteCallback(mMainThreadExecutor, () -> {
Log.d(TAG, "syncComplete");
};
@@ -73,7 +73,7 @@
See `frameworks/base/tests/SurfaceViewSyncTest` for a working example
```java
-SurfaceSyncGroup syncGroup = new SurfaceSyncGroup();
+SurfaceSyncGroup syncGroup = new SurfaceSyncGroup(NAME);
syncGroup.addSyncCompleteCallback(mMainThreadExecutor, () -> {
Log.d(TAG, "syncComplete");
};
diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java
index dd9483a..64992b9 100644
--- a/core/java/android/window/WindowOnBackInvokedDispatcher.java
+++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java
@@ -18,7 +18,9 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.Activity;
import android.content.Context;
+import android.content.ContextWrapper;
import android.os.Handler;
import android.os.RemoteException;
import android.os.SystemProperties;
@@ -63,10 +65,10 @@
/** Holds all callbacks by priorities. */
private final TreeMap<Integer, ArrayList<OnBackInvokedCallback>>
mOnBackInvokedCallbacks = new TreeMap<>();
- private final Checker mChecker;
+ private Checker mChecker;
- public WindowOnBackInvokedDispatcher(boolean applicationCallBackEnabled) {
- mChecker = new Checker(applicationCallBackEnabled);
+ public WindowOnBackInvokedDispatcher(@NonNull Context context) {
+ mChecker = new Checker(context);
}
/**
@@ -211,16 +213,25 @@
return null;
}
- /**
- * Returns the checker used to check whether a callback can be registered
- */
- @NonNull
- public Checker getChecker() {
- return mChecker;
- }
@NonNull
private static final BackProgressAnimator mProgressAnimator = new BackProgressAnimator();
+ /**
+ * The {@link Context} in ViewRootImp and Activity could be different, this will make sure it
+ * could update the checker condition base on the real context when binding the proxy
+ * dispatcher in PhoneWindow.
+ */
+ public void updateContext(@NonNull Context context) {
+ mChecker = new Checker(context);
+ }
+
+ /**
+ * Returns false if the legacy back behavior should be used.
+ */
+ public boolean isOnBackInvokedCallbackEnabled() {
+ return Checker.isOnBackInvokedCallbackEnabled(mChecker.getContext());
+ }
+
static class OnBackInvokedCallbackWrapper extends IOnBackInvokedCallback.Stub {
private final WeakReference<OnBackInvokedCallback> mCallback;
@@ -284,27 +295,13 @@
}
/**
- * Returns if the legacy back behavior should be used.
+ * Returns false if the legacy back behavior should be used.
* <p>
* Legacy back behavior dispatches KEYCODE_BACK instead of invoking the application registered
* {@link OnBackInvokedCallback}.
*/
- public static boolean isOnBackInvokedCallbackEnabled(@Nullable Context context) {
- // new back is enabled if the feature flag is enabled AND the app does not explicitly
- // request legacy back.
- boolean featureFlagEnabled = ENABLE_PREDICTIVE_BACK;
- // If the context is null, we assume true and fallback on the two other conditions.
- boolean appRequestsPredictiveBack =
- context != null && context.getApplicationInfo().isOnBackInvokedCallbackEnabled();
-
- if (DEBUG) {
- Log.d(TAG, TextUtils.formatSimple("App: %s featureFlagEnabled=%s "
- + "appRequestsPredictiveBack=%s alwaysEnforce=%s",
- context != null ? context.getApplicationInfo().packageName : "null context",
- featureFlagEnabled, appRequestsPredictiveBack, ALWAYS_ENFORCE_PREDICTIVE_BACK));
- }
-
- return featureFlagEnabled && (appRequestsPredictiveBack || ALWAYS_ENFORCE_PREDICTIVE_BACK);
+ public static boolean isOnBackInvokedCallbackEnabled(@NonNull Context context) {
+ return Checker.isOnBackInvokedCallbackEnabled(context);
}
@Override
@@ -313,17 +310,15 @@
mImeDispatcher = imeDispatcher;
}
-
/**
* Class used to check whether a callback can be registered or not. This is meant to be
* shared with {@link ProxyOnBackInvokedDispatcher} which needs to do the same checks.
*/
public static class Checker {
+ private WeakReference<Context> mContext;
- private final boolean mApplicationCallBackEnabled;
-
- public Checker(boolean applicationCallBackEnabled) {
- mApplicationCallBackEnabled = applicationCallBackEnabled;
+ public Checker(@NonNull Context context) {
+ mContext = new WeakReference<>(context);
}
/**
@@ -333,10 +328,9 @@
*/
public boolean checkApplicationCallbackRegistration(int priority,
OnBackInvokedCallback callback) {
- if (!mApplicationCallBackEnabled
- && !(callback instanceof CompatOnBackInvokedCallback)
- && !ALWAYS_ENFORCE_PREDICTIVE_BACK) {
- Log.w("OnBackInvokedCallback",
+ if (!isOnBackInvokedCallbackEnabled(getContext())
+ && !(callback instanceof CompatOnBackInvokedCallback)) {
+ Log.w(TAG,
"OnBackInvokedCallback is not enabled for the application."
+ "\nSet 'android:enableOnBackInvokedCallback=\"true\"' in the"
+ " application manifest.");
@@ -349,5 +343,64 @@
Objects.requireNonNull(callback);
return true;
}
+
+ private Context getContext() {
+ return mContext.get();
+ }
+
+ private static boolean isOnBackInvokedCallbackEnabled(@Nullable Context context) {
+ // new back is enabled if the feature flag is enabled AND the app does not explicitly
+ // request legacy back.
+ boolean featureFlagEnabled = ENABLE_PREDICTIVE_BACK;
+ if (!featureFlagEnabled) {
+ return false;
+ }
+
+ if (ALWAYS_ENFORCE_PREDICTIVE_BACK) {
+ return true;
+ }
+
+ // If the context is null, return false to use legacy back.
+ if (context == null) {
+ Log.w(TAG, "OnBackInvokedCallback is not enabled because context is null.");
+ return false;
+ }
+
+ boolean requestsPredictiveBack;
+
+ // Check if the context is from an activity.
+ while ((context instanceof ContextWrapper) && !(context instanceof Activity)) {
+ context = ((ContextWrapper) context).getBaseContext();
+ }
+
+ if (context instanceof Activity) {
+ final Activity activity = (Activity) context;
+
+ if (activity.getActivityInfo().hasOnBackInvokedCallbackEnabled()) {
+ requestsPredictiveBack =
+ activity.getActivityInfo().isOnBackInvokedCallbackEnabled();
+ } else {
+ requestsPredictiveBack =
+ context.getApplicationInfo().isOnBackInvokedCallbackEnabled();
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, TextUtils.formatSimple("Activity: %s isPredictiveBackEnabled=%s",
+ activity.getComponentName(),
+ requestsPredictiveBack));
+ }
+ } else {
+ requestsPredictiveBack =
+ context.getApplicationInfo().isOnBackInvokedCallbackEnabled();
+
+ if (DEBUG) {
+ Log.d(TAG, TextUtils.formatSimple("App: %s requestsPredictiveBack=%s",
+ context.getApplicationInfo().packageName,
+ requestsPredictiveBack));
+ }
+ }
+
+ return requestsPredictiveBack;
+ }
}
}
diff --git a/core/java/com/android/internal/app/AppLocaleCollector.java b/core/java/com/android/internal/app/AppLocaleCollector.java
index 65e8c64..7fe1e3a 100644
--- a/core/java/com/android/internal/app/AppLocaleCollector.java
+++ b/core/java/com/android/internal/app/AppLocaleCollector.java
@@ -19,49 +19,186 @@
import static com.android.internal.app.AppLocaleStore.AppLocaleResult.LocaleStatus.GET_SUPPORTED_LANGUAGE_FROM_ASSET;
import static com.android.internal.app.AppLocaleStore.AppLocaleResult.LocaleStatus.GET_SUPPORTED_LANGUAGE_FROM_LOCAL_CONFIG;
+import android.app.LocaleManager;
import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
import android.os.Build;
import android.os.LocaleList;
+import android.os.SystemProperties;
+import android.provider.Settings;
import android.util.Log;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodManager;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.HashMap;
import java.util.HashSet;
+import java.util.List;
import java.util.Locale;
import java.util.Set;
+import java.util.stream.Collectors;
/** The Locale data collector for per-app language. */
-class AppLocaleCollector implements LocalePickerWithRegion.LocaleCollectorBase {
+public class AppLocaleCollector implements LocalePickerWithRegion.LocaleCollectorBase {
private static final String TAG = AppLocaleCollector.class.getSimpleName();
private final Context mContext;
private final String mAppPackageName;
- private final LocaleStore.LocaleInfo mAppCurrentLocale;
+ private LocaleStore.LocaleInfo mAppCurrentLocale;
+ private Set<LocaleStore.LocaleInfo> mAllAppActiveLocales;
+ private Set<LocaleStore.LocaleInfo> mImeLocales;
+ private static final String PROP_APP_LANGUAGE_SUGGESTION =
+ "android.app.language.suggestion.enhanced";
+ private static final boolean ENABLED = true;
- AppLocaleCollector(Context context, String appPackageName) {
+ public AppLocaleCollector(Context context, String appPackageName) {
mContext = context;
mAppPackageName = appPackageName;
- mAppCurrentLocale = LocaleStore.getAppCurrentLocaleInfo(
- mContext, mAppPackageName);
+ }
+
+ @VisibleForTesting
+ public LocaleStore.LocaleInfo getAppCurrentLocale() {
+ return LocaleStore.getAppActivatedLocaleInfo(mContext, mAppPackageName, true);
+ }
+
+ /**
+ * Get all applications' activated locales.
+ * @return A set which includes all applications' activated LocaleInfo.
+ */
+ @VisibleForTesting
+ public Set<LocaleStore.LocaleInfo> getAllAppActiveLocales() {
+ PackageManager pm = mContext.getPackageManager();
+ LocaleManager lm = mContext.getSystemService(LocaleManager.class);
+ HashSet<LocaleStore.LocaleInfo> result = new HashSet<>();
+ if (pm != null && lm != null) {
+ HashMap<String, LocaleStore.LocaleInfo> map = new HashMap<>();
+ for (ApplicationInfo appInfo : pm.getInstalledApplications(
+ PackageManager.ApplicationInfoFlags.of(0))) {
+ LocaleStore.LocaleInfo localeInfo = LocaleStore.getAppActivatedLocaleInfo(
+ mContext, appInfo.packageName, false);
+ // For the locale to be added into the suggestion area, its country could not be
+ // empty.
+ if (localeInfo != null && localeInfo.getLocale().getCountry().length() > 0) {
+ map.put(localeInfo.getId(), localeInfo);
+ }
+ }
+ map.forEach((language, localeInfo) -> result.add(localeInfo));
+ }
+ return result;
+ }
+
+ /**
+ * Get all locales that active IME supports.
+ *
+ * @return A set which includes all LocaleInfo that active IME supports.
+ */
+ @VisibleForTesting
+ public Set<LocaleStore.LocaleInfo> getActiveImeLocales() {
+ Set<LocaleStore.LocaleInfo> activeImeLocales = null;
+ InputMethodManager imm = mContext.getSystemService(InputMethodManager.class);
+ if (imm != null) {
+ InputMethodInfo activeIme = getActiveIme(imm);
+ if (activeIme != null) {
+ activeImeLocales = LocaleStore.transformImeLanguageTagToLocaleInfo(
+ imm.getEnabledInputMethodSubtypeList(activeIme, true));
+ }
+ }
+ if (activeImeLocales == null) {
+ return Set.of();
+ } else {
+ return activeImeLocales.stream().filter(
+ // For the locale to be added into the suggestion area, its country could not be
+ // empty.
+ info -> info.getLocale().getCountry().length() > 0).collect(
+ Collectors.toSet());
+ }
+ }
+
+ private InputMethodInfo getActiveIme(InputMethodManager imm) {
+ InputMethodInfo activeIme = null;
+ List<InputMethodInfo> infoList = imm.getEnabledInputMethodList();
+ String imeId = Settings.Secure.getStringForUser(mContext.getContentResolver(),
+ Settings.Secure.DEFAULT_INPUT_METHOD, mContext.getUserId());
+ if (infoList != null && imeId != null) {
+ for (InputMethodInfo method : infoList) {
+ if (method.getId().equals(imeId)) {
+ activeIme = method;
+ }
+ }
+ }
+ return activeIme;
+ }
+
+ /**
+ * Get the AppLocaleResult that the application supports.
+ * @return The AppLocaleResult that the application supports.
+ */
+ @VisibleForTesting
+ public AppLocaleStore.AppLocaleResult getAppSupportedLocales() {
+ return AppLocaleStore.getAppSupportedLocales(mContext, mAppPackageName);
+ }
+
+ /**
+ * Get the locales that system supports excluding langTagsToIgnore.
+ *
+ * @param langTagsToIgnore A language set to be ignored.
+ * @param parent The parent locale.
+ * @param translatedOnly specified if is is only for translation.
+ * @return A set which includes the LocaleInfo that system supports, excluding langTagsToIgnore.
+ */
+ @VisibleForTesting
+ public Set<LocaleStore.LocaleInfo> getSystemSupportedLocale(Set<String> langTagsToIgnore,
+ LocaleStore.LocaleInfo parent, boolean translatedOnly) {
+ return LocaleStore.getLevelLocales(mContext, langTagsToIgnore, parent, translatedOnly);
+ }
+
+ /**
+ * Get the locales that system activates.
+ * @return A set which includes all the locales that system activates.
+ */
+ @VisibleForTesting
+ public List<LocaleStore.LocaleInfo> getSystemCurrentLocale() {
+ return LocaleStore.getSystemCurrentLocaleInfo();
}
@Override
public HashSet<String> getIgnoredLocaleList(boolean translatedOnly) {
HashSet<String> langTagsToIgnore = new HashSet<>();
- LocaleList systemLangList = LocaleList.getDefault();
- for(int i = 0; i < systemLangList.size(); i++) {
- langTagsToIgnore.add(systemLangList.get(i).toLanguageTag());
- }
-
if (mAppCurrentLocale != null) {
langTagsToIgnore.add(mAppCurrentLocale.getLocale().toLanguageTag());
}
+
+ if (SystemProperties.getBoolean(PROP_APP_LANGUAGE_SUGGESTION, ENABLED)) {
+ // Add the locale that other App activated
+ mAllAppActiveLocales.forEach(
+ info -> langTagsToIgnore.add(info.getLocale().toLanguageTag()));
+ // Add the locale that active IME enabled
+ mImeLocales.forEach(info -> langTagsToIgnore.add(info.getLocale().toLanguageTag()));
+ }
+
+ // Add System locales
+ LocaleList systemLangList = LocaleList.getDefault();
+ for (int i = 0; i < systemLangList.size(); i++) {
+ langTagsToIgnore.add(systemLangList.get(i).toLanguageTag());
+ }
return langTagsToIgnore;
}
@Override
public Set<LocaleStore.LocaleInfo> getSupportedLocaleList(LocaleStore.LocaleInfo parent,
boolean translatedOnly, boolean isForCountryMode) {
- AppLocaleStore.AppLocaleResult result =
- AppLocaleStore.getAppSupportedLocales(mContext, mAppPackageName);
+ if (mAppCurrentLocale == null) {
+ mAppCurrentLocale = getAppCurrentLocale();
+ }
+ if (mAllAppActiveLocales == null) {
+ mAllAppActiveLocales = getAllAppActiveLocales();
+ }
+ if (mImeLocales == null) {
+ mImeLocales = getActiveImeLocales();
+ }
+ AppLocaleStore.AppLocaleResult result = getAppSupportedLocales();
Set<String> langTagsToIgnore = getIgnoredLocaleList(translatedOnly);
Set<LocaleStore.LocaleInfo> appLocaleList = new HashSet<>();
Set<LocaleStore.LocaleInfo> systemLocaleList;
@@ -71,11 +208,9 @@
// Get system supported locale list
if (isForCountryMode) {
- systemLocaleList = LocaleStore.getLevelLocales(mContext,
- langTagsToIgnore, parent, translatedOnly);
+ systemLocaleList = getSystemSupportedLocale(langTagsToIgnore, parent, translatedOnly);
} else {
- systemLocaleList = LocaleStore.getLevelLocales(mContext, langTagsToIgnore,
- null /* no parent */, translatedOnly);
+ systemLocaleList = getSystemSupportedLocale(langTagsToIgnore, null, translatedOnly);
}
// Add current app locale
@@ -84,19 +219,46 @@
}
// Add current system language into suggestion list
- for(LocaleStore.LocaleInfo localeInfo:
- LocaleStore.getSystemCurrentLocaleInfo()) {
- boolean isNotCurrentLocale = mAppCurrentLocale == null
- || !localeInfo.getLocale().equals(mAppCurrentLocale.getLocale());
- if (!isForCountryMode && isNotCurrentLocale) {
- appLocaleList.add(localeInfo);
+ if (!isForCountryMode) {
+ boolean isCurrentLocale, isInAppOrIme;
+ for (LocaleStore.LocaleInfo localeInfo : getSystemCurrentLocale()) {
+ isCurrentLocale = mAppCurrentLocale != null
+ && localeInfo.getLocale().equals(mAppCurrentLocale.getLocale());
+ isInAppOrIme = existsInAppOrIme(localeInfo.getLocale());
+ if (!isCurrentLocale && !isInAppOrIme) {
+ appLocaleList.add(localeInfo);
+ }
}
}
- // Add the languages that included in system supported locale
+ // Add the languages that are included in system supported locale
+ Set<LocaleStore.LocaleInfo> suggestedSet = null;
if (shouldShowList) {
- appLocaleList.addAll(filterTheLanguagesNotIncludedInSystemLocale(
- systemLocaleList, result.mAppSupportedLocales));
+ appLocaleList.addAll(filterSupportedLocales(systemLocaleList,
+ result.mAppSupportedLocales));
+ suggestedSet = getSuggestedLocales(appLocaleList);
+ }
+
+ if (!isForCountryMode && SystemProperties.getBoolean(PROP_APP_LANGUAGE_SUGGESTION,
+ ENABLED)) {
+ // Add the language that other apps activate into the suggestion list.
+ Set<LocaleStore.LocaleInfo> localeSet = filterSupportedLocales(mAllAppActiveLocales,
+ result.mAppSupportedLocales);
+ if (suggestedSet != null) {
+ // Filter out the locale with the same language and country
+ // like zh-TW vs zh-Hant-TW.
+ localeSet = filterSameLanguageAndCountry(localeSet, suggestedSet);
+ }
+ appLocaleList.addAll(localeSet);
+ suggestedSet.addAll(localeSet);
+
+ // Add the language that the active IME enables into the suggestion list.
+ localeSet = filterSupportedLocales(mImeLocales, result.mAppSupportedLocales);
+ if (suggestedSet != null) {
+ localeSet = filterSameLanguageAndCountry(localeSet, suggestedSet);
+ }
+ appLocaleList.addAll(localeSet);
+ suggestedSet.addAll(localeSet);
}
// Add "system language" option
@@ -117,17 +279,55 @@
return true;
}
- private Set<LocaleStore.LocaleInfo> filterTheLanguagesNotIncludedInSystemLocale(
- Set<LocaleStore.LocaleInfo> systemLocaleList,
+ private Set<LocaleStore.LocaleInfo> getSuggestedLocales(Set<LocaleStore.LocaleInfo> localeSet) {
+ return localeSet.stream().filter(localeInfo -> localeInfo.isSuggested()).collect(
+ Collectors.toSet());
+ }
+
+ private Set<LocaleStore.LocaleInfo> filterSameLanguageAndCountry(
+ Set<LocaleStore.LocaleInfo> newLocaleList,
+ Set<LocaleStore.LocaleInfo> existingLocaleList) {
+ Set<LocaleStore.LocaleInfo> result = new HashSet<>(newLocaleList.size());
+ for (LocaleStore.LocaleInfo appLocaleInfo : newLocaleList) {
+ boolean same = false;
+ Locale appLocale = appLocaleInfo.getLocale();
+ for (LocaleStore.LocaleInfo localeInfo : existingLocaleList) {
+ Locale suggested = localeInfo.getLocale();
+ if (appLocale.getLanguage().equals(suggested.getLanguage())
+ && appLocale.getCountry().equals(suggested.getCountry())) {
+ same = true;
+ break;
+ }
+ }
+ if (!same) {
+ result.add(appLocaleInfo);
+ }
+ }
+ return result;
+ }
+
+ private boolean existsInAppOrIme(Locale locale) {
+ boolean existInApp = mAllAppActiveLocales.stream().anyMatch(
+ localeInfo -> localeInfo.getLocale().equals(locale));
+ if (existInApp) {
+ return true;
+ } else {
+ return mImeLocales.stream().anyMatch(
+ localeInfo -> localeInfo.getLocale().equals(locale));
+ }
+ }
+
+ private Set<LocaleStore.LocaleInfo> filterSupportedLocales(
+ Set<LocaleStore.LocaleInfo> suggestedLocales,
HashSet<Locale> appSupportedLocales) {
Set<LocaleStore.LocaleInfo> filteredList = new HashSet<>();
- for(LocaleStore.LocaleInfo li: systemLocaleList) {
+ for (LocaleStore.LocaleInfo li : suggestedLocales) {
if (appSupportedLocales.contains(li.getLocale())) {
filteredList.add(li);
} else {
- for(Locale l: appSupportedLocales) {
- if(LocaleList.matchesLanguageAndScript(li.getLocale(), l)) {
+ for (Locale l : appSupportedLocales) {
+ if (LocaleList.matchesLanguageAndScript(li.getLocale(), l)) {
filteredList.add(li);
break;
}
diff --git a/core/java/com/android/internal/app/AppLocaleStore.java b/core/java/com/android/internal/app/AppLocaleStore.java
index f3a322c..a450a05 100644
--- a/core/java/com/android/internal/app/AppLocaleStore.java
+++ b/core/java/com/android/internal/app/AppLocaleStore.java
@@ -30,7 +30,10 @@
import java.util.Locale;
import java.util.stream.Collectors;
-class AppLocaleStore {
+/**
+ * A class used to access an application's supporting locales.
+ */
+public class AppLocaleStore {
private static final String TAG = AppLocaleStore.class.getSimpleName();
public static AppLocaleResult getAppSupportedLocales(
@@ -148,8 +151,11 @@
return false;
}
- static class AppLocaleResult {
- enum LocaleStatus {
+ /**
+ * A class used to store an application's supporting locales.
+ */
+ public static class AppLocaleResult {
+ public enum LocaleStatus {
UNKNOWN_FAILURE,
NO_SUPPORTED_LANGUAGE_IN_APP,
ASSET_LOCALE_IS_EMPTY,
@@ -158,7 +164,7 @@
}
LocaleStatus mLocaleStatus;
- HashSet<Locale> mAppSupportedLocales;
+ public HashSet<Locale> mAppSupportedLocales;
public AppLocaleResult(LocaleStatus localeStatus, HashSet<Locale> appSupportedLocales) {
this.mLocaleStatus = localeStatus;
diff --git a/core/java/com/android/internal/app/LocaleStore.java b/core/java/com/android/internal/app/LocaleStore.java
index 689dec4..d2eee91 100644
--- a/core/java/com/android/internal/app/LocaleStore.java
+++ b/core/java/com/android/internal/app/LocaleStore.java
@@ -23,6 +23,7 @@
import android.provider.Settings;
import android.telephony.TelephonyManager;
import android.util.Log;
+import android.view.inputmethod.InputMethodSubtype;
import com.android.internal.annotations.VisibleForTesting;
@@ -45,10 +46,11 @@
@VisibleForTesting static final int SUGGESTION_TYPE_SIM = 1 << 0;
@VisibleForTesting static final int SUGGESTION_TYPE_CFG = 1 << 1;
// Only for per-app language picker
- private static final int SUGGESTION_TYPE_CURRENT = 1 << 2;
+ @VisibleForTesting static final int SUGGESTION_TYPE_CURRENT = 1 << 2;
// Only for per-app language picker
- private static final int SUGGESTION_TYPE_SYSTEM_LANGUAGE = 1 << 3;
-
+ @VisibleForTesting static final int SUGGESTION_TYPE_SYSTEM_LANGUAGE = 1 << 3;
+ // Only for per-app language picker
+ @VisibleForTesting static final int SUGGESTION_TYPE_OTHER_APP_LANGUAGE = 1 << 4;
private final Locale mLocale;
private final Locale mParent;
private final String mId;
@@ -259,7 +261,16 @@
}
}
- public static LocaleInfo getAppCurrentLocaleInfo(Context context, String appPackageName) {
+ /**
+ * Get the application's activated locale.
+ *
+ * @param context UI activity's context.
+ * @param appPackageName The application's package name.
+ * @param isAppSelected True if the application is selected in the UI; false otherwise.
+ * @return A LocaleInfo with the application's activated locale.
+ */
+ public static LocaleInfo getAppActivatedLocaleInfo(Context context, String appPackageName,
+ boolean isAppSelected) {
if (appPackageName == null) {
return null;
}
@@ -272,7 +283,11 @@
if (locale != null) {
LocaleInfo localeInfo = new LocaleInfo(locale);
- localeInfo.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_CURRENT;
+ if (isAppSelected) {
+ localeInfo.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_CURRENT;
+ } else {
+ localeInfo.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_OTHER_APP_LANGUAGE;
+ }
localeInfo.mIsTranslated = true;
return localeInfo;
}
@@ -283,6 +298,24 @@
}
/**
+ * Transform IME's language tag to LocaleInfo.
+ *
+ * @param list A list which includes IME's subtype.
+ * @return A LocaleInfo set which includes IME's language tags.
+ */
+ public static Set<LocaleInfo> transformImeLanguageTagToLocaleInfo(
+ List<InputMethodSubtype> list) {
+ Set<LocaleInfo> imeLocales = new HashSet<>();
+ for (InputMethodSubtype subtype : list) {
+ LocaleInfo localeInfo = new LocaleInfo(subtype.getLanguageTag());
+ localeInfo.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_OTHER_APP_LANGUAGE;
+ localeInfo.mIsTranslated = true;
+ imeLocales.add(localeInfo);
+ }
+ return imeLocales;
+ }
+
+ /**
* Returns a list of system languages with LocaleInfo
*/
public static List<LocaleInfo> getSystemCurrentLocaleInfo() {
@@ -458,4 +491,13 @@
}
return result;
}
+
+ /**
+ * API for testing.
+ */
+ @UnsupportedAppUsage
+ @VisibleForTesting
+ public static LocaleInfo fromLocale(Locale locale) {
+ return new LocaleInfo(locale);
+ }
}
diff --git a/core/java/com/android/internal/app/OWNERS b/core/java/com/android/internal/app/OWNERS
index 3833976..970728f 100644
--- a/core/java/com/android/internal/app/OWNERS
+++ b/core/java/com/android/internal/app/OWNERS
@@ -2,6 +2,7 @@
per-file *Resolver* = file:/packages/SystemUI/OWNERS
per-file *Chooser* = file:/packages/SystemUI/OWNERS
per-file SimpleIconFactory.java = file:/packages/SystemUI/OWNERS
+per-file AbstractMultiProfilePagerAdapter.java = file:/packages/SystemUI/OWNERS
per-file NetInitiatedActivity.java = file:/location/java/android/location/OWNERS
per-file *BatteryStats* = file:/BATTERY_STATS_OWNERS
@@ -11,4 +12,4 @@
per-file *Voice* = file:/core/java/android/service/voice/OWNERS
# System language settings
-per-file *Locale* = file:platform/packages/apps/Settings:/src/com/android/settings/localepicker/OWNERS
\ No newline at end of file
+per-file *Locale* = file:platform/packages/apps/Settings:/src/com/android/settings/localepicker/OWNERS
diff --git a/core/java/com/android/internal/content/InstallLocationUtils.java b/core/java/com/android/internal/content/InstallLocationUtils.java
index c456cf3..4d9c09e 100644
--- a/core/java/com/android/internal/content/InstallLocationUtils.java
+++ b/core/java/com/android/internal/content/InstallLocationUtils.java
@@ -31,6 +31,7 @@
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager;
+import android.os.SystemProperties;
import android.os.storage.IStorageManager;
import android.os.storage.StorageManager;
import android.os.storage.StorageVolume;
@@ -261,12 +262,12 @@
// We're left with new installations with either preferring external or auto, so just pick
// volume with most space
+ String bestCandidate = !volumePaths.isEmpty() ? volumePaths.keyAt(0) : null;
if (volumePaths.size() == 1) {
if (checkFitOnVolume(storageManager, volumePaths.valueAt(0), params)) {
- return volumePaths.keyAt(0);
+ return bestCandidate;
}
} else {
- String bestCandidate = null;
long bestCandidateAvailBytes = Long.MIN_VALUE;
for (String vol : volumePaths.keySet()) {
final String volumePath = volumePaths.get(vol);
@@ -289,6 +290,14 @@
}
+ // For new installations of a predefined size, check property to let it through
+ // regardless of the actual free space.
+ if (bestCandidate != null && Integer.MAX_VALUE == params.sizeBytes
+ && SystemProperties.getBoolean("debug.pm.install_skip_size_check_for_maxint",
+ false)) {
+ return bestCandidate;
+ }
+
throw new IOException("No special requests, but no room on allowed volumes. "
+ " allow3rdPartyOnInternal? " + allow3rdPartyOnInternal);
}
diff --git a/core/java/com/android/internal/inputmethod/SoftInputShowHideReason.java b/core/java/com/android/internal/inputmethod/SoftInputShowHideReason.java
index f1635eb..ec9184b 100644
--- a/core/java/com/android/internal/inputmethod/SoftInputShowHideReason.java
+++ b/core/java/com/android/internal/inputmethod/SoftInputShowHideReason.java
@@ -23,6 +23,7 @@
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.ImeProtoEnums;
import android.view.inputmethod.InputMethodManager;
import java.lang.annotation.Retention;
@@ -64,113 +65,114 @@
SoftInputShowHideReason.HIDE_SOFT_INPUT_IME_TOGGLE_SOFT_INPUT,
SoftInputShowHideReason.HIDE_SOFT_INPUT_EXTRACT_INPUT_CHANGED,
SoftInputShowHideReason.HIDE_SOFT_INPUT_IMM_DEPRECATION,
- SoftInputShowHideReason.HIDE_WINDOW_GAINED_FOCUS_WITHOUT_EDITOR})
+ SoftInputShowHideReason.HIDE_WINDOW_GAINED_FOCUS_WITHOUT_EDITOR
+})
public @interface SoftInputShowHideReason {
/** Show soft input by {@link android.view.inputmethod.InputMethodManager#showSoftInput}. */
- int SHOW_SOFT_INPUT = 0;
+ int SHOW_SOFT_INPUT = ImeProtoEnums.REASON_SHOW_SOFT_INPUT;
/** Show soft input when {@code InputMethodManagerService#attachNewInputLocked} called. */
- int ATTACH_NEW_INPUT = 1;
+ int ATTACH_NEW_INPUT = ImeProtoEnums.REASON_ATTACH_NEW_INPUT;
/** Show soft input by {@code InputMethodManagerService#showMySoftInput}. This is triggered when
* the IME process try to show the keyboard.
*
* @see android.inputmethodservice.InputMethodService#requestShowSelf(int)
*/
- int SHOW_SOFT_INPUT_FROM_IME = 2;
+ int SHOW_SOFT_INPUT_FROM_IME = ImeProtoEnums.REASON_SHOW_SOFT_INPUT_FROM_IME;
/**
* Hide soft input by
* {@link android.view.inputmethod.InputMethodManager#hideSoftInputFromWindow}.
*/
- int HIDE_SOFT_INPUT = 3;
+ int HIDE_SOFT_INPUT = ImeProtoEnums.REASON_HIDE_SOFT_INPUT;
/**
* Hide soft input by
* {@link android.inputmethodservice.InputMethodService#requestHideSelf(int)}.
*/
- int HIDE_SOFT_INPUT_FROM_IME = 4;
+ int HIDE_SOFT_INPUT_FROM_IME = ImeProtoEnums.REASON_HIDE_SOFT_INPUT_FROM_IME;
/**
* Show soft input when navigated forward to the window (with
- * {@link LayoutParams#SOFT_INPUT_IS_FORWARD_NAVIGATION}} which the focused view is text
+ * {@link LayoutParams#SOFT_INPUT_IS_FORWARD_NAVIGATION}) which the focused view is text
* editor and system will auto-show the IME when the window can resize or running on a large
* screen.
*/
- int SHOW_AUTO_EDITOR_FORWARD_NAV = 5;
+ int SHOW_AUTO_EDITOR_FORWARD_NAV = ImeProtoEnums.REASON_SHOW_AUTO_EDITOR_FORWARD_NAV;
/**
* Show soft input when navigated forward to the window with
* {@link LayoutParams#SOFT_INPUT_IS_FORWARD_NAVIGATION} and
* {@link LayoutParams#SOFT_INPUT_STATE_VISIBLE}.
*/
- int SHOW_STATE_VISIBLE_FORWARD_NAV = 6;
+ int SHOW_STATE_VISIBLE_FORWARD_NAV = ImeProtoEnums.REASON_SHOW_STATE_VISIBLE_FORWARD_NAV;
/**
* Show soft input when the window with {@link LayoutParams#SOFT_INPUT_STATE_ALWAYS_VISIBLE}.
*/
- int SHOW_STATE_ALWAYS_VISIBLE = 7;
+ int SHOW_STATE_ALWAYS_VISIBLE = ImeProtoEnums.REASON_SHOW_STATE_ALWAYS_VISIBLE;
/**
* Show soft input during {@code InputMethodManagerService} receive changes from
* {@code SettingsProvider}.
*/
- int SHOW_SETTINGS_ON_CHANGE = 8;
+ int SHOW_SETTINGS_ON_CHANGE = ImeProtoEnums.REASON_SHOW_SETTINGS_ON_CHANGE;
/** Hide soft input during switching user. */
- int HIDE_SWITCH_USER = 9;
+ int HIDE_SWITCH_USER = ImeProtoEnums.REASON_HIDE_SWITCH_USER;
/** Hide soft input when the user is invalid. */
- int HIDE_INVALID_USER = 10;
+ int HIDE_INVALID_USER = ImeProtoEnums.REASON_HIDE_INVALID_USER;
/**
* Hide soft input when the window with {@link LayoutParams#SOFT_INPUT_STATE_UNSPECIFIED} which
* the focused view is not text editor.
*/
- int HIDE_UNSPECIFIED_WINDOW = 11;
+ int HIDE_UNSPECIFIED_WINDOW = ImeProtoEnums.REASON_HIDE_UNSPECIFIED_WINDOW;
/**
* Hide soft input when navigated forward to the window with
* {@link LayoutParams#SOFT_INPUT_IS_FORWARD_NAVIGATION} and
* {@link LayoutParams#SOFT_INPUT_STATE_HIDDEN}.
*/
- int HIDE_STATE_HIDDEN_FORWARD_NAV = 12;
+ int HIDE_STATE_HIDDEN_FORWARD_NAV = ImeProtoEnums.REASON_HIDE_STATE_HIDDEN_FORWARD_NAV;
/**
* Hide soft input when the window with {@link LayoutParams#SOFT_INPUT_STATE_ALWAYS_HIDDEN}.
*/
- int HIDE_ALWAYS_HIDDEN_STATE = 13;
+ int HIDE_ALWAYS_HIDDEN_STATE = ImeProtoEnums.REASON_HIDE_ALWAYS_HIDDEN_STATE;
/** Hide soft input when "adb shell ime <command>" called. */
- int HIDE_RESET_SHELL_COMMAND = 14;
+ int HIDE_RESET_SHELL_COMMAND = ImeProtoEnums.REASON_HIDE_RESET_SHELL_COMMAND;
/**
* Hide soft input during {@code InputMethodManagerService} receive changes from
* {@code SettingsProvider}.
*/
- int HIDE_SETTINGS_ON_CHANGE = 15;
+ int HIDE_SETTINGS_ON_CHANGE = ImeProtoEnums.REASON_HIDE_SETTINGS_ON_CHANGE;
/**
* Hide soft input from {@link com.android.server.policy.PhoneWindowManager} when setting
* {@link com.android.internal.R.integer#config_shortPressOnPowerBehavior} in config.xml as
* dismiss IME.
*/
- int HIDE_POWER_BUTTON_GO_HOME = 16;
+ int HIDE_POWER_BUTTON_GO_HOME = ImeProtoEnums.REASON_HIDE_POWER_BUTTON_GO_HOME;
/** Hide soft input when attaching docked stack. */
- int HIDE_DOCKED_STACK_ATTACHED = 17;
+ int HIDE_DOCKED_STACK_ATTACHED = ImeProtoEnums.REASON_HIDE_DOCKED_STACK_ATTACHED;
/**
* Hide soft input when {@link com.android.server.wm.RecentsAnimationController} starts
* intercept touch from app window.
*/
- int HIDE_RECENTS_ANIMATION = 18;
+ int HIDE_RECENTS_ANIMATION = ImeProtoEnums.REASON_HIDE_RECENTS_ANIMATION;
/**
* Hide soft input when {@link com.android.wm.shell.bubbles.BubbleController} is expanding,
* switching, or collapsing Bubbles.
*/
- int HIDE_BUBBLES = 19;
+ int HIDE_BUBBLES = ImeProtoEnums.REASON_HIDE_BUBBLES;
/**
* Hide soft input when focusing the same window (e.g. screen turned-off and turn-on) which no
@@ -183,74 +185,78 @@
* only the dialog focused as it's the latest window with input focus) makes we need to hide
* soft-input when the same window focused again to align with the same behavior prior to R.
*/
- int HIDE_SAME_WINDOW_FOCUSED_WITHOUT_EDITOR = 20;
+ int HIDE_SAME_WINDOW_FOCUSED_WITHOUT_EDITOR =
+ ImeProtoEnums.REASON_HIDE_SAME_WINDOW_FOCUSED_WITHOUT_EDITOR;
/**
* Hide soft input when a {@link com.android.internal.inputmethod.IInputMethodClient} is
* removed.
*/
- int HIDE_REMOVE_CLIENT = 21;
+ int HIDE_REMOVE_CLIENT = ImeProtoEnums.REASON_HIDE_REMOVE_CLIENT;
/**
* Show soft input when the system invoking
* {@link com.android.server.wm.WindowManagerInternal#shouldRestoreImeVisibility}.
*/
- int SHOW_RESTORE_IME_VISIBILITY = 22;
+ int SHOW_RESTORE_IME_VISIBILITY = ImeProtoEnums.REASON_SHOW_RESTORE_IME_VISIBILITY;
/**
* Show soft input by
* {@link android.view.inputmethod.InputMethodManager#toggleSoftInput(int, int)};
*/
- int SHOW_TOGGLE_SOFT_INPUT = 23;
+ int SHOW_TOGGLE_SOFT_INPUT = ImeProtoEnums.REASON_SHOW_TOGGLE_SOFT_INPUT;
/**
* Hide soft input by
* {@link android.view.inputmethod.InputMethodManager#toggleSoftInput(int, int)};
*/
- int HIDE_TOGGLE_SOFT_INPUT = 24;
+ int HIDE_TOGGLE_SOFT_INPUT = ImeProtoEnums.REASON_HIDE_TOGGLE_SOFT_INPUT;
/**
* Show soft input by
* {@link android.view.InsetsController#show(int)};
*/
- int SHOW_SOFT_INPUT_BY_INSETS_API = 25;
+ int SHOW_SOFT_INPUT_BY_INSETS_API = ImeProtoEnums.REASON_SHOW_SOFT_INPUT_BY_INSETS_API;
/**
* Hide soft input if Ime policy has been set to {@link WindowManager#DISPLAY_IME_POLICY_HIDE}.
* See also {@code InputMethodManagerService#mImeHiddenByDisplayPolicy}.
*/
- int HIDE_DISPLAY_IME_POLICY_HIDE = 26;
+ int HIDE_DISPLAY_IME_POLICY_HIDE = ImeProtoEnums.REASON_HIDE_DISPLAY_IME_POLICY_HIDE;
/**
* Hide soft input by {@link android.view.InsetsController#hide(int)}.
*/
- int HIDE_SOFT_INPUT_BY_INSETS_API = 27;
+ int HIDE_SOFT_INPUT_BY_INSETS_API = ImeProtoEnums.REASON_HIDE_SOFT_INPUT_BY_INSETS_API;
/**
* Hide soft input by {@link android.inputmethodservice.InputMethodService#handleBack(boolean)}.
*/
- int HIDE_SOFT_INPUT_BY_BACK_KEY = 28;
+ int HIDE_SOFT_INPUT_BY_BACK_KEY = ImeProtoEnums.REASON_HIDE_SOFT_INPUT_BY_BACK_KEY;
/**
* Hide soft input by
* {@link android.inputmethodservice.InputMethodService#onToggleSoftInput(int, int)}.
*/
- int HIDE_SOFT_INPUT_IME_TOGGLE_SOFT_INPUT = 29;
+ int HIDE_SOFT_INPUT_IME_TOGGLE_SOFT_INPUT =
+ ImeProtoEnums.REASON_HIDE_SOFT_INPUT_IME_TOGGLE_SOFT_INPUT;
/**
* Hide soft input by
* {@link android.inputmethodservice.InputMethodService#onExtractingInputChanged(EditorInfo)})}.
*/
- int HIDE_SOFT_INPUT_EXTRACT_INPUT_CHANGED = 30;
+ int HIDE_SOFT_INPUT_EXTRACT_INPUT_CHANGED =
+ ImeProtoEnums.REASON_HIDE_SOFT_INPUT_EXTRACT_INPUT_CHANGED;
/**
* Hide soft input by the deprecated
* {@link InputMethodManager#hideSoftInputFromInputMethod(IBinder, int)}.
*/
- int HIDE_SOFT_INPUT_IMM_DEPRECATION = 31;
+ int HIDE_SOFT_INPUT_IMM_DEPRECATION = ImeProtoEnums.REASON_HIDE_SOFT_INPUT_IMM_DEPRECATION;
/**
* Hide soft input when the window gained focus without an editor from the IME shown window.
*/
- int HIDE_WINDOW_GAINED_FOCUS_WITHOUT_EDITOR = 32;
+ int HIDE_WINDOW_GAINED_FOCUS_WITHOUT_EDITOR =
+ ImeProtoEnums.REASON_HIDE_WINDOW_GAINED_FOCUS_WITHOUT_EDITOR;
}
diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java
index 7bd6ec8..d9e9a5f 100644
--- a/core/java/com/android/internal/jank/InteractionJankMonitor.java
+++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java
@@ -28,6 +28,8 @@
import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_APP_LAUNCH_FROM_RECENTS;
import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_APP_LAUNCH_FROM_WIDGET;
import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_APP_SWIPE_TO_RECENTS;
+import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_CLOSE_ALL_APPS_SWIPE;
+import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_CLOSE_ALL_APPS_TO_HOME;
import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_OPEN_ALL_APPS;
import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_QUICK_SWITCH;
import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_UNLOCK_ENTRANCE_ANIMATION;
@@ -228,6 +230,8 @@
public static final int CUJ_LOCKSCREEN_OCCLUSION = 64;
public static final int CUJ_RECENTS_SCROLLING = 65;
public static final int CUJ_LAUNCHER_APP_SWIPE_TO_RECENTS = 66;
+ public static final int CUJ_LAUNCHER_CLOSE_ALL_APPS_SWIPE = 67;
+ public static final int CUJ_LAUNCHER_CLOSE_ALL_APPS_TO_HOME = 68;
private static final int NO_STATSD_LOGGING = -1;
@@ -303,6 +307,8 @@
UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LOCKSCREEN_OCCLUSION,
UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__RECENTS_SCROLLING,
UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_APP_SWIPE_TO_RECENTS,
+ UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_CLOSE_ALL_APPS_SWIPE,
+ UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_CLOSE_ALL_APPS_TO_HOME,
};
private static class InstanceHolder {
@@ -393,7 +399,9 @@
CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION,
CUJ_LOCKSCREEN_OCCLUSION,
CUJ_RECENTS_SCROLLING,
- CUJ_LAUNCHER_APP_SWIPE_TO_RECENTS
+ CUJ_LAUNCHER_APP_SWIPE_TO_RECENTS,
+ CUJ_LAUNCHER_CLOSE_ALL_APPS_SWIPE,
+ CUJ_LAUNCHER_CLOSE_ALL_APPS_TO_HOME
})
@Retention(RetentionPolicy.SOURCE)
public @interface CujType {
@@ -911,6 +919,10 @@
return "RECENTS_SCROLLING";
case CUJ_LAUNCHER_APP_SWIPE_TO_RECENTS:
return "LAUNCHER_APP_SWIPE_TO_RECENTS";
+ case CUJ_LAUNCHER_CLOSE_ALL_APPS_SWIPE:
+ return "LAUNCHER_CLOSE_ALL_APPS_SWIPE";
+ case CUJ_LAUNCHER_CLOSE_ALL_APPS_TO_HOME:
+ return "LAUNCHER_CLOSE_ALL_APPS_TO_HOME";
}
return "UNKNOWN";
}
diff --git a/core/java/com/android/internal/os/ProcessCpuTracker.java b/core/java/com/android/internal/os/ProcessCpuTracker.java
index 0df006d..65655b7 100644
--- a/core/java/com/android/internal/os/ProcessCpuTracker.java
+++ b/core/java/com/android/internal/os/ProcessCpuTracker.java
@@ -841,7 +841,19 @@
return sw.toString();
}
- final public String printCurrentState(long now) {
+ /**
+ * Returns current CPU state with all the processes as a String, sorted by load
+ * in descending order.
+ */
+ public final String printCurrentState(long now) {
+ return printCurrentState(now, Integer.MAX_VALUE);
+ }
+
+ /**
+ * Returns current CPU state with the top {@code maxProcessesToDump} highest load
+ * processes as a String, sorted by load in descending order.
+ */
+ public final String printCurrentState(long now, int maxProcessesToDump) {
final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
buildWorkingProcs();
@@ -883,8 +895,8 @@
if (DEBUG) Slog.i(TAG, "totalTime " + totalTime + " over sample time "
+ (mCurrentSampleTime-mLastSampleTime));
- int N = mWorkingProcs.size();
- for (int i=0; i<N; i++) {
+ int dumpedProcessCount = Math.min(maxProcessesToDump, mWorkingProcs.size());
+ for (int i = 0; i < dumpedProcessCount; i++) {
Stats st = mWorkingProcs.get(i);
printProcessCPU(pw, st.added ? " +" : (st.removed ? " -": " "),
st.pid, st.name, (int)st.rel_uptime,
diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java
index fb38bba..ca98b50 100644
--- a/core/java/com/android/internal/policy/PhoneWindow.java
+++ b/core/java/com/android/internal/policy/PhoneWindow.java
@@ -357,8 +357,7 @@
mLayoutInflater = LayoutInflater.from(context);
mRenderShadowsInCompositor = Settings.Global.getInt(context.getContentResolver(),
DEVELOPMENT_RENDER_SHADOWS_IN_COMPOSITOR, 1) != 0;
- mProxyOnBackInvokedDispatcher = new ProxyOnBackInvokedDispatcher(
- context.getApplicationInfo().isOnBackInvokedCallbackEnabled());
+ mProxyOnBackInvokedDispatcher = new ProxyOnBackInvokedDispatcher(context);
}
/**
@@ -2160,6 +2159,7 @@
/** Notify when decor view is attached to window and {@link ViewRootImpl} is available. */
void onViewRootImplSet(ViewRootImpl viewRoot) {
viewRoot.setActivityConfigCallback(mActivityConfigCallback);
+ viewRoot.getOnBackInvokedDispatcher().updateContext(getContext());
mProxyOnBackInvokedDispatcher.setActualDispatcher(viewRoot.getOnBackInvokedDispatcher());
applyDecorFitsSystemWindows();
}
diff --git a/core/java/com/android/internal/view/IImeTracker.aidl b/core/java/com/android/internal/view/IImeTracker.aidl
new file mode 100644
index 0000000..b062ca7
--- /dev/null
+++ b/core/java/com/android/internal/view/IImeTracker.aidl
@@ -0,0 +1,94 @@
+/*
+ * 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.view;
+
+import android.view.inputmethod.ImeTracker;
+
+/**
+ * Interface to the global Ime tracker, used by all client applications.
+ * {@hide}
+ */
+interface IImeTracker {
+
+ /**
+ * Called when an IME show request is created,
+ * returns a new Binder to be associated with the IME tracking token.
+ *
+ * @param uid the uid of the client that requested the IME.
+ * @param origin the origin of the IME show request.
+ * @param reason the reason why the IME show request was created.
+ */
+ IBinder onRequestShow(int uid, int origin, int reason);
+
+ /**
+ * Called when an IME hide request is created,
+ * returns a new Binder to be associated with the IME tracking token.
+ *
+ * @param uid the uid of the client that requested the IME.
+ * @param origin the origin of the IME hide request.
+ * @param reason the reason why the IME hide request was created.
+ */
+ IBinder onRequestHide(int uid, int origin, int reason);
+
+ /**
+ * Called when the IME request progresses to a further phase.
+ *
+ * @param statsToken the token tracking the current IME request.
+ * @param phase the new phase the IME request reached.
+ */
+ oneway void onProgress(in IBinder statsToken, int phase);
+
+ /**
+ * Called when the IME request fails.
+ *
+ * @param statsToken the token tracking the current IME request.
+ * @param phase the phase the IME request failed at.
+ */
+ oneway void onFailed(in IBinder statsToken, int phase);
+
+ /**
+ * Called when the IME request is cancelled.
+ *
+ * @param statsToken the token tracking the current IME request.
+ * @param phase the phase the IME request was cancelled at.
+ */
+ oneway void onCancelled(in IBinder statsToken, int phase);
+
+ /**
+ * Called when the IME show request is successful.
+ *
+ * @param statsToken the token tracking the current IME request.
+ */
+ oneway void onShown(in IBinder statsToken);
+
+ /**
+ * Called when the IME hide request is successful.
+ *
+ * @param statsToken the token tracking the current IME request.
+ */
+ oneway void onHidden(in IBinder statsToken);
+
+ /**
+ * Checks whether there are any pending IME visibility requests.
+ *
+ * @return {@code true} iff there are pending IME visibility requests.
+ */
+ @EnforcePermission("TEST_INPUT_METHOD")
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
+ + "android.Manifest.permission.TEST_INPUT_METHOD)")
+ boolean hasPendingImeVisibilityRequests();
+}
diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl
index 00bc3f2..2106426 100644
--- a/core/java/com/android/internal/view/IInputMethodManager.aidl
+++ b/core/java/com/android/internal/view/IInputMethodManager.aidl
@@ -27,6 +27,7 @@
import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection;
import com.android.internal.inputmethod.IRemoteInputConnection;
import com.android.internal.inputmethod.InputBindResult;
+import com.android.internal.view.IImeTracker;
/**
* Public interface to the global input method manager, used by all client
@@ -158,4 +159,10 @@
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
+ "android.Manifest.permission.TEST_INPUT_METHOD)")
void setStylusWindowIdleTimeoutForTest(in IInputMethodClient client, long timeout);
+
+ /**
+ * Returns the singleton instance for the Ime Tracker Service.
+ * {@hide}
+ */
+ IImeTracker getImeTrackerService();
}
diff --git a/core/jni/android_media_AudioDescriptor.h b/core/jni/android_media_AudioDescriptor.h
index 680ac3f..bac761c 100644
--- a/core/jni/android_media_AudioDescriptor.h
+++ b/core/jni/android_media_AudioDescriptor.h
@@ -25,6 +25,8 @@
// keep these values in sync with ExtraAudioDescriptor.java
#define STANDARD_NONE 0
#define STANDARD_EDID 1
+#define STANDARD_SADB 2
+#define STANDARD_VSADB 3
static inline status_t audioStandardFromNative(audio_standard_t nStandard, int* standard) {
status_t result = NO_ERROR;
@@ -35,6 +37,12 @@
case AUDIO_STANDARD_EDID:
*standard = STANDARD_EDID;
break;
+ case AUDIO_STANDARD_SADB:
+ *standard = STANDARD_SADB;
+ break;
+ case AUDIO_STANDARD_VSADB:
+ *standard = STANDARD_VSADB;
+ break;
default:
result = BAD_VALUE;
}
diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp
index 28ac464..75f8402 100644
--- a/core/jni/android_media_AudioSystem.cpp
+++ b/core/jni/android_media_AudioSystem.cpp
@@ -2739,12 +2739,26 @@
}
static jint android_media_AudioSystem_removeDevicesRoleForStrategy(JNIEnv *env, jobject thiz,
- jint strategy, jint role) {
+ jint strategy, jint role,
+ jintArray jDeviceTypes,
+ jobjectArray jDeviceAddresses) {
+ AudioDeviceTypeAddrVector nDevices;
+ jint results = getVectorOfAudioDeviceTypeAddr(env, jDeviceTypes, jDeviceAddresses, nDevices);
+ if (results != NO_ERROR) {
+ return results;
+ }
+ int status = check_AudioSystem_Command(
+ AudioSystem::removeDevicesRoleForStrategy((product_strategy_t)strategy,
+ (device_role_t)role, nDevices));
+ return (jint)status;
+}
+
+static jint android_media_AudioSystem_clearDevicesRoleForStrategy(JNIEnv *env, jobject thiz,
+ jint strategy, jint role) {
return (jint)
- check_AudioSystem_Command(AudioSystem::removeDevicesRoleForStrategy((product_strategy_t)
- strategy,
- (device_role_t)
- role),
+ check_AudioSystem_Command(AudioSystem::clearDevicesRoleForStrategy((product_strategy_t)
+ strategy,
+ (device_role_t)role),
{NAME_NOT_FOUND});
}
@@ -3201,6 +3215,30 @@
return nativeToJavaStatus(status);
}
+static jboolean android_media_AudioSystem_supportsBluetoothVariableLatency(JNIEnv *env,
+ jobject thiz) {
+ bool supports;
+ if (AudioSystem::supportsBluetoothVariableLatency(&supports) != NO_ERROR) {
+ supports = false;
+ }
+ return supports;
+}
+
+static int android_media_AudioSystem_setBluetoothVariableLatencyEnabled(JNIEnv *env, jobject thiz,
+ jboolean enabled) {
+ return (jint)check_AudioSystem_Command(
+ AudioSystem::setBluetoothVariableLatencyEnabled(enabled));
+}
+
+static jboolean android_media_AudioSystem_isBluetoothVariableLatencyEnabled(JNIEnv *env,
+ jobject thiz) {
+ bool enabled;
+ if (AudioSystem::isBluetoothVariableLatencyEnabled(&enabled) != NO_ERROR) {
+ enabled = false;
+ }
+ return enabled;
+}
+
// ----------------------------------------------------------------------------
static const JNINativeMethod gMethods[] =
@@ -3317,8 +3355,10 @@
(void *)android_media_AudioSystem_isCallScreeningModeSupported},
{"setDevicesRoleForStrategy", "(II[I[Ljava/lang/String;)I",
(void *)android_media_AudioSystem_setDevicesRoleForStrategy},
- {"removeDevicesRoleForStrategy", "(II)I",
+ {"removeDevicesRoleForStrategy", "(II[I[Ljava/lang/String;)I",
(void *)android_media_AudioSystem_removeDevicesRoleForStrategy},
+ {"clearDevicesRoleForStrategy", "(II)I",
+ (void *)android_media_AudioSystem_clearDevicesRoleForStrategy},
{"getDevicesForRoleAndStrategy", "(IILjava/util/List;)I",
(void *)android_media_AudioSystem_getDevicesForRoleAndStrategy},
{"setDevicesRoleForCapturePreset", "(II[I[Ljava/lang/String;)I",
@@ -3364,7 +3404,13 @@
{"getPreferredMixerAttributes", "(Landroid/media/AudioAttributes;ILjava/util/List;)I",
(void *)android_media_AudioSystem_getPreferredMixerAttributes},
{"clearPreferredMixerAttributes", "(Landroid/media/AudioAttributes;II)I",
- (void *)android_media_AudioSystem_clearPreferredMixerAttributes}};
+ (void *)android_media_AudioSystem_clearPreferredMixerAttributes},
+ {"supportsBluetoothVariableLatency", "()Z",
+ (void *)android_media_AudioSystem_supportsBluetoothVariableLatency},
+ {"setBluetoothVariableLatencyEnabled", "(Z)I",
+ (void *)android_media_AudioSystem_setBluetoothVariableLatencyEnabled},
+ {"isBluetoothVariableLatencyEnabled", "()Z",
+ (void *)android_media_AudioSystem_isBluetoothVariableLatencyEnabled}};
static const JNINativeMethod gEventHandlerMethods[] = {
{"native_setup",
diff --git a/core/jni/com_android_internal_os_KernelAllocationStats.cpp b/core/jni/com_android_internal_os_KernelAllocationStats.cpp
index 5b10497..35c9487 100644
--- a/core/jni/com_android_internal_os_KernelAllocationStats.cpp
+++ b/core/jni/com_android_internal_os_KernelAllocationStats.cpp
@@ -44,7 +44,7 @@
static jobjectArray KernelAllocationStats_getDmabufAllocations(JNIEnv *env, jobject) {
std::vector<DmaBuffer> buffers;
- if (!dmabufinfo::ReadDmaBufs(&buffers)) {
+ if (!dmabufinfo::ReadProcfsDmaBufs(&buffers)) {
return nullptr;
}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 6dd7073..e031e5f 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -258,6 +258,7 @@
android:name="com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_DELIVERY" />
<protected-broadcast
android:name="android.bluetooth.pan.profile.action.CONNECTION_STATE_CHANGED" />
+ <protected-broadcast android:name="android.bluetooth.action.HAP_CONNECTION_STATE_CHANGED" />
<protected-broadcast android:name="android.bluetooth.action.LE_AUDIO_CONNECTION_STATE_CHANGED" />
<protected-broadcast android:name="android.bluetooth.action.LE_AUDIO_ACTIVE_DEVICE_CHANGED" />
<protected-broadcast android:name="android.bluetooth.action.LE_AUDIO_CONF_CHANGED" />
@@ -4649,7 +4650,8 @@
<p>Apps need to target API {@link android.os.Build.VERSION_CODES#S} or above to be able to
request this permission. Note that apps targeting lower API levels do not need this
permission to use exact alarm APIs.
- <p>Apps that hold this permission, always stay in the
+ <p>Apps that hold this permission and target API
+ {@link android.os.Build.VERSION_CODES#TIRAMISU} and below always stay in the
{@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_WORKING_SET WORKING_SET} or
lower standby bucket.
<p>If your app relies on exact alarms for core functionality, it can instead request
@@ -4675,7 +4677,7 @@
<p> Apps need to target API {@link android.os.Build.VERSION_CODES#TIRAMISU} or above to be
able to request this permission. Note that only one of {@code USE_EXACT_ALARM} or
{@code SCHEDULE_EXACT_ALARM} should be requested on a device. If your app is already using
- {@code SCHEDULE_EXACT_ALARM} on older SDKs but need {@code USE_EXACT_ALARM} on SDK 33 and
+ {@code SCHEDULE_EXACT_ALARM} on older SDKs but needs {@code USE_EXACT_ALARM} on SDK 33 and
above, then {@code SCHEDULE_EXACT_ALARM} should be declared with a max-sdk attribute, like:
<pre>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"
@@ -7429,6 +7431,14 @@
android:permission="android.permission.BIND_JOB_SERVICE" >
</service>
+ <service android:name="com.android.server.companion.datatransfer.contextsync.CallMetadataSyncInCallService"
+ android:permission="android.permission.BIND_INCALL_SERVICE"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.telecom.InCallService"/>
+ </intent-filter>
+ </service>
+
<provider
android:name="com.android.server.textclassifier.IconsContentProvider"
android:authorities="com.android.textclassifier.icons"
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 1072f57..1d1c02d 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -9401,8 +9401,34 @@
<attr name="label" />
<!-- The key character map file resource. -->
<attr name="keyboardLayout" format="reference" />
- <!-- The locales the given keyboard layout corresponds to. -->
- <attr name="locale" format="string" />
+ <!-- The locales the given keyboard layout corresponds to. This is a list of
+ BCP-47 conformant language tags separated by the delimiter ',' or '|'.
+ Some examples of language tags are: en-US, zh-Hans-CN, el-Grek-polyton.
+ It includes information for language code, country code, variant, and script
+ code like ‘Latn’, ‘Cyrl’, etc. -->
+ <attr name="keyboardLocale" format="string"/>
+ <!-- The layout type of the given keyboardLayout.
+ NOTE: The enum to int value mapping must remain stable -->
+ <attr name="keyboardLayoutType" format="enum">
+ <!-- Qwerty-based keyboard layout. -->
+ <enum name="qwerty" value="1" />
+ <!-- Qwertz-based keyboard layout. -->
+ <enum name="qwertz" value="2" />
+ <!-- Azerty-based keyboard layout. -->
+ <enum name="azerty" value="3" />
+ <!-- Dvorak keyboard layout. -->
+ <enum name="dvorak" value="4" />
+ <!-- Colemak keyboard layout. -->
+ <enum name="colemak" value="5" />
+ <!-- Workman keyboard layout. -->
+ <enum name="workman" value="6" />
+ <!-- Turkish-Q keyboard layout. -->
+ <enum name="turkish_q" value="7" />
+ <!-- Turkish-F keyboard layout. -->
+ <enum name="turkish_f" value="8" />
+ <!-- Keyboard layout that has been enhanced with a large number of extra characters. -->
+ <enum name="extended" value="9" />
+ </attr>
<!-- The vendor ID of the hardware the given layout corresponds to. @hide -->
<attr name="vendorId" format="integer" />
<!-- The product ID of the hardware the given layout corresponds to. @hide -->
diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml
index 44f85da..8ac13ef 100644
--- a/core/res/res/values/attrs_manifest.xml
+++ b/core/res/res/values/attrs_manifest.xml
@@ -1027,6 +1027,9 @@
<flag name="layoutDirection" value="0x2000" />
<!-- The color mode of the screen has changed (color gamut or dynamic range). -->
<flag name="colorMode" value="0x4000" />
+ <!-- The grammatical gender has changed, for example the user set the grammatical gender
+ from the UI. -->
+ <flag name="grammaticalGender" value="0x8000" />
<!-- The font scaling factor has changed, that is the user has
selected a new global font size. -->
<flag name="fontScale" value="0x40000000" />
@@ -3201,6 +3204,16 @@
letters.
-->
<attr name="requiredDisplayCategory" format="string"/>
+ <!-- If false, {@link android.view.KeyEvent#KEYCODE_BACK KEYCODE_BACK} and
+ {@link android.app.Activity#onBackPressed Activity.onBackPressed()}
+ and related event will be forwarded to the Activity and its views.
+
+ <p> If true, those events will be replaced by a call to
+ {@link android.window.OnBackInvokedCallback#onBackInvoked} on the focused window.
+
+ <p> By default, the behavior is configured by the same attribute in application.
+ -->
+ <attr name="enableOnBackInvokedCallback" format="boolean"/>
</declare-styleable>
<!-- The <code>activity-alias</code> tag declares a new
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 9cf9501..184f869 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -2672,9 +2672,9 @@
will be locked. -->
<bool name="config_multiuserDelayUserDataLocking">false</bool>
- <!-- Whether the device allows users to start in background on secondary displays.
+ <!-- Whether the device allows users to start in background visible on displays.
Should be false for most devices, except automotive vehicle with passenger displays. -->
- <bool name="config_multiuserUsersOnSecondaryDisplays">false</bool>
+ <bool name="config_multiuserVisibleBackgroundUsers">false</bool>
<!-- Whether to automatically switch to the designated Dock User (the user chosen for
displaying dreams, etc.) after a timeout when the device is docked. -->
@@ -2701,6 +2701,10 @@
<!-- Whether UI for multi user should be shown -->
<bool name="config_enableMultiUserUI">false</bool>
+ <!-- Whether multiple admins are allowed on the device. If set to true, new users can be created
+ with admin privileges and admin privileges can be granted/revoked from existing users. -->
+ <bool name="config_enableMultipleAdmins">false</bool>
+
<!-- Whether the new Auto Selection Network UI should be shown -->
<bool name="config_enableNewAutoSelectNetworkUI">false</bool>
@@ -4154,12 +4158,34 @@
-->
<string name="config_defaultAmbientContextDetectionService" translatable="false"></string>
+ <!-- The component names for the system's AmbientContextEvent detection services.
+ These services must be trusted, as it can be activated without explicit consent of the user.
+ See android.service.ambientcontext.AmbientContextDetectionService and
+ android.service.wearable.WearableSensingService.
+ -->
+ <string-array name="config_defaultAmbientContextServices">
+ <item>@string/config_defaultAmbientContextDetectionService</item>
+ <item>@string/config_defaultWearableSensingService</item>
+ </string-array>
+
<!-- The component name for the default system wearable sensing service.
This service must be trusted, as it can be activated without explicit consent of the user.
See android.service.wearable.WearableSensingService.
-->
<string name="config_defaultWearableSensingService" translatable="false"></string>
+ <!-- Component name that accepts ACTION_SEND intents for requesting ambient context consent for
+ wearable sensing. -->
+ <string translatable="false" name="config_defaultWearableSensingConsentComponent"></string>
+
+ <!-- Intent extra key for the caller's package name while requesting
+ ambient context consent for wearable sensing. -->
+ <string translatable="false" name="config_wearableAmbientContextPackageNameExtraKey"></string>
+
+ <!-- Intent extra key for the event code int array while requesting
+ ambient context consent for wearable sensing. -->
+ <string translatable="false" name="config_wearableAmbientContextEventArrayExtraKey"></string>
+
<!-- Component name that accepts ACTION_SEND intents for requesting ambient context consent. -->
<string translatable="false" name="config_defaultAmbientContextConsentComponent"></string>
@@ -5204,6 +5230,15 @@
the format [System DeviceState]:[WM Jetpack Posture], for example: "0:1". -->
<string-array name="config_device_state_postures" translatable="false" />
+ <!-- Which Surface rotations are considered as tabletop posture (horizontal hinge) when the
+ device is half-folded. Other half-folded postures will be assumed to be book (vertical
+ hinge) mode. Units: degrees; valid values: 0, 90, 180, 270. -->
+ <integer-array name="config_deviceTabletopRotations" />
+
+ <!-- This flag indicates that a display with fold-state FLAT should always be considered as
+ having a separating hinge. -->
+ <bool name="config_isDisplayHingeAlwaysSeparating">false</bool>
+
<!-- Aspect ratio of letterboxing for fixed orientation. Values <= 1.0 will be ignored.
Note: Activity min/max aspect ratio restrictions will still be respected.
Therefore this override can control the maximum screen area that can be occupied by
@@ -5252,14 +5287,26 @@
<!-- Horizontal position of a center of the letterboxed app window.
0 corresponds to the left side of the screen and 1 to the right side. If given value < 0
- or > 1, it is ignored and central position is used (0.5). -->
+ or > 1 it is ignored and for non-book mode central position is used (0.5); for book mode
+ left is used (0.0). -->
<item name="config_letterboxHorizontalPositionMultiplier" format="float" type="dimen">0.5</item>
<!-- Vertical position of a center of the letterboxed app window.
0 corresponds to the upper side of the screen and 1 to the lower side. If given value < 0
- or > 1, it is ignored and central position is used (0.5). -->
+ or > 1 it is ignored and for non-tabletop mode central position is used (0.5); for
+ tabletop mode top (0.0) is used. -->
<item name="config_letterboxVerticalPositionMultiplier" format="float" type="dimen">0.0</item>
+ <!-- Horizontal position of a center of the letterboxed app window when in book mode.
+ 0 corresponds to the left side of the screen and 1 to the right side. If given value < 0
+ or > 1, it is ignored and left position is used (0.0). -->
+ <item name="config_letterboxBookModePositionMultiplier" format="float" type="dimen">0.0</item>
+
+ <!-- Vertical position of a center of the letterboxed app window when in tabletop mode.
+ 0 corresponds to the upper side of the screen and 1 to the lower side. If given value < 0
+ or > 1, it is ignored and top position is used (0.0). -->
+ <item name="config_letterboxTabletopModePositionMultiplier" format="float" type="dimen">0.0</item>
+
<!-- Whether horizontal reachability repositioning is allowed for letterboxed fullscreen apps.
-->
<bool name="config_letterboxIsHorizontalReachabilityEnabled">false</bool>
@@ -5287,6 +5334,26 @@
If given value is outside of this range, the option 1 (center) is assummed. -->
<integer name="config_letterboxDefaultPositionForVerticalReachability">1</integer>
+ <!-- Default horizontal position of the letterboxed app window when reachability is
+ enabled and an app is fullscreen in landscape device orientation and in book mode. When
+ reachability is enabled, the position can change between left, center and right. This config
+ defines the default one:
+ - Option 0 - Left.
+ - Option 1 - Center.
+ - Option 2 - Right.
+ If given value is outside of this range, the option 0 (left) is assummed. -->
+ <integer name="config_letterboxDefaultPositionForBookModeReachability">0</integer>
+
+ <!-- Default vertical position of the letterboxed app window when reachability is
+ enabled and an app is fullscreen in portrait device orientation and in tabletop mode. When
+ reachability is enabled, the position can change between top, center and bottom. This config
+ defines the default one:
+ - Option 0 - Top.
+ - Option 1 - Center.
+ - Option 2 - Bottom.
+ If given value is outside of this range, the option 0 (top) is assummed. -->
+ <integer name="config_letterboxDefaultPositionForTabletopModeReachability">0</integer>
+
<!-- Whether displaying letterbox education is enabled for letterboxed fullscreen apps. -->
<bool name="config_letterboxIsEducationEnabled">false</bool>
@@ -5303,6 +5370,17 @@
TODO(b/255532890) Enable when ignoreOrientationRequest is set -->
<bool name="config_letterboxIsEnabledForTranslucentActivities">false</bool>
+ <!-- Whether sending compat fake focus for split screen resumed activities is enabled.
+ Needed because some game engines wait to get focus before drawing the content of
+ the app which isn't guaranteed by default in multi-window modes. -->
+ <bool name="config_isCompatFakeFocusEnabled">false</bool>
+
+ <!-- Whether camera compat treatment is enabled for issues caused by orientation mismatch
+ between camera buffers and an app window. This includes force rotation of fixed
+ orientation activities connected to the camera in fullscreen and showing a tooltip in
+ split screen. -->
+ <bool name="config_isWindowManagerCameraCompatTreatmentEnabled">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/public-staging.xml b/core/res/res/values/public-staging.xml
index 2924ddf..97feaac 100644
--- a/core/res/res/values/public-staging.xml
+++ b/core/res/res/values/public-staging.xml
@@ -122,6 +122,8 @@
<public name="physicalKeyboardHintLanguageTag" />
<public name="physicalKeyboardHintLayoutType" />
<public name="allowSharedIsolatedProcess" />
+ <public name="keyboardLocale" />
+ <public name="keyboardLayoutType" />
</staging-public-group>
<staging-public-group type="id" first-id="0x01cd0000">
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 068bb91..af29b23 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -353,6 +353,7 @@
<java-symbol type="bool" name="config_speed_up_audio_on_mt_calls" />
<java-symbol type="bool" name="config_useFixedVolume" />
<java-symbol type="bool" name="config_enableMultiUserUI"/>
+ <java-symbol type="bool" name="config_enableMultipleAdmins"/>
<java-symbol type="bool" name="config_enableNewAutoSelectNetworkUI"/>
<java-symbol type="bool" name="config_disableUsbPermissionDialogs"/>
<java-symbol type="dimen" name="config_highResTaskSnapshotScale" />
@@ -468,7 +469,7 @@
<java-symbol type="integer" name="config_multiuserMaximumUsers" />
<java-symbol type="integer" name="config_multiuserMaxRunningUsers" />
<java-symbol type="bool" name="config_multiuserDelayUserDataLocking" />
- <java-symbol type="bool" name="config_multiuserUsersOnSecondaryDisplays" />
+ <java-symbol type="bool" name="config_multiuserVisibleBackgroundUsers" />
<java-symbol type="bool" name="config_enableTimeoutToDockUserWhenDocked" />
<java-symbol type="integer" name="config_userTypePackageWhitelistMode"/>
<java-symbol type="xml" name="config_user_types" />
@@ -3704,6 +3705,10 @@
<java-symbol type="string" name="config_defaultSystemCaptionsManagerService" />
<java-symbol type="string" name="config_defaultAmbientContextDetectionService" />
<java-symbol type="string" name="config_defaultAmbientContextConsentComponent" />
+ <java-symbol type="string" name="config_defaultWearableSensingConsentComponent" />
+ <java-symbol type="string" name="config_wearableAmbientContextPackageNameExtraKey" />
+ <java-symbol type="string" name="config_wearableAmbientContextEventArrayExtraKey" />
+ <java-symbol type="array" name="config_defaultAmbientContextServices" />
<java-symbol type="string" name="config_ambientContextPackageNameExtraKey" />
<java-symbol type="string" name="config_ambientContextEventArrayExtraKey" />
<java-symbol type="string" name="config_defaultWearableSensingService" />
@@ -4403,6 +4408,8 @@
<java-symbol type="array" name="config_keep_warming_services" />
<java-symbol type="string" name="config_display_features" />
<java-symbol type="array" name="config_device_state_postures" />
+ <java-symbol type="array" name="config_deviceTabletopRotations" />
+ <java-symbol type="bool" name="config_isDisplayHingeAlwaysSeparating" />
<java-symbol type="dimen" name="controls_thumbnail_image_max_height" />
<java-symbol type="dimen" name="controls_thumbnail_image_max_width" />
@@ -4415,13 +4422,19 @@
<java-symbol type="color" name="config_letterboxBackgroundColor" />
<java-symbol type="dimen" name="config_letterboxHorizontalPositionMultiplier" />
<java-symbol type="dimen" name="config_letterboxVerticalPositionMultiplier" />
+ <java-symbol type="dimen" name="config_letterboxBookModePositionMultiplier" />
+ <java-symbol type="dimen" name="config_letterboxTabletopModePositionMultiplier" />
<java-symbol type="bool" name="config_letterboxIsHorizontalReachabilityEnabled" />
<java-symbol type="bool" name="config_letterboxIsVerticalReachabilityEnabled" />
<java-symbol type="integer" name="config_letterboxDefaultPositionForHorizontalReachability" />
<java-symbol type="integer" name="config_letterboxDefaultPositionForVerticalReachability" />
+ <java-symbol type="integer" name="config_letterboxDefaultPositionForBookModeReachability" />
+ <java-symbol type="integer" name="config_letterboxDefaultPositionForTabletopModeReachability" />
<java-symbol type="bool" name="config_letterboxIsEducationEnabled" />
<java-symbol type="dimen" name="config_letterboxDefaultMinAspectRatioForUnresizableApps" />
<java-symbol type="bool" name="config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled" />
+ <java-symbol type="bool" name="config_isCompatFakeFocusEnabled" />
+ <java-symbol type="bool" name="config_isWindowManagerCameraCompatTreatmentEnabled" />
<java-symbol type="bool" name="config_isCameraCompatControlForStretchedIssuesEnabled" />
<java-symbol type="bool" name="config_hideDisplayCutoutWithDisplayArea" />
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 87f91fa..9a999e4 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
@@ -72,9 +72,9 @@
/* value= */ 94300);
private static final ProgramSelector.Identifier RDS_IDENTIFIER =
new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_RDS_PI, 15019);
- private static final ProgramSelector.Identifier DAB_SID_EXT_IDENTIFIER =
- new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT,
- /* value= */ 0x10000111);
+ private static final ProgramSelector.Identifier DAB_DMB_SID_EXT_IDENTIFIER =
+ new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT,
+ /* value= */ 0xA000000111L);
private static final ProgramSelector.Identifier DAB_ENSEMBLE_IDENTIFIER =
new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE,
/* value= */ 0x1013);
@@ -89,7 +89,7 @@
private static final ProgramList.Chunk FM_RDS_ADD_CHUNK = new ProgramList.Chunk(IS_PURGE,
IS_COMPLETE, Set.of(FM_PROGRAM_INFO, RDS_PROGRAM_INFO),
- Set.of(DAB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER));
+ Set.of(DAB_DMB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER));
private static final ProgramList.Chunk FM_ADD_INCOMPLETE_CHUNK = new ProgramList.Chunk(IS_PURGE,
/* complete= */ false, Set.of(FM_PROGRAM_INFO), new ArraySet<>());
private static final ProgramList.Filter TEST_FILTER = new ProgramList.Filter(
@@ -216,7 +216,7 @@
public void isPurge_forChunk() {
ProgramList.Chunk chunk = new ProgramList.Chunk(IS_PURGE, IS_COMPLETE,
Set.of(FM_PROGRAM_INFO, RDS_PROGRAM_INFO),
- Set.of(DAB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER));
+ Set.of(DAB_DMB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER));
assertWithMessage("Puring chunk").that(chunk.isPurge()).isEqualTo(IS_PURGE);
}
@@ -225,7 +225,7 @@
public void isComplete_forChunk() {
ProgramList.Chunk chunk = new ProgramList.Chunk(IS_PURGE, IS_COMPLETE,
Set.of(FM_PROGRAM_INFO, RDS_PROGRAM_INFO),
- Set.of(DAB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER));
+ Set.of(DAB_DMB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER));
assertWithMessage("Complete chunk").that(chunk.isComplete()).isEqualTo(IS_COMPLETE);
}
@@ -234,7 +234,7 @@
public void getModified_forChunk() {
ProgramList.Chunk chunk = new ProgramList.Chunk(IS_PURGE, IS_COMPLETE,
Set.of(FM_PROGRAM_INFO, RDS_PROGRAM_INFO),
- Set.of(DAB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER));
+ Set.of(DAB_DMB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER));
assertWithMessage("Modified program info in chunk")
.that(chunk.getModified()).containsExactly(FM_PROGRAM_INFO, RDS_PROGRAM_INFO);
@@ -244,10 +244,10 @@
public void getRemoved_forChunk() {
ProgramList.Chunk chunk = new ProgramList.Chunk(IS_PURGE, IS_COMPLETE,
Set.of(FM_PROGRAM_INFO, RDS_PROGRAM_INFO),
- Set.of(DAB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER));
+ Set.of(DAB_DMB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER));
assertWithMessage("Removed program identifiers in chunk").that(chunk.getRemoved())
- .containsExactly(DAB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER);
+ .containsExactly(DAB_DMB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER);
}
@Test
@@ -276,6 +276,19 @@
}
@Test
+ public void getProgramList_forTunerAdapterWhenServiceDied_fails() throws Exception {
+ Map<String, String> parameters = Map.of("ParameterKeyMock", "ParameterValueMock");
+ createRadioTuner();
+ doThrow(new RemoteException()).when(mTunerMock).startProgramListUpdates(any());
+
+ RuntimeException thrown = assertThrows(RuntimeException.class,
+ () -> mRadioTuner.getProgramList(parameters));
+
+ assertWithMessage("Exception for getting program list when service is dead")
+ .that(thrown).hasMessageThat().contains("Service died");
+ }
+
+ @Test
public void getDynamicProgramList_forTunerAdapter() throws Exception {
createRadioTuner();
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramSelectorTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramSelectorTest.java
index 5bd018b..9399907 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramSelectorTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramSelectorTest.java
@@ -36,18 +36,18 @@
private static final long AM_FREQUENCY = 700;
private static final ProgramSelector.Identifier FM_IDENTIFIER = new ProgramSelector.Identifier(
ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, FM_FREQUENCY);
- private static final ProgramSelector.Identifier DAB_SID_EXT_IDENTIFIER_1 =
- new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT,
- /* value= */ 0x1000011);
- private static final ProgramSelector.Identifier DAB_SID_EXT_IDENTIFIER_2 =
- new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT,
- /* value= */ 0x10000112);
+ private static final ProgramSelector.Identifier DAB_DMB_SID_EXT_IDENTIFIER_1 =
+ new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT,
+ /* value= */ 0xA000000111L);
+ private static final ProgramSelector.Identifier DAB_DMB_SID_EXT_IDENTIFIER_2 =
+ new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT,
+ /* value= */ 0xA000000112L);
private static final ProgramSelector.Identifier DAB_ENSEMBLE_IDENTIFIER =
new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE,
/* value= */ 0x1001);
private static final ProgramSelector.Identifier DAB_FREQUENCY_IDENTIFIER =
new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY,
- /* value= */ 94500);
+ /* value= */ 220352);
@Test
public void getType_forIdentifier() {
@@ -80,13 +80,13 @@
@Test
public void equals_withDifferentTypesForIdentifiers_returnsFalse() {
assertWithMessage("Identifier with different identifier type")
- .that(FM_IDENTIFIER).isNotEqualTo(DAB_SID_EXT_IDENTIFIER_1);
+ .that(FM_IDENTIFIER).isNotEqualTo(DAB_DMB_SID_EXT_IDENTIFIER_1);
}
@Test
public void equals_withDifferentValuesForIdentifiers_returnsFalse() {
assertWithMessage("Identifier with different identifier value")
- .that(DAB_SID_EXT_IDENTIFIER_2).isNotEqualTo(DAB_SID_EXT_IDENTIFIER_1);
+ .that(DAB_DMB_SID_EXT_IDENTIFIER_2).isNotEqualTo(DAB_DMB_SID_EXT_IDENTIFIER_1);
}
@Test
@@ -168,19 +168,19 @@
@Test
public void getFirstId_withIdInSelector() {
ProgramSelector.Identifier[] secondaryIds = new ProgramSelector.Identifier[]{
- DAB_ENSEMBLE_IDENTIFIER, DAB_SID_EXT_IDENTIFIER_2, DAB_FREQUENCY_IDENTIFIER};
+ DAB_ENSEMBLE_IDENTIFIER, DAB_DMB_SID_EXT_IDENTIFIER_2, DAB_FREQUENCY_IDENTIFIER};
ProgramSelector selector = getDabSelector(secondaryIds, /* vendorIds= */ null);
- long firstIdValue = selector.getFirstId(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT);
+ long firstIdValue = selector.getFirstId(ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT);
assertWithMessage("Value of the first DAB_SID_EXT identifier")
- .that(firstIdValue).isEqualTo(DAB_SID_EXT_IDENTIFIER_1.getValue());
+ .that(firstIdValue).isEqualTo(DAB_DMB_SID_EXT_IDENTIFIER_1.getValue());
}
@Test
public void getFirstId_withIdNotInSelector() {
ProgramSelector.Identifier[] secondaryIds = new ProgramSelector.Identifier[]{
- DAB_ENSEMBLE_IDENTIFIER, DAB_SID_EXT_IDENTIFIER_2};
+ DAB_ENSEMBLE_IDENTIFIER, DAB_DMB_SID_EXT_IDENTIFIER_2};
ProgramSelector selector = getDabSelector(secondaryIds, /* vendorIds= */ null);
int idType = ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY;
@@ -195,13 +195,13 @@
@Test
public void getAllIds_withIdInSelector() {
ProgramSelector.Identifier[] secondaryIds = new ProgramSelector.Identifier[]{
- DAB_ENSEMBLE_IDENTIFIER, DAB_SID_EXT_IDENTIFIER_2, DAB_FREQUENCY_IDENTIFIER};
+ DAB_ENSEMBLE_IDENTIFIER, DAB_DMB_SID_EXT_IDENTIFIER_2, DAB_FREQUENCY_IDENTIFIER};
ProgramSelector.Identifier[] allIdsExpected =
- {DAB_SID_EXT_IDENTIFIER_1, DAB_SID_EXT_IDENTIFIER_2};
+ {DAB_DMB_SID_EXT_IDENTIFIER_1, DAB_DMB_SID_EXT_IDENTIFIER_2};
ProgramSelector selector = getDabSelector(secondaryIds, /* vendorIds= */ null);
ProgramSelector.Identifier[] allIds =
- selector.getAllIds(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT);
+ selector.getAllIds(ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT);
assertWithMessage("All DAB_SID_EXT identifiers in selector")
.that(allIds).isEqualTo(allIdsExpected);
@@ -244,14 +244,14 @@
@Test
public void withSecondaryPreferred() {
ProgramSelector.Identifier[] secondaryIds = new ProgramSelector.Identifier[]{
- DAB_ENSEMBLE_IDENTIFIER, DAB_SID_EXT_IDENTIFIER_2, DAB_FREQUENCY_IDENTIFIER};
+ DAB_ENSEMBLE_IDENTIFIER, DAB_DMB_SID_EXT_IDENTIFIER_2, DAB_FREQUENCY_IDENTIFIER};
long[] vendorIdsExpected = {12345, 678};
ProgramSelector selector = getDabSelector(secondaryIds, vendorIdsExpected);
ProgramSelector.Identifier[] secondaryIdsExpected = new ProgramSelector.Identifier[]{
- DAB_ENSEMBLE_IDENTIFIER, DAB_FREQUENCY_IDENTIFIER, DAB_SID_EXT_IDENTIFIER_1};
+ DAB_ENSEMBLE_IDENTIFIER, DAB_FREQUENCY_IDENTIFIER, DAB_DMB_SID_EXT_IDENTIFIER_1};
ProgramSelector selectorPreferred =
- selector.withSecondaryPreferred(DAB_SID_EXT_IDENTIFIER_1);
+ selector.withSecondaryPreferred(DAB_DMB_SID_EXT_IDENTIFIER_1);
assertWithMessage("Program type")
.that(selectorPreferred.getProgramType()).isEqualTo(selector.getProgramType());
@@ -458,7 +458,7 @@
private ProgramSelector getDabSelector(@Nullable ProgramSelector.Identifier[] secondaryIds,
@Nullable long[] vendorIds) {
- return new ProgramSelector(DAB_PROGRAM_TYPE, DAB_SID_EXT_IDENTIFIER_1, secondaryIds,
+ return new ProgramSelector(DAB_PROGRAM_TYPE, DAB_DMB_SID_EXT_IDENTIFIER_1, secondaryIds,
vendorIds);
}
}
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 03742eb..afbf8c3 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,7 +18,9 @@
import static com.google.common.truth.Truth.assertWithMessage;
+import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
@@ -105,17 +107,17 @@
private static final int INFO_FLAGS = 0b110001;
private static final int SIGNAL_QUALITY = 2;
private static final ProgramSelector.Identifier DAB_SID_EXT_IDENTIFIER =
- new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT,
- /* value= */ 0x10000111);
+ new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT,
+ /* value= */ 0xA000000111L);
private static final ProgramSelector.Identifier DAB_SID_EXT_IDENTIFIER_RELATED =
- new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT,
- /* value= */ 0x10000113);
+ new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT,
+ /* value= */ 0xA000000113L);
private static final ProgramSelector.Identifier DAB_ENSEMBLE_IDENTIFIER =
new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE,
/* value= */ 0x1013);
private static final ProgramSelector.Identifier DAB_FREQUENCY_IDENTIFIER =
new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY,
- /* value= */ 95500);
+ /* value= */ 220352);
private static final ProgramSelector DAB_SELECTOR =
new ProgramSelector(ProgramSelector.PROGRAM_TYPE_DAB, DAB_SID_EXT_IDENTIFIER,
new ProgramSelector.Identifier[]{
@@ -859,13 +861,13 @@
@Test
public void getLogicallyTunedTo_forProgramInfo() {
assertWithMessage("Identifier logically tuned to in DAB program info")
- .that(DAB_PROGRAM_INFO.getLogicallyTunedTo()).isEqualTo(DAB_FREQUENCY_IDENTIFIER);
+ .that(DAB_PROGRAM_INFO.getLogicallyTunedTo()).isEqualTo(DAB_SID_EXT_IDENTIFIER);
}
@Test
public void getPhysicallyTunedTo_forProgramInfo() {
assertWithMessage("Identifier physically tuned to DAB program info")
- .that(DAB_PROGRAM_INFO.getPhysicallyTunedTo()).isEqualTo(DAB_SID_EXT_IDENTIFIER);
+ .that(DAB_PROGRAM_INFO.getPhysicallyTunedTo()).isEqualTo(DAB_FREQUENCY_IDENTIFIER);
}
@Test
@@ -1006,6 +1008,35 @@
}
@Test
+ public void listModules_forRadioManagerWithNullListAsInput_fails() throws Exception {
+ createRadioManager();
+
+ assertWithMessage("Status when listing module with empty list input")
+ .that(mRadioManager.listModules(null)).isEqualTo(RadioManager.STATUS_BAD_VALUE);
+ }
+
+ @Test
+ public void listModules_withNullListFromService_fails() throws Exception {
+ createRadioManager();
+ when(mRadioServiceMock.listModules()).thenReturn(null);
+ List<RadioManager.ModuleProperties> modules = new ArrayList<>();
+
+ assertWithMessage("Status for listing module when getting null list from HAL client")
+ .that(mRadioManager.listModules(modules)).isEqualTo(RadioManager.STATUS_ERROR);
+ }
+
+ @Test
+ public void listModules_whenServiceDied_fails() throws Exception {
+ createRadioManager();
+ when(mRadioServiceMock.listModules()).thenThrow(new RemoteException());
+ List<RadioManager.ModuleProperties> modules = new ArrayList<>();
+
+ assertWithMessage("Status for listing module when HAL client service is dead")
+ .that(mRadioManager.listModules(modules))
+ .isEqualTo(RadioManager.STATUS_DEAD_OBJECT);
+ }
+
+ @Test
public void openTuner_forRadioModule() throws Exception {
createRadioManager();
int moduleId = 0;
@@ -1019,6 +1050,18 @@
}
@Test
+ public void openTuner_whenServiceDied_returnsNull() throws Exception {
+ createRadioManager();
+ when(mRadioServiceMock.openTuner(anyInt(), any(), anyBoolean(), any(), anyInt()))
+ .thenThrow(new RemoteException());
+
+ RadioTuner nullTuner = mRadioManager.openTuner(/* moduleId= */ 0, FM_BAND_CONFIG,
+ /* withAudio= */ true, mCallbackMock, /* handler= */ null);
+
+ assertWithMessage("Radio tuner when service is dead").that(nullTuner).isNull();
+ }
+
+ @Test
public void addAnnouncementListener_withListenerNotAddedBefore() throws Exception {
createRadioManager();
Set<Integer> enableTypeSet = createAnnouncementTypeSet(EVENT_ANNOUNCEMENT_TYPE);
@@ -1049,6 +1092,21 @@
}
@Test
+ public void addAnnouncementListener_whenServiceDied_throwException() throws Exception {
+ createRadioManager();
+ String exceptionMessage = "service is dead";
+ when(mRadioServiceMock.addAnnouncementListener(any(), any()))
+ .thenThrow(new RemoteException(exceptionMessage));
+ Set<Integer> enableTypeSet = createAnnouncementTypeSet(EVENT_ANNOUNCEMENT_TYPE);
+
+ RuntimeException thrown = assertThrows(RuntimeException.class,
+ () -> mRadioManager.addAnnouncementListener(enableTypeSet, mEventListener));
+
+ assertWithMessage("Exception for adding announcement listener with dead service")
+ .that(thrown).hasMessageThat().contains(exceptionMessage);
+ }
+
+ @Test
public void removeAnnouncementListener_withListenerNotAddedBefore_ignores() throws Exception {
createRadioManager();
@@ -1104,8 +1162,8 @@
}
private static RadioManager.ProgramInfo createDabProgramInfo(ProgramSelector selector) {
- return new RadioManager.ProgramInfo(selector, DAB_FREQUENCY_IDENTIFIER,
- DAB_SID_EXT_IDENTIFIER, Arrays.asList(DAB_SID_EXT_IDENTIFIER_RELATED), INFO_FLAGS,
+ return new RadioManager.ProgramInfo(selector, DAB_SID_EXT_IDENTIFIER,
+ DAB_FREQUENCY_IDENTIFIER, Arrays.asList(DAB_SID_EXT_IDENTIFIER_RELATED), INFO_FLAGS,
SIGNAL_QUALITY, METADATA, /* vendorInfo= */ null);
}
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 d851a7724..c8b4493 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
@@ -18,10 +18,13 @@
import static com.google.common.truth.Truth.assertWithMessage;
+import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -37,6 +40,7 @@
import android.hardware.radio.RadioMetadata;
import android.hardware.radio.RadioTuner;
import android.os.Build;
+import android.os.RemoteException;
import org.junit.After;
import org.junit.Before;
@@ -46,7 +50,6 @@
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
-import java.util.Arrays;
import java.util.List;
import java.util.Map;
@@ -131,6 +134,24 @@
}
@Test
+ public void setConfiguration_withInvalidParameters_fails() throws Exception {
+ doThrow(new IllegalArgumentException()).when(mTunerMock).setConfiguration(any());
+
+ assertWithMessage("Status for setting configuration with invalid parameters")
+ .that(mRadioTuner.setConfiguration(TEST_BAND_CONFIG))
+ .isEqualTo(RadioManager.STATUS_BAD_VALUE);
+ }
+
+ @Test
+ public void setConfiguration_whenServiceDied_fails() throws Exception {
+ doThrow(new RemoteException()).when(mTunerMock).setConfiguration(any());
+
+ assertWithMessage("Status for setting configuration when service is dead")
+ .that(mRadioTuner.setConfiguration(TEST_BAND_CONFIG))
+ .isEqualTo(RadioManager.STATUS_DEAD_OBJECT);
+ }
+
+ @Test
public void getConfiguration_forTunerAdapter() throws Exception {
when(mTunerMock.getConfiguration()).thenReturn(TEST_BAND_CONFIG);
RadioManager.BandConfig[] bandConfigs = new RadioManager.BandConfig[1];
@@ -144,14 +165,52 @@
}
@Test
+ public void getConfiguration_withInvalidParameters_fails() throws Exception {
+ RadioManager.BandConfig[] bandConfigs = new RadioManager.BandConfig[0];
+
+ IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class,
+ () -> mRadioTuner.getConfiguration(bandConfigs));
+
+ assertWithMessage("Exception for getting configuration with invalid parameters")
+ .that(thrown).hasMessageThat().contains("must be an array of length 1");
+ }
+
+ @Test
+ public void getConfiguration_whenServiceDied_fails() throws Exception {
+ doThrow(new RemoteException()).when(mTunerMock).getConfiguration();
+ RadioManager.BandConfig[] bandConfigs = new RadioManager.BandConfig[1];
+
+ assertWithMessage("Status for getting configuration when service is dead")
+ .that(mRadioTuner.getConfiguration(bandConfigs))
+ .isEqualTo(RadioManager.STATUS_DEAD_OBJECT);
+ }
+
+ @Test
public void setMute_forTunerAdapter() {
- int status = mRadioTuner.setMute(/* mute= */ true);
+ int status = mRadioTuner.setMute(true);
assertWithMessage("Status for setting mute")
.that(status).isEqualTo(RadioManager.STATUS_OK);
}
@Test
+ public void setMute_whenIllegalState_fails() throws Exception {
+ doThrow(new IllegalStateException()).when(mTunerMock).setMuted(anyBoolean());
+
+ assertWithMessage("Status for setting muted when service is in illegal state")
+ .that(mRadioTuner.setMute(true)).isEqualTo(RadioManager.STATUS_ERROR);
+ }
+
+ @Test
+ public void setMute_whenServiceDied_fails() throws Exception {
+ doThrow(new RemoteException()).when(mTunerMock).setMuted(anyBoolean());
+
+ assertWithMessage("Status for setting muted when service is dead")
+ .that(mRadioTuner.setMute(true))
+ .isEqualTo(RadioManager.STATUS_DEAD_OBJECT);
+ }
+
+ @Test
public void getMute_forTunerAdapter() throws Exception {
when(mTunerMock.isMuted()).thenReturn(true);
@@ -161,6 +220,14 @@
}
@Test
+ public void getMute_whenServiceDied_returnsTrue() throws Exception {
+ when(mTunerMock.isMuted()).thenThrow(new RemoteException());
+
+ assertWithMessage("Status for getting muted when service is dead")
+ .that(mRadioTuner.getMute()).isEqualTo(true);
+ }
+
+ @Test
public void step_forTunerAdapter_succeeds() throws Exception {
doAnswer(invocation -> {
mTunerCallback.onCurrentProgramInfoChanged(FM_PROGRAM_INFO);
@@ -176,6 +243,24 @@
}
@Test
+ public void step_whenIllegalState_fails() throws Exception {
+ doThrow(new IllegalStateException()).when(mTunerMock).step(anyBoolean(), anyBoolean());
+
+ assertWithMessage("Status for stepping when service is in illegal state")
+ .that(mRadioTuner.step(RadioTuner.DIRECTION_UP, /* skipSubChannel= */ false))
+ .isEqualTo(RadioManager.STATUS_INVALID_OPERATION);
+ }
+
+ @Test
+ public void step_whenServiceDied_fails() throws Exception {
+ doThrow(new RemoteException()).when(mTunerMock).step(anyBoolean(), anyBoolean());
+
+ assertWithMessage("Status for stepping when service is dead")
+ .that(mRadioTuner.step(RadioTuner.DIRECTION_UP, /* skipSubChannel= */ false))
+ .isEqualTo(RadioManager.STATUS_DEAD_OBJECT);
+ }
+
+ @Test
public void scan_forTunerAdapter_succeeds() throws Exception {
doAnswer(invocation -> {
mTunerCallback.onCurrentProgramInfoChanged(FM_PROGRAM_INFO);
@@ -191,13 +276,31 @@
}
@Test
+ public void scan_whenIllegalState_fails() throws Exception {
+ doThrow(new IllegalStateException()).when(mTunerMock).seek(anyBoolean(), anyBoolean());
+
+ assertWithMessage("Status for scanning when service is in illegal state")
+ .that(mRadioTuner.scan(RadioTuner.DIRECTION_UP, /* skipSubChannel= */ false))
+ .isEqualTo(RadioManager.STATUS_INVALID_OPERATION);
+ }
+
+ @Test
+ public void scan_whenServiceDied_fails() throws Exception {
+ doThrow(new RemoteException()).when(mTunerMock).seek(anyBoolean(), anyBoolean());
+
+ assertWithMessage("Status for scan when service is dead")
+ .that(mRadioTuner.scan(RadioTuner.DIRECTION_UP, /* skipSubChannel= */ true))
+ .isEqualTo(RadioManager.STATUS_DEAD_OBJECT);
+ }
+
+ @Test
public void seek_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);
+ int scanStatus = mRadioTuner.seek(RadioTuner.DIRECTION_DOWN, /* skipSubChannel= */ false);
verify(mTunerMock).seek(/* directionDown= */ true, /* skipSubChannel= */ false);
assertWithMessage("Status for seeking")
@@ -212,13 +315,31 @@
return RadioManager.STATUS_OK;
}).when(mTunerMock).seek(anyBoolean(), anyBoolean());
- mRadioTuner.scan(RadioTuner.DIRECTION_UP, /* skipSubChannel*/ true);
+ mRadioTuner.seek(RadioTuner.DIRECTION_UP, /* skipSubChannel= */ true);
verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onTuneFailed(
RadioTuner.TUNER_RESULT_TIMEOUT, FM_SELECTOR);
}
@Test
+ public void seek_whenIllegalState_fails() throws Exception {
+ doThrow(new IllegalStateException()).when(mTunerMock).seek(anyBoolean(), anyBoolean());
+
+ assertWithMessage("Status for seeking when service is in illegal state")
+ .that(mRadioTuner.seek(RadioTuner.DIRECTION_UP, /* skipSubChannel= */ false))
+ .isEqualTo(RadioManager.STATUS_INVALID_OPERATION);
+ }
+
+ @Test
+ public void seek_whenServiceDied_fails() throws Exception {
+ doThrow(new RemoteException()).when(mTunerMock).seek(anyBoolean(), anyBoolean());
+
+ assertWithMessage("Status for seeking when service is dead")
+ .that(mRadioTuner.seek(RadioTuner.DIRECTION_UP, /* skipSubChannel= */ true))
+ .isEqualTo(RadioManager.STATUS_DEAD_OBJECT);
+ }
+
+ @Test
public void tune_withChannelsForTunerAdapter_succeeds() {
int status = mRadioTuner.tune(/* channel= */ 92300, /* subChannel= */ 0);
@@ -228,6 +349,33 @@
}
@Test
+ public void tune_withInvalidChannel_fails() throws Exception {
+ doThrow(new IllegalArgumentException()).when(mTunerMock).tune(any());
+
+ assertWithMessage("Status for tuning when service is in illegal state")
+ .that(mRadioTuner.tune(/* channel= */ 300, /* subChannel= */ 0))
+ .isEqualTo(RadioManager.STATUS_BAD_VALUE);
+ }
+
+ @Test
+ public void tune_withChannelsWhenIllegalState_fails() throws Exception {
+ doThrow(new IllegalStateException()).when(mTunerMock).tune(any());
+
+ assertWithMessage("Status for tuning when service is in illegal state")
+ .that(mRadioTuner.tune(/* channel= */ 92300, /* subChannel= */ 0))
+ .isEqualTo(RadioManager.STATUS_INVALID_OPERATION);
+ }
+
+ @Test
+ public void tune_withChannelsWhenServiceDied_fails() throws Exception {
+ doThrow(new RemoteException()).when(mTunerMock).tune(any());
+
+ assertWithMessage("Status for tuning when service is dead")
+ .that(mRadioTuner.tune(/* channel= */ 92300, /* subChannel= */ 0))
+ .isEqualTo(RadioManager.STATUS_DEAD_OBJECT);
+ }
+
+ @Test
public void tune_withValidSelectorForTunerAdapter_succeeds() throws Exception {
mRadioTuner.tune(FM_SELECTOR);
@@ -250,6 +398,17 @@
}
@Test
+ public void tune_withSelectorWhenServiceDied_fails() throws Exception {
+ doThrow(new RemoteException()).when(mTunerMock).tune(any());
+
+ RuntimeException thrown = assertThrows(RuntimeException.class,
+ () -> mRadioTuner.tune(FM_SELECTOR));
+
+ assertWithMessage("Exception for tuning when service is dead")
+ .that(thrown).hasMessageThat().contains("Service died");
+ }
+
+ @Test
public void cancel_forTunerAdapter() throws Exception {
mRadioTuner.tune(FM_SELECTOR);
@@ -259,6 +418,22 @@
}
@Test
+ public void cancel_whenIllegalState_fails() throws Exception {
+ doThrow(new IllegalStateException()).when(mTunerMock).cancel();
+
+ assertWithMessage("Status for canceling when service is in illegal state")
+ .that(mRadioTuner.cancel()).isEqualTo(RadioManager.STATUS_INVALID_OPERATION);
+ }
+
+ @Test
+ public void cancel_forTunerAdapterWhenServiceDied_fails() throws Exception {
+ doThrow(new RemoteException()).when(mTunerMock).cancel();
+
+ assertWithMessage("Status for canceling when service is dead")
+ .that(mRadioTuner.cancel()).isEqualTo(RadioManager.STATUS_DEAD_OBJECT);
+ }
+
+ @Test
public void cancelAnnouncement_forTunerAdapter() throws Exception {
mRadioTuner.cancelAnnouncement();
@@ -266,6 +441,17 @@
}
@Test
+ public void cancelAnnouncement_whenServiceDied_fails() throws Exception {
+ doThrow(new RemoteException()).when(mTunerMock).cancelAnnouncement();
+
+ RuntimeException thrown = assertThrows(RuntimeException.class,
+ () -> mRadioTuner.cancelAnnouncement());
+
+ assertWithMessage("Exception for canceling announcement when service is dead")
+ .that(thrown).hasMessageThat().contains("Service died");
+ }
+
+ @Test
public void getProgramInfo_beforeProgramInfoSetForTunerAdapter() {
RadioManager.ProgramInfo[] programInfoArray = new RadioManager.ProgramInfo[1];
@@ -295,13 +481,24 @@
when(mTunerMock.getImage(anyInt())).thenReturn(bitmapExpected);
int imageId = 1;
- Bitmap image = mRadioTuner.getMetadataImage(/* id= */ imageId);
+ Bitmap image = mRadioTuner.getMetadataImage(imageId);
assertWithMessage("Image obtained from id %s", imageId)
.that(image).isEqualTo(bitmapExpected);
}
@Test
+ public void getMetadataImage_whenServiceDied_fails() throws Exception {
+ when(mTunerMock.getImage(anyInt())).thenThrow(new RemoteException());
+
+ RuntimeException thrown = assertThrows(RuntimeException.class,
+ () -> mRadioTuner.getMetadataImage(/* id= */ 1));
+
+ assertWithMessage("Exception for getting metadata image when service is dead")
+ .that(thrown).hasMessageThat().contains("Service died");
+ }
+
+ @Test
public void startBackgroundScan_forTunerAdapter() throws Exception {
when(mTunerMock.startBackgroundScan()).thenReturn(false);
@@ -312,6 +509,17 @@
}
@Test
+ public void startBackgroundScan_whenServiceDied_fails() throws Exception {
+ when(mTunerMock.startBackgroundScan()).thenThrow(new RemoteException());
+
+ RuntimeException thrown = assertThrows(RuntimeException.class,
+ () -> mRadioTuner.startBackgroundScan());
+
+ assertWithMessage("Exception for background scan when service is dead")
+ .that(thrown).hasMessageThat().contains("Service died");
+ }
+
+ @Test
public void isAnalogForced_forTunerAdapter() throws Exception {
when(mTunerMock.isConfigFlagSet(RadioManager.CONFIG_FORCE_ANALOG)).thenReturn(true);
@@ -322,6 +530,19 @@
}
@Test
+ public void isAnalogForced_whenNotSupported_fails() throws Exception {
+ String errorMessage = "Analog forced switch is not supported";
+ when(mTunerMock.isConfigFlagSet(RadioManager.CONFIG_FORCE_ANALOG))
+ .thenThrow(new UnsupportedOperationException(errorMessage));
+
+ IllegalStateException thrown = assertThrows(IllegalStateException.class,
+ () -> mRadioTuner.isAnalogForced());
+
+ assertWithMessage("Exception for checking analog playback switch when not supported")
+ .that(thrown).hasMessageThat().contains(errorMessage);
+ }
+
+ @Test
public void setAnalogForced_forTunerAdapter() throws Exception {
boolean analogForced = true;
@@ -331,6 +552,19 @@
}
@Test
+ public void setAnalogForced_whenNotSupported_fails() throws Exception {
+ String errorMessage = "Analog forced switch is not supported";
+ doThrow(new UnsupportedOperationException(errorMessage))
+ .when(mTunerMock).setConfigFlag(eq(RadioManager.CONFIG_FORCE_ANALOG), anyBoolean());
+
+ IllegalStateException thrown = assertThrows(IllegalStateException.class,
+ () -> mRadioTuner.setAnalogForced(/* isForced= */ false));
+
+ assertWithMessage("Exception for setting analog playback switch when not supported")
+ .that(thrown).hasMessageThat().contains(errorMessage);
+ }
+
+ @Test
public void isConfigFlagSupported_forTunerAdapter() throws Exception {
when(mTunerMock.isConfigFlagSupported(RadioManager.CONFIG_DAB_DAB_LINKING))
.thenReturn(true);
@@ -343,6 +577,17 @@
}
@Test
+ public void isConfigFlagSupported_whenServiceDied_fails() throws Exception {
+ when(mTunerMock.isConfigFlagSupported(anyInt())).thenThrow(new RemoteException());
+
+ RuntimeException thrown = assertThrows(RuntimeException.class,
+ () -> mRadioTuner.isConfigFlagSupported(RadioManager.CONFIG_DAB_DAB_LINKING));
+
+ assertWithMessage("Exception for checking config flag support when service is dead")
+ .that(thrown).hasMessageThat().contains("Service died");
+ }
+
+ @Test
public void isConfigFlagSet_forTunerAdapter() throws Exception {
when(mTunerMock.isConfigFlagSet(RadioManager.CONFIG_DAB_FM_SOFT_LINKING))
.thenReturn(true);
@@ -355,6 +600,17 @@
}
@Test
+ public void isConfigFlagSet_whenServiceDied_fails() throws Exception {
+ when(mTunerMock.isConfigFlagSet(anyInt())).thenThrow(new RemoteException());
+
+ RuntimeException thrown = assertThrows(RuntimeException.class,
+ () -> mRadioTuner.isConfigFlagSet(RadioManager.CONFIG_DAB_DAB_LINKING));
+
+ assertWithMessage("Exception for getting config flag when service is dead")
+ .that(thrown).hasMessageThat().contains("Service died");
+ }
+
+ @Test
public void setConfigFlag_forTunerAdapter() throws Exception {
boolean dabFmLinking = true;
@@ -364,8 +620,20 @@
}
@Test
+ public void setConfigFlag_whenServiceDied_fails() throws Exception {
+ doThrow(new RemoteException()).when(mTunerMock).setConfigFlag(anyInt(), anyBoolean());
+
+ RuntimeException thrown = assertThrows(RuntimeException.class,
+ () -> mRadioTuner.setConfigFlag(RadioManager.CONFIG_DAB_DAB_LINKING,
+ /* value= */ true));
+
+ assertWithMessage("Exception for setting config flag when service is dead")
+ .that(thrown).hasMessageThat().contains("Service died");
+ }
+
+ @Test
public void getParameters_forTunerAdapter() throws Exception {
- List<String> parameterKeys = Arrays.asList("ParameterKeyMock");
+ List<String> parameterKeys = List.of("ParameterKeyMock");
Map<String, String> parameters = Map.of("ParameterKeyMock", "ParameterValueMock");
when(mTunerMock.getParameters(parameterKeys)).thenReturn(parameters);
@@ -374,6 +642,18 @@
}
@Test
+ public void getParameters_whenServiceDied_fails() throws Exception {
+ List<String> parameterKeys = List.of("ParameterKeyMock");
+ when(mTunerMock.getParameters(parameterKeys)).thenThrow(new RemoteException());
+
+ RuntimeException thrown = assertThrows(RuntimeException.class,
+ () -> mRadioTuner.getParameters(parameterKeys));
+
+ assertWithMessage("Exception for getting parameters when service is dead")
+ .that(thrown).hasMessageThat().contains("Service died");
+ }
+
+ @Test
public void setParameters_forTunerAdapter() throws Exception {
Map<String, String> parameters = Map.of("ParameterKeyMock", "ParameterValueMock");
when(mTunerMock.setParameters(parameters)).thenReturn(parameters);
@@ -383,6 +663,18 @@
}
@Test
+ public void setParameters_whenServiceDied_fails() throws Exception {
+ Map<String, String> parameters = Map.of("ParameterKeyMock", "ParameterValueMock");
+ when(mTunerMock.setParameters(parameters)).thenThrow(new RemoteException());
+
+ RuntimeException thrown = assertThrows(RuntimeException.class,
+ () -> mRadioTuner.setParameters(parameters));
+
+ assertWithMessage("Exception for setting parameters when service is dead")
+ .that(thrown).hasMessageThat().contains("Service died");
+ }
+
+ @Test
public void isAntennaConnected_forTunerAdapter() throws Exception {
mTunerCallback.onAntennaState(/* connected= */ false);
@@ -391,6 +683,15 @@
}
@Test
+ public void onError_forTunerAdapter() throws Exception {
+ int errorStatus = RadioTuner.ERROR_HARDWARE_FAILURE;
+
+ mTunerCallback.onError(errorStatus);
+
+ verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onError(errorStatus);
+ }
+
+ @Test
public void hasControl_forTunerAdapter() throws Exception {
when(mTunerMock.isClosed()).thenReturn(true);
@@ -398,6 +699,14 @@
}
@Test
+ public void hasControl_whenServiceDied_returnsFalse() throws Exception {
+ when(mTunerMock.isClosed()).thenThrow(new RemoteException());
+
+ assertWithMessage("Control on tuner when service is dead")
+ .that(mRadioTuner.hasControl()).isFalse();
+ }
+
+ @Test
public void onConfigurationChanged_forTunerCallbackAdapter() throws Exception {
mTunerCallback.onConfigurationChanged(TEST_BAND_CONFIG);
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/AidlTestUtils.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/AidlTestUtils.java
index a421218..82db716 100644
--- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/AidlTestUtils.java
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/AidlTestUtils.java
@@ -19,6 +19,7 @@
import android.hardware.broadcastradio.Metadata;
import android.hardware.broadcastradio.ProgramIdentifier;
import android.hardware.broadcastradio.ProgramInfo;
+import android.hardware.broadcastradio.ProgramListChunk;
import android.hardware.broadcastradio.VendorKeyValue;
import android.hardware.radio.ProgramSelector;
import android.hardware.radio.RadioManager;
@@ -41,17 +42,25 @@
/* dabFrequencyTable= */ null, /* vendorInfo= */ null);
}
- static RadioManager.ProgramInfo makeProgramInfo(ProgramSelector selector, int signalQuality) {
+ static RadioManager.ProgramInfo makeProgramInfo(ProgramSelector selector,
+ ProgramSelector.Identifier logicallyTunedTo,
+ ProgramSelector.Identifier physicallyTunedTo, int signalQuality) {
return new RadioManager.ProgramInfo(selector,
- selector.getPrimaryId(), selector.getPrimaryId(), /* relatedContents= */ null,
+ logicallyTunedTo, physicallyTunedTo, /* relatedContents= */ null,
/* infoFlags= */ 0, signalQuality,
new RadioMetadata.Builder().build(), new ArrayMap<>());
}
- static RadioManager.ProgramInfo makeProgramInfo(int programType,
- ProgramSelector.Identifier identifier, int signalQuality) {
- ProgramSelector selector = makeProgramSelector(programType, identifier);
- return makeProgramInfo(selector, signalQuality);
+ static RadioManager.ProgramInfo makeProgramInfo(ProgramSelector selector, int signalQuality) {
+ return makeProgramInfo(selector, selector.getPrimaryId(), selector.getPrimaryId(),
+ signalQuality);
+ }
+
+ static ProgramIdentifier makeHalIdentifier(@IdentifierType int type, long value) {
+ ProgramIdentifier halDabId = new ProgramIdentifier();
+ halDabId.type = type;
+ halDabId.value = value;
+ return halDabId;
}
static ProgramSelector makeFmSelector(long freq) {
@@ -67,44 +76,48 @@
}
static android.hardware.broadcastradio.ProgramSelector makeHalFmSelector(int freq) {
- ProgramIdentifier halId = new ProgramIdentifier();
- halId.type = IdentifierType.AMFM_FREQUENCY_KHZ;
- halId.value = freq;
-
- android.hardware.broadcastradio.ProgramSelector halSelector =
- new android.hardware.broadcastradio.ProgramSelector();
- halSelector.primaryId = halId;
- halSelector.secondaryIds = new ProgramIdentifier[0];
- return halSelector;
+ ProgramIdentifier halId = makeHalIdentifier(IdentifierType.AMFM_FREQUENCY_KHZ, freq);
+ return makeHalSelector(halId, /* secondaryIds= */ new ProgramIdentifier[0]);
}
- static ProgramInfo programInfoToHalProgramInfo(RadioManager.ProgramInfo info) {
- // Note that because ConversionUtils does not by design provide functions for all
- // conversions, this function only copies fields that are set by makeProgramInfo().
- ProgramInfo hwInfo = new ProgramInfo();
- hwInfo.selector = ConversionUtils.programSelectorToHalProgramSelector(info.getSelector());
- hwInfo.logicallyTunedTo =
- ConversionUtils.identifierToHalProgramIdentifier(info.getLogicallyTunedTo());
- hwInfo.physicallyTunedTo =
- ConversionUtils.identifierToHalProgramIdentifier(info.getPhysicallyTunedTo());
- hwInfo.signalQuality = info.getSignalStrength();
- hwInfo.relatedContent = new ProgramIdentifier[]{};
- hwInfo.metadata = new Metadata[]{};
- return hwInfo;
+ static android.hardware.broadcastradio.ProgramSelector makeHalSelector(
+ ProgramIdentifier primaryId, ProgramIdentifier[] secondaryIds) {
+ android.hardware.broadcastradio.ProgramSelector hwSelector =
+ new android.hardware.broadcastradio.ProgramSelector();
+ hwSelector.primaryId = primaryId;
+ hwSelector.secondaryIds = secondaryIds;
+ return hwSelector;
}
static ProgramInfo makeHalProgramInfo(
android.hardware.broadcastradio.ProgramSelector hwSel, int hwSignalQuality) {
+ return makeHalProgramInfo(hwSel, hwSel.primaryId, hwSel.primaryId, hwSignalQuality);
+ }
+
+ static ProgramInfo makeHalProgramInfo(
+ android.hardware.broadcastradio.ProgramSelector hwSel,
+ ProgramIdentifier logicallyTunedTo, ProgramIdentifier physicallyTunedTo,
+ int hwSignalQuality) {
ProgramInfo hwInfo = new ProgramInfo();
hwInfo.selector = hwSel;
- hwInfo.logicallyTunedTo = hwSel.primaryId;
- hwInfo.physicallyTunedTo = hwSel.primaryId;
+ hwInfo.logicallyTunedTo = logicallyTunedTo;
+ hwInfo.physicallyTunedTo = physicallyTunedTo;
hwInfo.signalQuality = hwSignalQuality;
hwInfo.relatedContent = new ProgramIdentifier[]{};
hwInfo.metadata = new Metadata[]{};
return hwInfo;
}
+ static ProgramListChunk makeProgramListChunk(boolean purge, boolean complete,
+ ProgramInfo[] modified, ProgramIdentifier[] removed) {
+ ProgramListChunk halChunk = new ProgramListChunk();
+ halChunk.purge = purge;
+ halChunk.complete = complete;
+ halChunk.modified = modified;
+ halChunk.removed = removed;
+ return halChunk;
+ }
+
static VendorKeyValue makeVendorKeyValue(String vendorKey, String vendorValue) {
VendorKeyValue vendorKeyValue = new VendorKeyValue();
vendorKeyValue.key = vendorKey;
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 f404082..98103f6 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
@@ -173,6 +173,19 @@
}
@Test
+ public void openSession_withoutAudio_fails() throws Exception {
+ createBroadcastRadioService();
+
+ IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class,
+ () -> mBroadcastRadioService.openSession(FM_RADIO_MODULE_ID,
+ /* legacyConfig= */ null, /* withAudio= */ false, mTunerCallbackMock,
+ TARGET_SDK_VERSION));
+
+ assertWithMessage("Exception for opening session without audio")
+ .that(thrown).hasMessageThat().contains("not supported");
+ }
+
+ @Test
public void binderDied_forDeathRecipient() throws Exception {
createBroadcastRadioService();
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 a1cebb6..710c150 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
@@ -20,9 +20,13 @@
import android.hardware.broadcastradio.AmFmRegionConfig;
import android.hardware.broadcastradio.DabTableEntry;
import android.hardware.broadcastradio.IdentifierType;
+import android.hardware.broadcastradio.ProgramIdentifier;
+import android.hardware.broadcastradio.ProgramInfo;
+import android.hardware.broadcastradio.ProgramListChunk;
import android.hardware.broadcastradio.Properties;
import android.hardware.broadcastradio.VendorKeyValue;
import android.hardware.radio.Announcement;
+import android.hardware.radio.ProgramList;
import android.hardware.radio.ProgramSelector;
import android.hardware.radio.RadioManager;
import android.os.Build;
@@ -33,19 +37,20 @@
import org.junit.Test;
import java.util.Map;
+import java.util.Set;
public final class ConversionUtilsTest {
- private static final int FM_LOWER_LIMIT = 87500;
- private static final int FM_UPPER_LIMIT = 108000;
+ private static final int FM_LOWER_LIMIT = 87_500;
+ private static final int FM_UPPER_LIMIT = 108_000;
private static final int FM_SPACING = 200;
private static final int AM_LOWER_LIMIT = 540;
- private static final int AM_UPPER_LIMIT = 1700;
+ private static final int AM_UPPER_LIMIT = 1_700;
private static final int AM_SPACING = 10;
private static final String DAB_ENTRY_LABEL_1 = "5A";
- private static final int DAB_ENTRY_FREQUENCY_1 = 174928;
+ private static final int DAB_ENTRY_FREQUENCY_1 = 174_928;
private static final String DAB_ENTRY_LABEL_2 = "12D";
- private static final int DAB_ENTRY_FREQUENCY_2 = 229072;
+ private static final int DAB_ENTRY_FREQUENCY_2 = 229_072;
private static final String VENDOR_INFO_KEY_1 = "vendorKey1";
private static final String VENDOR_INFO_VALUE_1 = "vendorValue1";
private static final String VENDOR_INFO_KEY_2 = "vendorKey2";
@@ -57,6 +62,50 @@
private static final String TEST_VERSION = "versionMock";
private static final String TEST_SERIAL = "serialMock";
+ private static final int TEST_SIGNAL_QUALITY = 1;
+ private static final long TEST_DAB_DMB_SID_EXT_VALUE = 0xA000000111L;
+ private static final long TEST_DAB_ENSEMBLE_VALUE = 0x1001;
+ private static final long TEST_DAB_FREQUENCY_VALUE = 220_352;
+ private static final long TEST_FM_FREQUENCY_VALUE = 92_100;
+ private static final long TEST_VENDOR_ID_VALUE = 9_901;
+
+ private static final ProgramSelector.Identifier TEST_DAB_SID_EXT_ID =
+ new ProgramSelector.Identifier(
+ ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT, TEST_DAB_DMB_SID_EXT_VALUE);
+ private static final ProgramSelector.Identifier TEST_DAB_ENSEMBLE_ID =
+ new ProgramSelector.Identifier(
+ ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE, TEST_DAB_ENSEMBLE_VALUE);
+ private static final ProgramSelector.Identifier TEST_DAB_FREQUENCY_ID =
+ new ProgramSelector.Identifier(
+ ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY, TEST_DAB_FREQUENCY_VALUE);
+ private static final ProgramSelector.Identifier TEST_FM_FREQUENCY_ID =
+ new ProgramSelector.Identifier(
+ ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, TEST_FM_FREQUENCY_VALUE);
+ private static final ProgramSelector.Identifier TEST_VENDOR_ID =
+ new ProgramSelector.Identifier(
+ ProgramSelector.IDENTIFIER_TYPE_VENDOR_START, TEST_VENDOR_ID_VALUE);
+
+ private static final ProgramIdentifier TEST_HAL_DAB_SID_EXT_ID =
+ AidlTestUtils.makeHalIdentifier(IdentifierType.DAB_SID_EXT, TEST_DAB_DMB_SID_EXT_VALUE);
+ private static final ProgramIdentifier TEST_HAL_DAB_ENSEMBLE_ID =
+ AidlTestUtils.makeHalIdentifier(IdentifierType.DAB_ENSEMBLE, TEST_DAB_ENSEMBLE_VALUE);
+ private static final ProgramIdentifier TEST_HAL_DAB_FREQUENCY_ID =
+ AidlTestUtils.makeHalIdentifier(IdentifierType.DAB_FREQUENCY_KHZ,
+ TEST_DAB_FREQUENCY_VALUE);
+ private static final ProgramIdentifier TEST_HAL_FM_FREQUENCY_ID =
+ AidlTestUtils.makeHalIdentifier(IdentifierType.AMFM_FREQUENCY_KHZ,
+ TEST_FM_FREQUENCY_VALUE);
+ private static final ProgramIdentifier TEST_HAL_VENDOR_ID =
+ AidlTestUtils.makeHalIdentifier(IdentifierType.VENDOR_START,
+ TEST_VENDOR_ID_VALUE);
+
+ private static final ProgramSelector TEST_DAB_SELECTOR = new ProgramSelector(
+ ProgramSelector.PROGRAM_TYPE_DAB, TEST_DAB_SID_EXT_ID,
+ new ProgramSelector.Identifier[]{TEST_DAB_FREQUENCY_ID, TEST_DAB_ENSEMBLE_ID},
+ /* vendorIds= */ null);
+ private static final ProgramSelector TEST_FM_SELECTOR =
+ AidlTestUtils.makeFmSelector(TEST_FM_FREQUENCY_VALUE);
+
private static final int TEST_ENABLED_TYPE = Announcement.TYPE_EMERGENCY;
private static final int TEST_ANNOUNCEMENT_FREQUENCY = FM_LOWER_LIMIT + FM_SPACING;
@@ -159,6 +208,246 @@
}
@Test
+ public void identifierToHalProgramIdentifier_withDabId() {
+ ProgramIdentifier halDabId =
+ ConversionUtils.identifierToHalProgramIdentifier(TEST_DAB_SID_EXT_ID);
+
+ expect.withMessage("Converted HAL DAB identifier").that(halDabId)
+ .isEqualTo(TEST_HAL_DAB_SID_EXT_ID);
+ }
+
+ @Test
+ public void identifierFromHalProgramIdentifier_withDabId() {
+ ProgramSelector.Identifier dabId =
+ ConversionUtils.identifierFromHalProgramIdentifier(TEST_HAL_DAB_SID_EXT_ID);
+
+ expect.withMessage("Converted DAB identifier").that(dabId).isEqualTo(TEST_DAB_SID_EXT_ID);
+ }
+
+ @Test
+ public void programSelectorToHalProgramSelector_withValidSelector() {
+ android.hardware.broadcastradio.ProgramSelector halDabSelector =
+ ConversionUtils.programSelectorToHalProgramSelector(TEST_DAB_SELECTOR);
+
+ expect.withMessage("Primary identifier of converted HAL DAB selector")
+ .that(halDabSelector.primaryId).isEqualTo(TEST_HAL_DAB_SID_EXT_ID);
+ expect.withMessage("Secondary identifiers of converted HAL DAB selector")
+ .that(halDabSelector.secondaryIds).asList()
+ .containsExactly(TEST_HAL_DAB_FREQUENCY_ID, TEST_HAL_DAB_ENSEMBLE_ID);
+ }
+
+ @Test
+ public void programSelectorToHalProgramSelector_withInvalidDabSelector_returnsNull() {
+ ProgramSelector invalidDbSelector = new ProgramSelector(ProgramSelector.PROGRAM_TYPE_DAB,
+ TEST_DAB_SID_EXT_ID,
+ new ProgramSelector.Identifier[0],
+ new long[0]);
+
+ android.hardware.broadcastradio.ProgramSelector invalidHalDabSelector =
+ ConversionUtils.programSelectorToHalProgramSelector(invalidDbSelector);
+
+ expect.withMessage("Invalid HAL DAB selector without required secondary ids")
+ .that(invalidHalDabSelector).isNull();
+ }
+
+ @Test
+ public void programSelectorFromHalProgramSelector_withValidSelector() {
+ android.hardware.broadcastradio.ProgramSelector halDabSelector =
+ AidlTestUtils.makeHalSelector(TEST_HAL_DAB_SID_EXT_ID, new ProgramIdentifier[]{
+ TEST_HAL_DAB_ENSEMBLE_ID, TEST_HAL_DAB_FREQUENCY_ID});
+
+ ProgramSelector dabSelector =
+ ConversionUtils.programSelectorFromHalProgramSelector(halDabSelector);
+
+ expect.withMessage("Primary identifier of converted DAB selector")
+ .that(dabSelector.getPrimaryId()).isEqualTo(TEST_DAB_SID_EXT_ID);
+ expect.withMessage("Secondary identifiers of converted DAB selector")
+ .that(dabSelector.getSecondaryIds()).asList()
+ .containsExactly(TEST_DAB_FREQUENCY_ID, TEST_DAB_ENSEMBLE_ID);
+ }
+
+ @Test
+ public void programSelectorFromHalProgramSelector_withInvalidSelector_returnsNull() {
+ android.hardware.broadcastradio.ProgramSelector invalidHalDabSelector =
+ AidlTestUtils.makeHalSelector(TEST_HAL_DAB_SID_EXT_ID, new ProgramIdentifier[]{});
+
+ ProgramSelector invalidDabSelector =
+ ConversionUtils.programSelectorFromHalProgramSelector(invalidHalDabSelector);
+
+ expect.withMessage("Invalid DAB selector without required secondary ids")
+ .that(invalidDabSelector).isNull();
+ }
+
+ @Test
+ public void programInfoFromHalProgramInfo_withValidProgramInfo() {
+ android.hardware.broadcastradio.ProgramSelector halDabSelector =
+ AidlTestUtils.makeHalSelector(TEST_HAL_DAB_SID_EXT_ID, new ProgramIdentifier[]{
+ TEST_HAL_DAB_ENSEMBLE_ID, TEST_HAL_DAB_FREQUENCY_ID});
+ ProgramInfo halProgramInfo = AidlTestUtils.makeHalProgramInfo(halDabSelector,
+ TEST_HAL_DAB_SID_EXT_ID, TEST_HAL_DAB_FREQUENCY_ID, TEST_SIGNAL_QUALITY);
+
+ RadioManager.ProgramInfo programInfo =
+ ConversionUtils.programInfoFromHalProgramInfo(halProgramInfo);
+
+ expect.withMessage("Primary id of selector of converted program info")
+ .that(programInfo.getSelector().getPrimaryId()).isEqualTo(TEST_DAB_SID_EXT_ID);
+ expect.withMessage("Secondary id of selector of converted program info")
+ .that(programInfo.getSelector().getSecondaryIds()).asList()
+ .containsExactly(TEST_DAB_ENSEMBLE_ID, TEST_DAB_FREQUENCY_ID);
+ expect.withMessage("Logically tuned identifier of converted program info")
+ .that(programInfo.getLogicallyTunedTo()).isEqualTo(TEST_DAB_SID_EXT_ID);
+ expect.withMessage("Physically tuned identifier of converted program info")
+ .that(programInfo.getPhysicallyTunedTo()).isEqualTo(TEST_DAB_FREQUENCY_ID);
+ expect.withMessage("Signal quality of converted program info")
+ .that(programInfo.getSignalStrength()).isEqualTo(TEST_SIGNAL_QUALITY);
+ }
+
+ @Test
+ public void programInfoFromHalProgramInfo_withInvalidDabProgramInfo() {
+ android.hardware.broadcastradio.ProgramSelector invalidHalDabSelector =
+ AidlTestUtils.makeHalSelector(TEST_HAL_DAB_SID_EXT_ID,
+ new ProgramIdentifier[]{TEST_HAL_DAB_ENSEMBLE_ID, TEST_HAL_DAB_FREQUENCY_ID});
+ ProgramInfo halProgramInfo = AidlTestUtils.makeHalProgramInfo(invalidHalDabSelector,
+ TEST_HAL_DAB_SID_EXT_ID, TEST_HAL_DAB_ENSEMBLE_ID, TEST_SIGNAL_QUALITY);
+
+ RadioManager.ProgramInfo programInfo =
+ ConversionUtils.programInfoFromHalProgramInfo(halProgramInfo);
+
+ expect.withMessage("Invalid DAB program info with incorrect type of physically tuned to id")
+ .that(programInfo).isNull();
+ }
+
+ @Test
+ public void chunkFromHalProgramListChunk_withValidChunk() {
+ boolean purge = false;
+ boolean complete = true;
+ android.hardware.broadcastradio.ProgramSelector halDabSelector =
+ AidlTestUtils.makeHalSelector(TEST_HAL_DAB_SID_EXT_ID, new ProgramIdentifier[]{
+ TEST_HAL_DAB_ENSEMBLE_ID, TEST_HAL_DAB_FREQUENCY_ID});
+ ProgramInfo halDabInfo = AidlTestUtils.makeHalProgramInfo(halDabSelector,
+ TEST_HAL_DAB_SID_EXT_ID, TEST_HAL_DAB_FREQUENCY_ID, TEST_SIGNAL_QUALITY);
+ RadioManager.ProgramInfo dabInfo =
+ ConversionUtils.programInfoFromHalProgramInfo(halDabInfo);
+ ProgramListChunk halChunk = AidlTestUtils.makeProgramListChunk(purge, complete,
+ new ProgramInfo[]{halDabInfo},
+ new ProgramIdentifier[]{TEST_HAL_VENDOR_ID, TEST_HAL_FM_FREQUENCY_ID});
+
+ ProgramList.Chunk chunk = ConversionUtils.chunkFromHalProgramListChunk(halChunk);
+
+ expect.withMessage("Purged state of the converted valid program list chunk")
+ .that(chunk.isPurge()).isEqualTo(purge);
+ expect.withMessage("Completion state of the converted valid program list chunk")
+ .that(chunk.isComplete()).isEqualTo(complete);
+ expect.withMessage("Modified program info in the converted valid program list chunk")
+ .that(chunk.getModified()).containsExactly(dabInfo);
+ expect.withMessage("Removed program ides in the converted valid program list chunk")
+ .that(chunk.getRemoved()).containsExactly(TEST_VENDOR_ID, TEST_FM_FREQUENCY_ID);
+ }
+
+ @Test
+ public void chunkFromHalProgramListChunk_withInvalidModifiedProgramInfo() {
+ boolean purge = true;
+ boolean complete = false;
+ android.hardware.broadcastradio.ProgramSelector halDabSelector =
+ AidlTestUtils.makeHalSelector(TEST_HAL_DAB_SID_EXT_ID, new ProgramIdentifier[]{
+ TEST_HAL_DAB_ENSEMBLE_ID, TEST_HAL_DAB_FREQUENCY_ID});
+ ProgramInfo halDabInfo = AidlTestUtils.makeHalProgramInfo(halDabSelector,
+ TEST_HAL_DAB_SID_EXT_ID, TEST_HAL_DAB_ENSEMBLE_ID, TEST_SIGNAL_QUALITY);
+ ProgramListChunk halChunk = AidlTestUtils.makeProgramListChunk(purge, complete,
+ new ProgramInfo[]{halDabInfo}, new ProgramIdentifier[]{TEST_HAL_FM_FREQUENCY_ID});
+
+ ProgramList.Chunk chunk = ConversionUtils.chunkFromHalProgramListChunk(halChunk);
+
+ expect.withMessage("Purged state of the converted invalid program list chunk")
+ .that(chunk.isPurge()).isEqualTo(purge);
+ expect.withMessage("Completion state of the converted invalid program list chunk")
+ .that(chunk.isComplete()).isEqualTo(complete);
+ expect.withMessage("Modified program info in the converted invalid program list chunk")
+ .that(chunk.getModified()).isEmpty();
+ expect.withMessage("Removed program ids in the converted invalid program list chunk")
+ .that(chunk.getRemoved()).containsExactly(TEST_FM_FREQUENCY_ID);
+ }
+
+ @Test
+ public void programSelectorMeetsSdkVersionRequirement_withLowerVersionId_returnsFalse() {
+ expect.withMessage("Selector %s without required SDK version", TEST_DAB_SELECTOR)
+ .that(ConversionUtils.programSelectorMeetsSdkVersionRequirement(TEST_DAB_SELECTOR,
+ Build.VERSION_CODES.TIRAMISU)).isFalse();
+ }
+
+ @Test
+ public void programSelectorMeetsSdkVersionRequirement_withRequiredVersionId_returnsTrue() {
+ expect.withMessage("Selector %s with required SDK version", TEST_FM_SELECTOR)
+ .that(ConversionUtils.programSelectorMeetsSdkVersionRequirement(TEST_FM_SELECTOR,
+ Build.VERSION_CODES.TIRAMISU)).isTrue();
+ }
+
+ @Test
+ public void programInfoMeetsSdkVersionRequirement_withLowerVersionId_returnsFalse() {
+ RadioManager.ProgramInfo dabProgramInfo = AidlTestUtils.makeProgramInfo(TEST_DAB_SELECTOR,
+ TEST_DAB_SID_EXT_ID, TEST_DAB_FREQUENCY_ID, TEST_SIGNAL_QUALITY);
+
+ expect.withMessage("Program info %s without required SDK version", dabProgramInfo)
+ .that(ConversionUtils.programInfoMeetsSdkVersionRequirement(dabProgramInfo,
+ Build.VERSION_CODES.TIRAMISU)).isFalse();
+ }
+
+ @Test
+ public void programInfoMeetsSdkVersionRequirement_withRequiredVersionId_returnsTrue() {
+ RadioManager.ProgramInfo fmProgramInfo = AidlTestUtils.makeProgramInfo(TEST_FM_SELECTOR,
+ TEST_SIGNAL_QUALITY);
+
+ expect.withMessage("Program info %s with required SDK version", fmProgramInfo)
+ .that(ConversionUtils.programInfoMeetsSdkVersionRequirement(fmProgramInfo,
+ Build.VERSION_CODES.TIRAMISU)).isTrue();
+ }
+
+ @Test
+ public void convertChunkToTargetSdkVersion_withLowerSdkVersion() {
+ RadioManager.ProgramInfo dabProgramInfo = AidlTestUtils.makeProgramInfo(TEST_DAB_SELECTOR,
+ TEST_DAB_SID_EXT_ID, TEST_DAB_FREQUENCY_ID, TEST_SIGNAL_QUALITY);
+ RadioManager.ProgramInfo fmProgramInfo = AidlTestUtils.makeProgramInfo(TEST_FM_SELECTOR,
+ TEST_SIGNAL_QUALITY);
+ ProgramList.Chunk chunk = new ProgramList.Chunk(/* purge= */ true,
+ /* complete= */ true, Set.of(dabProgramInfo, fmProgramInfo),
+ Set.of(TEST_DAB_SID_EXT_ID, TEST_DAB_ENSEMBLE_ID, TEST_VENDOR_ID));
+
+ ProgramList.Chunk convertedChunk = ConversionUtils.convertChunkToTargetSdkVersion(chunk,
+ Build.VERSION_CODES.TIRAMISU);
+
+ expect.withMessage(
+ "Purged state of the converted program list chunk with lower SDK version")
+ .that(convertedChunk.isPurge()).isEqualTo(chunk.isPurge());
+ expect.withMessage(
+ "Completion state of the converted program list chunk with lower SDK version")
+ .that(convertedChunk.isComplete()).isEqualTo(chunk.isComplete());
+ expect.withMessage(
+ "Modified program info in the converted program list chunk with lower SDK version")
+ .that(convertedChunk.getModified()).containsExactly(fmProgramInfo);
+ expect.withMessage(
+ "Removed program ids in the converted program list chunk with lower SDK version")
+ .that(convertedChunk.getRemoved())
+ .containsExactly(TEST_DAB_ENSEMBLE_ID, TEST_VENDOR_ID);
+ }
+
+ @Test
+ public void convertChunkToTargetSdkVersion_withRequiredSdkVersion() {
+ RadioManager.ProgramInfo dabProgramInfo = AidlTestUtils.makeProgramInfo(TEST_DAB_SELECTOR,
+ TEST_DAB_SID_EXT_ID, TEST_DAB_FREQUENCY_ID, TEST_SIGNAL_QUALITY);
+ RadioManager.ProgramInfo fmProgramInfo = AidlTestUtils.makeProgramInfo(TEST_FM_SELECTOR,
+ TEST_SIGNAL_QUALITY);
+ ProgramList.Chunk chunk = new ProgramList.Chunk(/* purge= */ true,
+ /* complete= */ true, Set.of(dabProgramInfo, fmProgramInfo),
+ Set.of(TEST_DAB_SID_EXT_ID, TEST_DAB_ENSEMBLE_ID, TEST_VENDOR_ID));
+
+ ProgramList.Chunk convertedChunk = ConversionUtils.convertChunkToTargetSdkVersion(chunk,
+ Build.VERSION_CODES.CUR_DEVELOPMENT);
+
+ expect.withMessage("Converted program list chunk with required SDK version")
+ .that(convertedChunk).isEqualTo(chunk);
+ }
+
+ @Test
public void announcementFromHalAnnouncement_typesMatch() {
expect.withMessage("Announcement type")
.that(ANNOUNCEMENT.getType()).isEqualTo(TEST_ENABLED_TYPE);
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 c5c6349..d7723ac 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
@@ -26,7 +26,9 @@
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -43,6 +45,8 @@
import android.hardware.radio.RadioManager;
import android.hardware.radio.RadioTuner;
import android.os.Build;
+import android.os.ParcelableException;
+import android.os.RemoteException;
import android.os.ServiceSpecificException;
import android.util.ArrayMap;
import android.util.ArraySet;
@@ -57,7 +61,6 @@
import org.mockito.Mock;
import org.mockito.verification.VerificationWithTimeout;
-import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -71,10 +74,10 @@
timeout(/* millis= */ 200);
private static final int SIGNAL_QUALITY = 1;
private static final long AM_FM_FREQUENCY_SPACING = 500;
- private static final long[] AM_FM_FREQUENCY_LIST = {97500, 98100, 99100};
+ private static final long[] AM_FM_FREQUENCY_LIST = {97_500, 98_100, 99_100};
private static final RadioManager.FmBandDescriptor FM_BAND_DESCRIPTOR =
new RadioManager.FmBandDescriptor(RadioManager.REGION_ITU_1, RadioManager.BAND_FM,
- /* lowerLimit= */ 87500, /* upperLimit= */ 108000, /* spacing= */ 100,
+ /* lowerLimit= */ 87_500, /* upperLimit= */ 108_000, /* spacing= */ 100,
/* stereo= */ false, /* rds= */ false, /* ta= */ false, /* af= */ false,
/* ea= */ false);
private static final RadioManager.BandConfig FM_BAND_CONFIG =
@@ -200,6 +203,17 @@
}
@Test
+ public void setConfiguration_forNonCurrentUser_doesNotInvokesCallback() throws Exception {
+ openAidlClients(/* numClients= */ 1);
+ doReturn(false).when(() -> RadioServiceUserController.isCurrentOrSystemUser());
+
+ mTunerSessions[0].setConfiguration(FM_BAND_CONFIG);
+
+ verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT.times(0))
+ .onConfigurationChanged(FM_BAND_CONFIG);
+ }
+
+ @Test
public void getConfiguration() throws Exception {
openAidlClients(/* numClients= */ 1);
mTunerSessions[0].setConfiguration(FM_BAND_CONFIG);
@@ -330,10 +344,17 @@
@Test
public void tune_withUnsupportedSelector_throwsException() throws Exception {
+ ProgramSelector.Identifier dabPrimaryId =
+ new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT,
+ /* value= */ 0xA000000111L);
+ ProgramSelector.Identifier[] dabSecondaryIds = new ProgramSelector.Identifier[]{
+ new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE,
+ /* value= */ 1337),
+ new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY,
+ /* value= */ 225648)};
+ ProgramSelector unsupportedSelector = new ProgramSelector(ProgramSelector.PROGRAM_TYPE_DAB,
+ dabPrimaryId, dabSecondaryIds, /* vendorIds= */ null);
openAidlClients(/* numClients= */ 1);
- ProgramSelector unsupportedSelector = AidlTestUtils.makeProgramSelector(
- ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY, new ProgramSelector.Identifier(
- ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY, /* value= */ 300));
UnsupportedOperationException thrown = assertThrows(UnsupportedOperationException.class,
() -> mTunerSessions[0].tune(unsupportedSelector));
@@ -343,7 +364,22 @@
}
@Test
- public void tune_forCurrentUser_doesNotTune() throws Exception {
+ public void tune_withInvalidSelector_throwsIllegalArgumentException() throws Exception {
+ openAidlClients(/* numClients= */ 1);
+ ProgramSelector.Identifier invalidDabId = new ProgramSelector.Identifier(
+ ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE, /* value= */ 0x1001);
+ ProgramSelector invalidSel = new ProgramSelector(ProgramSelector.PROGRAM_TYPE_DAB,
+ invalidDabId, new ProgramSelector.Identifier[0], new long[0]);
+
+ IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class,
+ () -> mTunerSessions[0].tune(invalidSel));
+
+ assertWithMessage("Exception for tuning on DAB selector without DAB_SID_EXT primary id")
+ .that(thrown).hasMessageThat().contains("tune: INVALID_ARGUMENTS");
+ }
+
+ @Test
+ public void tune_forNonCurrentUser_doesNotTune() throws Exception {
openAidlClients(/* numClients= */ 1);
doReturn(false).when(() -> RadioServiceUserController.isCurrentOrSystemUser());
ProgramSelector initialSel = AidlTestUtils.makeFmSelector(AM_FM_FREQUENCY_LIST[1]);
@@ -357,6 +393,21 @@
}
@Test
+ public void tune_withHalHasUnknownError_fails() throws Exception {
+ openAidlClients(/* numClients= */ 1);
+ ProgramSelector sel = AidlTestUtils.makeFmSelector(AM_FM_FREQUENCY_LIST[1]);
+ doThrow(new ServiceSpecificException(Result.UNKNOWN_ERROR))
+ .when(mBroadcastRadioMock).tune(any());
+
+ ParcelableException thrown = assertThrows(ParcelableException.class, () -> {
+ mTunerSessions[0].tune(sel);
+ });
+
+ assertWithMessage("Exception for tuning when HAL has unknown error")
+ .that(thrown).hasMessageThat().contains("UNKNOWN_ERROR");
+ }
+
+ @Test
public void step_withDirectionUp() throws Exception {
long initFreq = AM_FM_FREQUENCY_LIST[1];
ProgramSelector initialSel = AidlTestUtils.makeFmSelector(initFreq);
@@ -391,6 +442,35 @@
}
@Test
+ public void step_forNonCurrentUser_doesNotStep() throws Exception {
+ long initFreq = AM_FM_FREQUENCY_LIST[1];
+ ProgramSelector initialSel = AidlTestUtils.makeFmSelector(initFreq);
+ openAidlClients(/* numClients= */ 1);
+ mHalCurrentInfo = AidlTestUtils.makeHalProgramInfo(
+ ConversionUtils.programSelectorToHalProgramSelector(initialSel), SIGNAL_QUALITY);
+ doReturn(false).when(() -> RadioServiceUserController.isCurrentOrSystemUser());
+
+ mTunerSessions[0].step(/* directionDown= */ true, /* skipSubChannel= */ false);
+
+ verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT.times(0))
+ .onCurrentProgramInfoChanged(any());
+ }
+
+ @Test
+ public void step_withHalInInvalidState_fails() throws Exception {
+ openAidlClients(/* numClients= */ 1);
+ doThrow(new ServiceSpecificException(Result.INVALID_STATE))
+ .when(mBroadcastRadioMock).step(anyBoolean());
+
+ IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> {
+ mTunerSessions[0].step(/* directionDown= */ true, /* skipSubChannel= */ false);
+ });
+
+ assertWithMessage("Exception for stepping when HAL is in invalid state")
+ .that(thrown).hasMessageThat().contains("INVALID_STATE");
+ }
+
+ @Test
public void seek_withDirectionUp() throws Exception {
long initFreq = AM_FM_FREQUENCY_LIST[2];
ProgramSelector initialSel = AidlTestUtils.makeFmSelector(initFreq);
@@ -432,11 +512,44 @@
ConversionUtils.programSelectorToHalProgramSelector(initialSel), SIGNAL_QUALITY);
mTunerSessions[0].seek(/* directionDown= */ true, /* skipSubChannel= */ false);
+
verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT)
.onCurrentProgramInfoChanged(seekUpInfo);
}
@Test
+ public void seek_forNonCurrentUser_doesNotSeek() throws Exception {
+ long initFreq = AM_FM_FREQUENCY_LIST[2];
+ ProgramSelector initialSel = AidlTestUtils.makeFmSelector(initFreq);
+ RadioManager.ProgramInfo seekUpInfo = AidlTestUtils.makeProgramInfo(
+ AidlTestUtils.makeFmSelector(getSeekFrequency(initFreq, /* seekDown= */ true)),
+ SIGNAL_QUALITY);
+ openAidlClients(/* numClients= */ 1);
+ mHalCurrentInfo = AidlTestUtils.makeHalProgramInfo(
+ ConversionUtils.programSelectorToHalProgramSelector(initialSel), SIGNAL_QUALITY);
+ doReturn(false).when(() -> RadioServiceUserController.isCurrentOrSystemUser());
+
+ mTunerSessions[0].seek(/* directionDown= */ true, /* skipSubChannel= */ false);
+
+ verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT.times(0))
+ .onCurrentProgramInfoChanged(seekUpInfo);
+ }
+
+ @Test
+ public void seek_withHalHasInternalError_fails() throws Exception {
+ openAidlClients(/* numClients= */ 1);
+ doThrow(new ServiceSpecificException(Result.INTERNAL_ERROR))
+ .when(mBroadcastRadioMock).seek(anyBoolean(), anyBoolean());
+
+ ParcelableException thrown = assertThrows(ParcelableException.class, () -> {
+ mTunerSessions[0].seek(/* directionDown= */ true, /* skipSubChannel= */ false);
+ });
+
+ assertWithMessage("Exception for seeking when HAL has internal error")
+ .that(thrown).hasMessageThat().contains("INTERNAL_ERROR");
+ }
+
+ @Test
public void cancel() throws Exception {
openAidlClients(/* numClients= */ 1);
ProgramSelector initialSel = AidlTestUtils.makeFmSelector(AM_FM_FREQUENCY_LIST[1]);
@@ -448,6 +561,32 @@
}
@Test
+ public void cancel_forNonCurrentUser_doesNotCancel() throws Exception {
+ openAidlClients(/* numClients= */ 1);
+ ProgramSelector initialSel = AidlTestUtils.makeFmSelector(AM_FM_FREQUENCY_LIST[1]);
+ mTunerSessions[0].tune(initialSel);
+ doReturn(false).when(() -> RadioServiceUserController.isCurrentOrSystemUser());
+
+ mTunerSessions[0].cancel();
+
+ verify(mBroadcastRadioMock, never()).cancel();
+ }
+
+ @Test
+ public void cancel_whenHalThrowsRemoteException_fails() throws Exception {
+ openAidlClients(/* numClients= */ 1);
+ String exceptionMessage = "HAL service died.";
+ doThrow(new RemoteException(exceptionMessage)).when(mBroadcastRadioMock).cancel();
+
+ RuntimeException thrown = assertThrows(RuntimeException.class, () -> {
+ mTunerSessions[0].cancel();
+ });
+
+ assertWithMessage("Exception for canceling when HAL throws remote exception")
+ .that(thrown).hasMessageThat().contains(exceptionMessage);
+ }
+
+ @Test
public void getImage_withInvalidId_throwsIllegalArgumentException() throws Exception {
openAidlClients(/* numClients= */ 1);
int imageId = IBroadcastRadio.INVALID_IMAGE;
@@ -471,6 +610,21 @@
}
@Test
+ public void getImage_whenHalThrowsException_fails() throws Exception {
+ openAidlClients(/* numClients= */ 1);
+ String exceptionMessage = "HAL service died.";
+ when(mBroadcastRadioMock.getImage(anyInt()))
+ .thenThrow(new RemoteException(exceptionMessage));
+
+ RuntimeException thrown = assertThrows(RuntimeException.class, () -> {
+ mTunerSessions[0].getImage(/* id= */ 1);
+ });
+
+ assertWithMessage("Exception for getting image when HAL throws remote exception")
+ .that(thrown).hasMessageThat().contains(exceptionMessage);
+ }
+
+ @Test
public void startBackgroundScan() throws Exception {
openAidlClients(/* numClients= */ 1);
@@ -480,6 +634,16 @@
}
@Test
+ public void startBackgroundScan_forNonCurrentUser_doesNotInvokesCallback() throws Exception {
+ openAidlClients(/* numClients= */ 1);
+ doReturn(false).when(() -> RadioServiceUserController.isCurrentOrSystemUser());
+
+ mTunerSessions[0].startBackgroundScan();
+
+ verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT.times(0)).onBackgroundScanComplete();
+ }
+
+ @Test
public void stopProgramListUpdates() throws Exception {
openAidlClients(/* numClients= */ 1);
ProgramList.Filter aidlFilter = new ProgramList.Filter(new ArraySet<>(), new ArraySet<>(),
@@ -492,6 +656,19 @@
}
@Test
+ public void stopProgramListUpdates_forNonCurrentUser_doesNotStopUpdates() throws Exception {
+ openAidlClients(/* numClients= */ 1);
+ ProgramList.Filter aidlFilter = new ProgramList.Filter(new ArraySet<>(), new ArraySet<>(),
+ /* includeCategories= */ true, /* excludeModifications= */ false);
+ mTunerSessions[0].startProgramListUpdates(aidlFilter);
+ doReturn(false).when(() -> RadioServiceUserController.isCurrentOrSystemUser());
+
+ mTunerSessions[0].stopProgramListUpdates();
+
+ verify(mBroadcastRadioMock, never()).stopProgramListUpdates();
+ }
+
+ @Test
public void isConfigFlagSupported_withUnsupportedFlag_returnsFalse() throws Exception {
openAidlClients(/* numClients= */ 1);
int flag = UNSUPPORTED_CONFIG_FLAG;
@@ -547,6 +724,17 @@
}
@Test
+ public void setConfigFlag_forNonCurrentUser_doesNotSetConfigFlag() throws Exception {
+ openAidlClients(/* numClients= */ 1);
+ int flag = UNSUPPORTED_CONFIG_FLAG + 1;
+ doReturn(false).when(() -> RadioServiceUserController.isCurrentOrSystemUser());
+
+ mTunerSessions[0].setConfigFlag(flag, /* value= */ true);
+
+ verify(mBroadcastRadioMock, never()).setConfigFlag(flag, /* value= */ true);
+ }
+
+ @Test
public void isConfigFlagSet_withUnsupportedFlag_throwsRuntimeException()
throws Exception {
openAidlClients(/* numClients= */ 1);
@@ -556,7 +744,7 @@
mTunerSessions[0].isConfigFlagSet(flag);
});
- assertWithMessage("Exception for check if unsupported flag %s is set", flag)
+ assertWithMessage("Exception for checking if unsupported flag %s is set", flag)
.that(thrown).hasMessageThat().contains("isConfigFlagSet: NOT_SUPPORTED");
}
@@ -574,6 +762,20 @@
}
@Test
+ public void isConfigFlagSet_whenHalThrowsRemoteException_fails() throws Exception {
+ openAidlClients(/* numClients= */ 1);
+ int flag = UNSUPPORTED_CONFIG_FLAG + 1;
+ doThrow(new RemoteException()).when(mBroadcastRadioMock).isConfigFlagSet(anyInt());
+
+ RuntimeException thrown = assertThrows(RuntimeException.class, () -> {
+ mTunerSessions[0].isConfigFlagSet(flag);
+ });
+
+ assertWithMessage("Exception for checking config flag when HAL throws remote exception")
+ .that(thrown).hasMessageThat().contains("Failed to check flag");
+ }
+
+ @Test
public void setParameters_withMockParameters() throws Exception {
openAidlClients(/* numClients= */ 1);
Map<String, String> parametersSet = Map.of("mockParam1", "mockValue1",
@@ -586,16 +788,58 @@
}
@Test
+ public void setParameters_forNonCurrentUser_doesNotSetParameters() throws Exception {
+ openAidlClients(/* numClients= */ 1);
+ Map<String, String> parametersSet = Map.of("mockParam1", "mockValue1",
+ "mockParam2", "mockValue2");
+ doReturn(false).when(() -> RadioServiceUserController.isCurrentOrSystemUser());
+
+ mTunerSessions[0].setParameters(parametersSet);
+
+ verify(mBroadcastRadioMock, never()).setParameters(any());
+ }
+
+ @Test
+ public void setParameters_whenHalThrowsRemoteException_fails() throws Exception {
+ openAidlClients(/* numClients= */ 1);
+ Map<String, String> parametersSet = Map.of("mockParam1", "mockValue1",
+ "mockParam2", "mockValue2");
+ String exceptionMessage = "HAL service died.";
+ when(mBroadcastRadioMock.setParameters(any()))
+ .thenThrow(new RemoteException(exceptionMessage));
+
+ RuntimeException thrown = assertThrows(RuntimeException.class, () -> {
+ mTunerSessions[0].setParameters(parametersSet);
+ });
+
+ assertWithMessage("Exception for setting parameters when HAL throws remote exception")
+ .that(thrown).hasMessageThat().contains(exceptionMessage);
+ }
+
+ @Test
public void getParameters_withMockKeys() throws Exception {
openAidlClients(/* numClients= */ 1);
- List<String> parameterKeys = new ArrayList<>(2);
- parameterKeys.add("mockKey1");
- parameterKeys.add("mockKey2");
+ List<String> parameterKeys = List.of("mockKey1", "mockKey2");
mTunerSessions[0].getParameters(parameterKeys);
- verify(mBroadcastRadioMock).getParameters(
- parameterKeys.toArray(new String[0]));
+ verify(mBroadcastRadioMock).getParameters(parameterKeys.toArray(new String[0]));
+ }
+
+ @Test
+ public void getParameters_whenServiceThrowsRemoteException_fails() throws Exception {
+ openAidlClients(/* numClients= */ 1);
+ List<String> parameterKeys = List.of("mockKey1", "mockKey2");
+ String exceptionMessage = "HAL service died.";
+ when(mBroadcastRadioMock.getParameters(any()))
+ .thenThrow(new RemoteException(exceptionMessage));
+
+ RuntimeException thrown = assertThrows(RuntimeException.class, () -> {
+ mTunerSessions[0].getParameters(parameterKeys);
+ });
+
+ assertWithMessage("Exception for getting parameters when HAL throws remote exception")
+ .that(thrown).hasMessageThat().contains(exceptionMessage);
}
@Test
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 db16c03..6e54dcf 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
@@ -324,10 +324,17 @@
@Test
public void tune_withUnsupportedSelector_throwsException() throws Exception {
+ ProgramSelector.Identifier dabPrimaryId =
+ new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT,
+ /* value= */ 0xA00111);
+ ProgramSelector.Identifier[] dabSecondaryIds = new ProgramSelector.Identifier[]{
+ new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE,
+ /* value= */ 1337),
+ new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY,
+ /* value= */ 225648)};
+ ProgramSelector unsupportedSelector = new ProgramSelector(ProgramSelector.PROGRAM_TYPE_DAB,
+ dabPrimaryId, dabSecondaryIds, /* vendorIds= */ null);
openAidlClients(/* numClients= */ 1);
- ProgramSelector unsupportedSelector = TestUtils.makeProgramSelector(
- ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY, new ProgramSelector.Identifier(
- ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY, /* value= */ 300));
UnsupportedOperationException thrown = assertThrows(UnsupportedOperationException.class,
() -> mTunerSessions[0].tune(unsupportedSelector));
diff --git a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java
index 4011933..abbbb2f 100644
--- a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java
+++ b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java
@@ -16,6 +16,7 @@
package android.app.activity;
+import static android.companion.virtual.VirtualDeviceManager.DEVICE_ID_INVALID;
import static android.content.Intent.ACTION_EDIT;
import static android.content.Intent.ACTION_VIEW;
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
@@ -204,7 +205,8 @@
try {
// Send process level config change.
ClientTransaction transaction = newTransaction(activityThread, null);
- transaction.addCallback(ConfigurationChangeItem.obtain(new Configuration(newConfig)));
+ transaction.addCallback(ConfigurationChangeItem.obtain(
+ new Configuration(newConfig), DEVICE_ID_INVALID));
appThread.scheduleTransaction(transaction);
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
@@ -413,12 +415,14 @@
activity.mTestLatch = new CountDownLatch(1);
ClientTransaction transaction = newTransaction(activityThread, null);
- transaction.addCallback(ConfigurationChangeItem.obtain(processConfigLandscape));
+ transaction.addCallback(ConfigurationChangeItem.obtain(
+ processConfigLandscape, DEVICE_ID_INVALID));
appThread.scheduleTransaction(transaction);
transaction = newTransaction(activityThread, activity.getActivityToken());
transaction.addCallback(ActivityConfigurationChangeItem.obtain(activityConfigLandscape));
- transaction.addCallback(ConfigurationChangeItem.obtain(processConfigPortrait));
+ transaction.addCallback(ConfigurationChangeItem.obtain(
+ processConfigPortrait, DEVICE_ID_INVALID));
transaction.addCallback(ActivityConfigurationChangeItem.obtain(activityConfigPortrait));
appThread.scheduleTransaction(transaction);
@@ -530,7 +534,7 @@
? ORIENTATION_LANDSCAPE : ORIENTATION_PORTRAIT;
activityThread.updatePendingConfiguration(newAppConfig);
- activityThread.handleConfigurationChanged(newAppConfig);
+ activityThread.handleConfigurationChanged(newAppConfig, DEVICE_ID_INVALID);
try {
assertEquals("Virtual display orientation must not change when process"
@@ -548,7 +552,7 @@
private static void restoreConfig(ActivityThread thread, Configuration originalConfig) {
thread.getConfiguration().seq = originalConfig.seq - 1;
ResourcesManager.getInstance().getConfiguration().seq = originalConfig.seq - 1;
- thread.handleConfigurationChanged(originalConfig);
+ thread.handleConfigurationChanged(originalConfig, DEVICE_ID_INVALID);
}
@Test
@@ -626,7 +630,7 @@
newAppConfig.seq++;
final ActivityThread activityThread = activity.getActivityThread();
- activityThread.handleConfigurationChanged(newAppConfig);
+ activityThread.handleConfigurationChanged(newAppConfig, DEVICE_ID_INVALID);
// Verify that application config update was applied, but didn't change activity config.
assertEquals("Activity config must not change if the process config changes",
@@ -771,7 +775,8 @@
final ClientTransactionItem callbackItem = ActivityRelaunchItem.obtain(null,
null, 0, new MergedConfiguration(), false /* preserveWindow */);
final ResumeActivityItem resumeStateRequest =
- ResumeActivityItem.obtain(true /* isForward */);
+ ResumeActivityItem.obtain(true /* isForward */,
+ false /* shouldSendCompatFakeFocus*/);
final ClientTransaction transaction = newTransaction(activity);
transaction.addCallback(callbackItem);
@@ -782,7 +787,8 @@
private static ClientTransaction newResumeTransaction(Activity activity) {
final ResumeActivityItem resumeStateRequest =
- ResumeActivityItem.obtain(true /* isForward */);
+ ResumeActivityItem.obtain(true /* isForward */,
+ false /* shouldSendCompatFakeFocus */);
final ClientTransaction transaction = newTransaction(activity);
transaction.setLifecycleStateRequest(resumeStateRequest);
diff --git a/core/tests/coretests/src/android/app/backup/BackupManagerTest.java b/core/tests/coretests/src/android/app/backup/BackupManagerTest.java
index cbf167c..27ee82e 100644
--- a/core/tests/coretests/src/android/app/backup/BackupManagerTest.java
+++ b/core/tests/coretests/src/android/app/backup/BackupManagerTest.java
@@ -83,6 +83,13 @@
() -> mBackupManager.getBackupRestoreEventLogger(agent));
}
+ @Test
+ public void testGetDelayedRestoreLogger_returnsRestoreLogger() {
+ BackupRestoreEventLogger logger = mBackupManager.getDelayedRestoreLogger();
+
+ assertThat(logger.getOperationType()).isEqualTo(OperationType.RESTORE);
+ }
+
private static BackupAgent getTestAgent() {
return new BackupAgent() {
@Override
diff --git a/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java b/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java
index 993ecf6..ca6735b 100644
--- a/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java
+++ b/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java
@@ -98,15 +98,15 @@
@Test
public void testRecycleConfigurationChangeItem() {
- ConfigurationChangeItem emptyItem = ConfigurationChangeItem.obtain(null);
- ConfigurationChangeItem item = ConfigurationChangeItem.obtain(config());
+ ConfigurationChangeItem emptyItem = ConfigurationChangeItem.obtain(null, 0);
+ ConfigurationChangeItem item = ConfigurationChangeItem.obtain(config(), 1);
assertNotSame(item, emptyItem);
assertFalse(item.equals(emptyItem));
item.recycle();
assertEquals(item, emptyItem);
- ConfigurationChangeItem item2 = ConfigurationChangeItem.obtain(config());
+ ConfigurationChangeItem item2 = ConfigurationChangeItem.obtain(config(), 1);
assertSame(item, item2);
assertFalse(item2.equals(emptyItem));
}
@@ -147,6 +147,7 @@
persistableBundle.putInt("k", 4);
IBinder assistToken = new Binder();
IBinder shareableActivityToken = new Binder();
+ int deviceId = 3;
Supplier<LaunchActivityItem> itemSupplier = () -> new LaunchActivityItemBuilder()
.setIntent(intent).setIdent(ident).setInfo(activityInfo).setCurConfig(config())
@@ -155,7 +156,7 @@
.setPendingResults(resultInfoList()).setPendingNewIntents(referrerIntentList())
.setIsForward(true).setAssistToken(assistToken)
.setShareableActivityToken(shareableActivityToken)
- .setTaskFragmentToken(new Binder()).build();
+ .setTaskFragmentToken(new Binder()).setDeviceId(deviceId).build();
LaunchActivityItem emptyItem = new LaunchActivityItemBuilder().build();
LaunchActivityItem item = itemSupplier.get();
@@ -236,15 +237,15 @@
@Test
public void testRecycleResumeActivityItem() {
- ResumeActivityItem emptyItem = ResumeActivityItem.obtain(false);
- ResumeActivityItem item = ResumeActivityItem.obtain(3, true);
+ ResumeActivityItem emptyItem = ResumeActivityItem.obtain(false, false);
+ ResumeActivityItem item = ResumeActivityItem.obtain(3, true, false);
assertNotSame(item, emptyItem);
assertFalse(item.equals(emptyItem));
item.recycle();
assertEquals(item, emptyItem);
- ResumeActivityItem item2 = ResumeActivityItem.obtain(2, true);
+ ResumeActivityItem item2 = ResumeActivityItem.obtain(2, true, false);
assertSame(item, item2);
assertFalse(item2.equals(emptyItem));
}
diff --git a/core/tests/coretests/src/android/app/servertransaction/TestUtils.java b/core/tests/coretests/src/android/app/servertransaction/TestUtils.java
index 2cd890c..0ed6a29 100644
--- a/core/tests/coretests/src/android/app/servertransaction/TestUtils.java
+++ b/core/tests/coretests/src/android/app/servertransaction/TestUtils.java
@@ -95,6 +95,7 @@
private ActivityInfo mInfo;
private Configuration mCurConfig;
private Configuration mOverrideConfig;
+ private int mDeviceId;
private String mReferrer;
private IVoiceInteractor mVoiceInteractor;
private int mProcState;
@@ -135,6 +136,11 @@
return this;
}
+ LaunchActivityItemBuilder setDeviceId(int deviceId) {
+ mDeviceId = deviceId;
+ return this;
+ }
+
LaunchActivityItemBuilder setReferrer(String referrer) {
mReferrer = referrer;
return this;
@@ -207,7 +213,7 @@
LaunchActivityItem build() {
return LaunchActivityItem.obtain(mIntent, mIdent, mInfo,
- mCurConfig, mOverrideConfig, mReferrer, mVoiceInteractor,
+ mCurConfig, mOverrideConfig, mDeviceId, mReferrer, mVoiceInteractor,
mProcState, mState, mPersistentState, mPendingResults, mPendingNewIntents,
mActivityOptions, mIsForward, mProfilerInfo, mAssistToken,
null /* activityClientController */, mShareableActivityToken,
diff --git a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
index a0ed026..48a8249 100644
--- a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
+++ b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
@@ -25,53 +25,26 @@
import static org.junit.Assert.assertTrue;
import android.app.ActivityOptions;
-import android.app.ContentProviderHolder;
-import android.app.IApplicationThread;
-import android.app.IInstrumentationWatcher;
-import android.app.IUiAutomationConnection;
-import android.app.ProfilerInfo;
import android.app.servertransaction.TestUtils.LaunchActivityItemBuilder;
-import android.content.AutofillOptions;
-import android.content.ComponentName;
-import android.content.ContentCaptureOptions;
-import android.content.IIntentReceiver;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
-import android.content.pm.ParceledListSlice;
-import android.content.pm.ProviderInfo;
-import android.content.pm.ProviderInfoList;
-import android.content.pm.ServiceInfo;
-import android.content.res.CompatibilityInfo;
import android.content.res.Configuration;
import android.os.Binder;
import android.os.Bundle;
-import android.os.Debug;
-import android.os.IBinder;
import android.os.Parcel;
-import android.os.ParcelFileDescriptor;
import android.os.Parcelable;
import android.os.PersistableBundle;
-import android.os.RemoteCallback;
-import android.os.RemoteException;
-import android.os.SharedMemory;
import android.platform.test.annotations.Presubmit;
-import android.view.autofill.AutofillId;
-import android.view.translation.TranslationSpec;
-import android.view.translation.UiTranslationSpec;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
-import com.android.internal.app.IVoiceInteractor;
-
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
/**
* Test parcelling and unparcelling of transactions and transaction items.
@@ -97,7 +70,7 @@
@Test
public void testConfigurationChange() {
// Write to parcel
- ConfigurationChangeItem item = ConfigurationChangeItem.obtain(config());
+ ConfigurationChangeItem item = ConfigurationChangeItem.obtain(config(), 1 /* deviceId */);
writeAndPrepareForReading(item);
// Read from parcel and assert
@@ -249,7 +222,7 @@
public void testResume() {
// Write to parcel
ResumeActivityItem item = ResumeActivityItem.obtain(27 /* procState */,
- true /* isForward */);
+ true /* isForward */, false /* shouldSendCompatFakeFocus */);
writeAndPrepareForReading(item);
// Read from parcel and assert
diff --git a/core/tests/coretests/src/android/service/controls/OWNERS b/core/tests/coretests/src/android/service/controls/OWNERS
new file mode 100644
index 0000000..bf67034
--- /dev/null
+++ b/core/tests/coretests/src/android/service/controls/OWNERS
@@ -0,0 +1 @@
+include platform/frameworks/base:/core/java/android/service/controls/OWNERS
\ No newline at end of file
diff --git a/core/tests/coretests/src/android/view/InsetsControllerTest.java b/core/tests/coretests/src/android/view/InsetsControllerTest.java
index c917302..5ec93e5 100644
--- a/core/tests/coretests/src/android/view/InsetsControllerTest.java
+++ b/core/tests/coretests/src/android/view/InsetsControllerTest.java
@@ -71,6 +71,7 @@
import android.view.WindowManager.BadTokenException;
import android.view.WindowManager.LayoutParams;
import android.view.animation.LinearInterpolator;
+import android.view.inputmethod.ImeTracker;
import android.widget.TextView;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -136,7 +137,8 @@
private boolean mImeRequestedShow;
@Override
- public int requestShow(boolean fromController) {
+ public int requestShow(boolean fromController,
+ ImeTracker.Token statsToken) {
if (fromController || mImeRequestedShow) {
mImeRequestedShow = true;
return SHOW_IMMEDIATELY;
diff --git a/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java b/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java
index 3a3eeee..0486e3c 100644
--- a/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java
+++ b/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java
@@ -43,6 +43,7 @@
import android.view.SurfaceControl.Transaction;
import android.view.WindowManager.BadTokenException;
import android.view.WindowManager.LayoutParams;
+import android.view.inputmethod.ImeTracker;
import android.widget.TextView;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -221,7 +222,8 @@
return new InsetsSourceConsumer(ITYPE_IME, ime(), state,
() -> mMockTransaction, controller) {
@Override
- public int requestShow(boolean fromController) {
+ public int requestShow(boolean fromController,
+ ImeTracker.Token statsToken) {
return SHOW_IMMEDIATELY;
}
};
diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java
index 1d06448..2afbb47 100644
--- a/core/tests/coretests/src/android/view/ViewRootImplTest.java
+++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java
@@ -297,6 +297,20 @@
}
}
+ @Test
+ public void whenDispatchFakeFocus_focusDoesNotPersist() throws Exception {
+ View view = new View(sContext);
+ attachViewToWindow(view);
+ view.clearFocus();
+
+ assertThat(view.hasWindowFocus()).isFalse();
+
+ mViewRootImpl = view.getViewRootImpl();
+
+ mViewRootImpl.dispatchCompatFakeFocus();
+ assertThat(view.hasWindowFocus()).isFalse();
+ }
+
/**
* When window doesn't have focus, keys should be dropped.
*/
diff --git a/core/tests/coretests/src/android/view/inputmethod/TextAppearanceInfoTest.java b/core/tests/coretests/src/android/view/inputmethod/TextAppearanceInfoTest.java
new file mode 100644
index 0000000..f93cd18
--- /dev/null
+++ b/core/tests/coretests/src/android/view/inputmethod/TextAppearanceInfoTest.java
@@ -0,0 +1,281 @@
+/*
+ * 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.view.inputmethod;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Typeface;
+import android.graphics.fonts.FontStyle;
+import android.graphics.text.LineBreakConfig;
+import android.os.LocaleList;
+import android.os.Parcel;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.TextPaint;
+import android.text.style.AbsoluteSizeSpan;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.ScaleXSpan;
+import android.text.style.StyleSpan;
+import android.text.style.TypefaceSpan;
+import android.view.ViewGroup;
+import android.widget.EditText;
+
+import androidx.annotation.NonNull;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class TextAppearanceInfoTest {
+ private static final float EPSILON = 0.0000001f;
+ private static final String TEST_TEXT = "Happy birthday!";
+ private static final float TEXT_SIZE = 16.5f;
+ private static final LocaleList TEXT_LOCALES = LocaleList.forLanguageTags("en,ja");
+ private static final String FONT_FAMILY_NAME = "sans-serif";
+ private static final int TEXT_WEIGHT = FontStyle.FONT_WEIGHT_MEDIUM;
+ private static final int TEXT_STYLE = Typeface.ITALIC;
+ private static final boolean ALL_CAPS = true;
+ private static final float SHADOW_DX = 2.0f;
+ private static final float SHADOW_DY = 2.0f;
+ private static final float SHADOW_RADIUS = 2.0f;
+ private static final int SHADOW_COLOR = Color.GRAY;
+ private static final boolean ELEGANT_TEXT_HEIGHT = true;
+ private static final boolean FALLBACK_LINE_SPACING = true;
+ private static final float LETTER_SPACING = 5.0f;
+ private static final String FONT_FEATURE_SETTINGS = "smcp";
+ private static final String FONT_VARIATION_SETTINGS = "'wdth' 1.0";
+ private static final int LINE_BREAK_STYLE = LineBreakConfig.LINE_BREAK_STYLE_LOOSE;
+ private static final int LINE_BREAK_WORD_STYLE = LineBreakConfig.LINE_BREAK_WORD_STYLE_PHRASE;
+ private static final float TEXT_SCALEX = 1.5f;
+ private static final int HIGHLIGHT_TEXT_COLOR = Color.YELLOW;
+ private static final int TEXT_COLOR = Color.RED;
+ private static final int HINT_TEXT_COLOR = Color.GREEN;
+ private static final int LINK_TEXT_COLOR = Color.BLUE;
+
+ private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
+ private final EditText mEditText = new EditText(mContext);
+ private final SpannableStringBuilder mSpannableText = new SpannableStringBuilder(TEST_TEXT);
+ private Canvas mCanvas;
+
+ @Before
+ public void setUp() {
+ mEditText.setText(mSpannableText);
+ mEditText.getPaint().setTextSize(TEXT_SIZE);
+ mEditText.setTextLocales(TEXT_LOCALES);
+ Typeface family = Typeface.create(FONT_FAMILY_NAME, Typeface.NORMAL);
+ mEditText.setTypeface(
+ Typeface.create(family, TEXT_WEIGHT, (TEXT_STYLE & Typeface.ITALIC) != 0));
+ mEditText.setAllCaps(ALL_CAPS);
+ mEditText.setShadowLayer(SHADOW_RADIUS, SHADOW_DX, SHADOW_DY, SHADOW_COLOR);
+ mEditText.setElegantTextHeight(ELEGANT_TEXT_HEIGHT);
+ mEditText.setFallbackLineSpacing(FALLBACK_LINE_SPACING);
+ mEditText.setLetterSpacing(LETTER_SPACING);
+ mEditText.setFontFeatureSettings(FONT_FEATURE_SETTINGS);
+ mEditText.setFontVariationSettings(FONT_VARIATION_SETTINGS);
+ mEditText.setLineBreakStyle(LINE_BREAK_STYLE);
+ mEditText.setLineBreakWordStyle(LINE_BREAK_WORD_STYLE);
+ mEditText.setTextScaleX(TEXT_SCALEX);
+ mEditText.setHighlightColor(HIGHLIGHT_TEXT_COLOR);
+ mEditText.setTextColor(TEXT_COLOR);
+ mEditText.setHintTextColor(HINT_TEXT_COLOR);
+ mEditText.setLinkTextColor(LINK_TEXT_COLOR);
+ ViewGroup.LayoutParams params =
+ new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ mEditText.setLayoutParams(params);
+ mEditText.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ Bitmap bitmap =
+ Bitmap.createBitmap(
+ Math.max(1, mEditText.getMeasuredWidth()),
+ Math.max(1, mEditText.getMeasuredHeight()),
+ Bitmap.Config.ARGB_8888);
+ mEditText.layout(0, 0, mEditText.getMeasuredWidth(), mEditText.getMeasuredHeight());
+ mCanvas = new Canvas(bitmap);
+ mEditText.draw(mCanvas);
+ }
+
+ @Test
+ public void testCreateFromTextView_noSpan() {
+ assertTextAppearanceInfoContentsEqual(TextAppearanceInfo.createFromTextView(mEditText));
+ }
+
+ @Test
+ public void testCreateFromTextView_withSpan1() {
+ AbsoluteSizeSpan sizeSpan = new AbsoluteSizeSpan(30);
+ mSpannableText.setSpan(sizeSpan, 0, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ ForegroundColorSpan colorSpan = new ForegroundColorSpan(Color.CYAN);
+ mSpannableText.setSpan(colorSpan, 1, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ TypefaceSpan typefaceSpan = new TypefaceSpan("cursive");
+ mSpannableText.setSpan(typefaceSpan, 2, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ mEditText.setText(mSpannableText);
+
+ // |Happy birthday!
+ mEditText.setSelection(0);
+ TextAppearanceInfo info1 = TextAppearanceInfo.createFromTextView(mEditText);
+ assertEquals(info1.getTextSize(), TEXT_SIZE, EPSILON);
+ assertEquals(info1.getTextColor(), TEXT_COLOR);
+ assertEquals(info1.getSystemFontFamilyName(), FONT_FAMILY_NAME);
+
+ // H|appy birthday!
+ mEditText.setSelection(1);
+ TextAppearanceInfo info2 = TextAppearanceInfo.createFromTextView(mEditText);
+ assertEquals(info2.getTextSize(), 30f, EPSILON);
+ assertEquals(info2.getTextColor(), TEXT_COLOR);
+ assertEquals(info2.getSystemFontFamilyName(), FONT_FAMILY_NAME);
+
+ // Ha|ppy birthday!
+ mEditText.setSelection(2);
+ TextAppearanceInfo info3 = TextAppearanceInfo.createFromTextView(mEditText);
+ assertEquals(info3.getTextSize(), 30f, EPSILON);
+ assertEquals(info3.getTextColor(), Color.CYAN);
+ assertEquals(info3.getSystemFontFamilyName(), FONT_FAMILY_NAME);
+
+ // Ha[ppy birthday!]
+ mEditText.setSelection(2, mSpannableText.length());
+ TextAppearanceInfo info4 = TextAppearanceInfo.createFromTextView(mEditText);
+ assertEquals(info4.getTextSize(), 30f, EPSILON);
+ assertEquals(info4.getTextColor(), Color.CYAN);
+ assertEquals(info4.getSystemFontFamilyName(), FONT_FAMILY_NAME);
+
+ // Happy| birthday!
+ mEditText.setSelection(5);
+ TextAppearanceInfo info5 = TextAppearanceInfo.createFromTextView(mEditText);
+ assertEquals(info5.getTextSize(), 30f, EPSILON);
+ assertEquals(info5.getTextColor(), Color.CYAN);
+ assertEquals(info5.getSystemFontFamilyName(), "cursive");
+ }
+
+ @Test
+ public void testCreateFromTextView_withSpan2() {
+ // aab|
+ SpannableStringBuilder spannableText = new SpannableStringBuilder("aab");
+
+ AbsoluteSizeSpan sizeSpan = new AbsoluteSizeSpan(30);
+ spannableText.setSpan(sizeSpan, 0, 3, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+
+ ForegroundColorSpan colorSpan = new ForegroundColorSpan(Color.CYAN);
+ spannableText.setSpan(colorSpan, 0, 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ StyleSpan styleSpan = new StyleSpan(Typeface.BOLD);
+ spannableText.setSpan(styleSpan, 1, 2, Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
+
+ TypefaceSpan typefaceSpan = new TypefaceSpan("cursive");
+ spannableText.setSpan(typefaceSpan, 3, 3, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+
+ ScaleXSpan scaleXSpan = new ScaleXSpan(2.0f);
+ spannableText.setSpan(scaleXSpan, 3, 3, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+
+ mEditText.setText(spannableText);
+ mEditText.setSelection(3);
+ TextAppearanceInfo info = TextAppearanceInfo.createFromTextView(mEditText);
+
+ // The character before cursor 'b' should only have an AbsoluteSizeSpan.
+ assertEquals(info.getTextSize(), 30f, EPSILON);
+ assertEquals(info.getTextColor(), TEXT_COLOR);
+ assertEquals(info.getTextStyle(), TEXT_STYLE);
+ assertEquals(info.getSystemFontFamilyName(), FONT_FAMILY_NAME);
+ assertEquals(info.getTextScaleX(), TEXT_SCALEX, EPSILON);
+ }
+
+ @Test
+ public void testCreateFromTextView_contradictorySpans() {
+ // Set multiple contradictory spans
+ AbsoluteSizeSpan sizeSpan1 = new AbsoluteSizeSpan(30);
+ CustomForegroundColorSpan colorSpan1 = new CustomForegroundColorSpan(Color.BLUE);
+ AbsoluteSizeSpan sizeSpan2 = new AbsoluteSizeSpan(10);
+ CustomForegroundColorSpan colorSpan2 = new CustomForegroundColorSpan(Color.GREEN);
+
+ mSpannableText.setSpan(sizeSpan1, 0, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ mSpannableText.setSpan(colorSpan1, 0, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ mSpannableText.setSpan(sizeSpan2, 0, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ mSpannableText.setSpan(colorSpan2, 0, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ mEditText.setText(mSpannableText);
+ mEditText.draw(mCanvas);
+ mEditText.setSelection(3);
+ // Get a copy of the real TextPaint after setting the last span
+ TextPaint realTextPaint = colorSpan2.lastTextPaint;
+ assertNotNull(realTextPaint);
+ TextAppearanceInfo info1 = TextAppearanceInfo.createFromTextView(mEditText);
+ // Verify the real TextPaint equals the last span of multiple contradictory spans
+ assertEquals(info1.getTextSize(), 10f, EPSILON);
+ assertEquals(info1.getTextSize(), realTextPaint.getTextSize(), EPSILON);
+ assertEquals(info1.getTextColor(), Color.GREEN);
+ assertEquals(info1.getTextColor(), realTextPaint.getColor());
+ assertEquals(info1.getSystemFontFamilyName(), FONT_FAMILY_NAME);
+ }
+
+ private void assertTextAppearanceInfoContentsEqual(TextAppearanceInfo textAppearanceInfo) {
+ assertEquals(textAppearanceInfo.getTextSize(), TEXT_SIZE, EPSILON);
+ assertEquals(textAppearanceInfo.getTextLocales(), TEXT_LOCALES);
+ assertEquals(textAppearanceInfo.getSystemFontFamilyName(), FONT_FAMILY_NAME);
+ assertEquals(textAppearanceInfo.getTextFontWeight(), TEXT_WEIGHT);
+ assertEquals(textAppearanceInfo.getTextStyle(), TEXT_STYLE);
+ assertEquals(textAppearanceInfo.isAllCaps(), ALL_CAPS);
+ assertEquals(textAppearanceInfo.getShadowRadius(), SHADOW_RADIUS, EPSILON);
+ assertEquals(textAppearanceInfo.getShadowDx(), SHADOW_DX, EPSILON);
+ assertEquals(textAppearanceInfo.getShadowDy(), SHADOW_DY, EPSILON);
+ assertEquals(textAppearanceInfo.getShadowColor(), SHADOW_COLOR);
+ assertEquals(textAppearanceInfo.isElegantTextHeight(), ELEGANT_TEXT_HEIGHT);
+ assertEquals(textAppearanceInfo.isFallbackLineSpacing(), FALLBACK_LINE_SPACING);
+ assertEquals(textAppearanceInfo.getLetterSpacing(), LETTER_SPACING, EPSILON);
+ assertEquals(textAppearanceInfo.getFontFeatureSettings(), FONT_FEATURE_SETTINGS);
+ assertEquals(textAppearanceInfo.getFontVariationSettings(), FONT_VARIATION_SETTINGS);
+ assertEquals(textAppearanceInfo.getLineBreakStyle(), LINE_BREAK_STYLE);
+ assertEquals(textAppearanceInfo.getLineBreakWordStyle(), LINE_BREAK_WORD_STYLE);
+ assertEquals(textAppearanceInfo.getTextScaleX(), TEXT_SCALEX, EPSILON);
+ assertEquals(textAppearanceInfo.getTextColor(), TEXT_COLOR);
+ assertEquals(textAppearanceInfo.getHighlightTextColor(), HIGHLIGHT_TEXT_COLOR);
+ assertEquals(textAppearanceInfo.getHintTextColor(), HINT_TEXT_COLOR);
+ assertEquals(textAppearanceInfo.getLinkTextColor(), LINK_TEXT_COLOR);
+ }
+
+ static class CustomForegroundColorSpan extends ForegroundColorSpan {
+ @Nullable public TextPaint lastTextPaint = null;
+
+ CustomForegroundColorSpan(int color) {
+ super(color);
+ }
+
+ CustomForegroundColorSpan(@NonNull Parcel src) {
+ super(src);
+ }
+
+ @Override
+ public void updateDrawState(@NonNull TextPaint tp) {
+ super.updateDrawState(tp);
+ // Copy the real TextPaint
+ TextPaint tpCopy = new TextPaint();
+ tpCopy.set(tp);
+ lastTextPaint = tpCopy;
+ }
+ }
+}
diff --git a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
index 9d6b29e..3b6e8ea 100644
--- a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
+++ b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
@@ -19,12 +19,15 @@
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
import android.os.RemoteException;
import android.platform.test.annotations.Presubmit;
import android.view.IWindow;
@@ -61,13 +64,22 @@
private OnBackAnimationCallback mCallback1;
@Mock
private OnBackAnimationCallback mCallback2;
+ @Mock
+ private Context mContext;
+ @Mock
+ private ApplicationInfo mApplicationInfo;
+
private final BackMotionEvent mBackEvent = new BackMotionEvent(
0, 0, 0, BackEvent.EDGE_LEFT, null);
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
- mDispatcher = new WindowOnBackInvokedDispatcher(true /* applicationCallbackEnabled */);
+
+ doReturn(true).when(mApplicationInfo).isOnBackInvokedCallbackEnabled();
+ doReturn(mApplicationInfo).when(mContext).getApplicationInfo();
+
+ mDispatcher = new WindowOnBackInvokedDispatcher(mContext);
mDispatcher.attachToWindow(mWindowSession, mWindow);
}
diff --git a/core/tests/hdmitests/src/android/hardware/hdmi/HdmiPortInfoTest.java b/core/tests/hdmitests/src/android/hardware/hdmi/HdmiPortInfoTest.java
index d8dc1ea..64df348 100755
--- a/core/tests/hdmitests/src/android/hardware/hdmi/HdmiPortInfoTest.java
+++ b/core/tests/hdmitests/src/android/hardware/hdmi/HdmiPortInfoTest.java
@@ -37,26 +37,38 @@
boolean isCec = true;
boolean isMhl = false;
boolean isArcSupported = false;
+ boolean isEarcSupported = false;
new EqualsTester()
.addEqualityGroup(
- new HdmiPortInfo(portId, portType, address, isCec, isMhl, isArcSupported),
- new HdmiPortInfo(portId, portType, address, isCec, isMhl, isArcSupported))
+ new HdmiPortInfo(portId, portType, address, isCec, isMhl, isArcSupported,
+ isEarcSupported),
+ new HdmiPortInfo(portId, portType, address, isCec, isMhl, isArcSupported,
+ isEarcSupported))
.addEqualityGroup(
new HdmiPortInfo(
- portId + 1, portType, address, isCec, isMhl, isArcSupported))
+ portId + 1, portType, address, isCec, isMhl, isArcSupported,
+ isEarcSupported))
.addEqualityGroup(
new HdmiPortInfo(
- portId, portType + 1, address, isCec, isMhl, isArcSupported))
+ portId, portType + 1, address, isCec, isMhl, isArcSupported,
+ isEarcSupported))
.addEqualityGroup(
new HdmiPortInfo(
- portId, portType, address + 1, isCec, isMhl, isArcSupported))
+ portId, portType, address + 1, isCec, isMhl, isArcSupported,
+ isEarcSupported))
.addEqualityGroup(
- new HdmiPortInfo(portId, portType, address, !isCec, isMhl, isArcSupported))
+ new HdmiPortInfo(portId, portType, address, !isCec, isMhl, isArcSupported,
+ isEarcSupported))
.addEqualityGroup(
- new HdmiPortInfo(portId, portType, address, isCec, !isMhl, isArcSupported))
+ new HdmiPortInfo(portId, portType, address, isCec, !isMhl, isArcSupported,
+ isEarcSupported))
.addEqualityGroup(
- new HdmiPortInfo(portId, portType, address, isCec, isMhl, !isArcSupported))
+ new HdmiPortInfo(portId, portType, address, isCec, isMhl, !isArcSupported,
+ isEarcSupported))
+ .addEqualityGroup(
+ new HdmiPortInfo(portId, portType, address, isCec, isMhl, isArcSupported,
+ !isEarcSupported))
.testEquals();
}
}
diff --git a/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java b/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java
index 88b2de7..539eb62 100644
--- a/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java
+++ b/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java
@@ -302,7 +302,7 @@
private void resumeActivity(ActivityClientRecord r) {
mThread.handleResumeActivity(r, true /* finalStateRequest */,
- true /* isForward */, "test");
+ true /* isForward */, false /* shouldSendCompatFakeFocus */, "test");
}
private void pauseActivity(ActivityClientRecord r) {
@@ -347,7 +347,7 @@
doNothing().when(packageInfo).updateApplicationInfo(any(), any());
return new ActivityClientRecord(mock(IBinder.class), Intent.makeMainActivity(component),
- 0 /* ident */, info, new Configuration(), null /* referrer */,
+ 0 /* ident */, info, new Configuration(), 0 /*deviceId */, null /* referrer */,
null /* voiceInteractor */, null /* state */, null /* persistentState */,
null /* pendingResults */, null /* pendingNewIntents */,
null /* activityOptions */, true /* isForward */, null /* profilerInfo */,
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index 278b958..bc3af1d 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -313,6 +313,12 @@
"group": "WM_DEBUG_IME",
"at": "com\/android\/server\/wm\/DisplayContent.java"
},
+ "-1812743677": {
+ "message": "Display id=%d is ignoring all orientation requests, camera is active and the top activity is eligible for force rotation, return %s,portrait activity: %b, is natural orientation portrait: %b.",
+ "level": "VERBOSE",
+ "group": "WM_DEBUG_ORIENTATION",
+ "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java"
+ },
"-1810446914": {
"message": "Trying to update display configuration for system\/invalid process.",
"level": "WARN",
@@ -493,6 +499,12 @@
"group": "WM_DEBUG_ORIENTATION",
"at": "com\/android\/server\/wm\/WindowManagerService.java"
},
+ "-1631991057": {
+ "message": "Display id=%d is notified that Camera %s is closed but activity is still refreshing. Rescheduling an update.",
+ "level": "VERBOSE",
+ "group": "WM_DEBUG_ORIENTATION",
+ "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java"
+ },
"-1630752478": {
"message": "removeLockedTask: removed %s",
"level": "DEBUG",
@@ -637,6 +649,12 @@
"group": "WM_DEBUG_WINDOW_INSETS",
"at": "com\/android\/server\/wm\/InsetsSourceProvider.java"
},
+ "-1480918485": {
+ "message": "Refreshed activity: %s",
+ "level": "INFO",
+ "group": "WM_DEBUG_STATES",
+ "at": "com\/android\/server\/wm\/ActivityRecord.java"
+ },
"-1480772131": {
"message": "No app or window is requesting an orientation, return %d for display id=%d",
"level": "VERBOSE",
@@ -1375,6 +1393,12 @@
"group": "WM_DEBUG_CONFIGURATION",
"at": "com\/android\/server\/wm\/ActivityRecord.java"
},
+ "-799396645": {
+ "message": "Display id=%d is notified that Camera %s is closed, updating rotation.",
+ "level": "VERBOSE",
+ "group": "WM_DEBUG_ORIENTATION",
+ "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java"
+ },
"-799003045": {
"message": "Set animatingExit: reason=remove\/replaceWindow win=%s",
"level": "VERBOSE",
@@ -1585,6 +1609,12 @@
"group": "WM_DEBUG_FOCUS_LIGHT",
"at": "com\/android\/server\/wm\/DisplayContent.java"
},
+ "-637815408": {
+ "message": "Invalid surface rotation angle in config_deviceTabletopRotations: %d",
+ "level": "ERROR",
+ "group": "WM_DEBUG_ORIENTATION",
+ "at": "com\/android\/server\/wm\/DisplayRotation.java"
+ },
"-636553602": {
"message": "commitVisibility: %s: visible=%b visibleRequested=%b, isInTransition=%b, runningAnimation=%b, caller=%s",
"level": "VERBOSE",
@@ -1597,6 +1627,12 @@
"group": "WM_DEBUG_SCREEN_ON",
"at": "com\/android\/server\/wm\/DisplayContent.java"
},
+ "-627759820": {
+ "message": "Display id=%d is notified that Camera %s is open for package %s",
+ "level": "VERBOSE",
+ "group": "WM_DEBUG_ORIENTATION",
+ "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java"
+ },
"-622997754": {
"message": "postWindowRemoveCleanupLocked: %s",
"level": "VERBOSE",
@@ -1627,12 +1663,6 @@
"group": "WM_ERROR",
"at": "com\/android\/server\/wm\/WindowManagerService.java"
},
- "-576580969": {
- "message": "viewServerWindowCommand: bootFinished() failed.",
- "level": "WARN",
- "group": "WM_ERROR",
- "at": "com\/android\/server\/wm\/WindowManagerService.java"
- },
"-576070986": {
"message": "Performing post-rotate rotation after seamless rotation",
"level": "INFO",
@@ -1975,6 +2005,12 @@
"group": "WM_DEBUG_STATES",
"at": "com\/android\/server\/wm\/ActivityRecord.java"
},
+ "-254406860": {
+ "message": "Unable to tell MediaProjectionManagerService about visibility change on the active projection: %s",
+ "level": "ERROR",
+ "group": "WM_DEBUG_CONTENT_RECORDING",
+ "at": "com\/android\/server\/wm\/ContentRecorder.java"
+ },
"-251259736": {
"message": "No longer freezing: %s",
"level": "VERBOSE",
@@ -2029,6 +2065,12 @@
"group": "WM_DEBUG_ANIM",
"at": "com\/android\/server\/wm\/SurfaceAnimator.java"
},
+ "-206549078": {
+ "message": "Not moving display (displayId=%d) to top. Top focused displayId=%d. Reason: config_perDisplayFocusEnabled",
+ "level": "INFO",
+ "group": "WM_DEBUG_FOCUS_LIGHT",
+ "at": "com\/android\/server\/wm\/WindowManagerService.java"
+ },
"-203358733": {
"message": "commitFinishDrawingLocked: mDrawState=READY_TO_SHOW %s",
"level": "INFO",
@@ -2149,6 +2191,12 @@
"group": "WM_SHOW_TRANSACTIONS",
"at": "com\/android\/server\/wm\/Session.java"
},
+ "-81260230": {
+ "message": "Display id=%d is notified that Camera %s is closed, scheduling rotation update.",
+ "level": "VERBOSE",
+ "group": "WM_DEBUG_ORIENTATION",
+ "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java"
+ },
"-81121442": {
"message": "ImeContainer just became organized but it doesn't have a parent or the parent doesn't have a surface control. mSurfaceControl=%s imeParentSurfaceControl=%s",
"level": "ERROR",
@@ -2257,6 +2305,12 @@
"group": "WM_DEBUG_RESIZE",
"at": "com\/android\/server\/wm\/WindowState.java"
},
+ "34682671": {
+ "message": "Not moving display (displayId=%d) to top. Top focused displayId=%d. Reason: FLAG_STEAL_TOP_FOCUS_DISABLED",
+ "level": "INFO",
+ "group": "WM_DEBUG_FOCUS_LIGHT",
+ "at": "com\/android\/server\/wm\/WindowManagerService.java"
+ },
"35398067": {
"message": "goodToGo(): onAnimationStart, transit=%s, apps=%d, wallpapers=%d, nonApps=%d",
"level": "DEBUG",
@@ -3235,6 +3289,12 @@
"group": "WM_DEBUG_STATES",
"at": "com\/android\/server\/wm\/TaskFragment.java"
},
+ "939638078": {
+ "message": "config_deviceTabletopRotations is not defined. Half-fold letterboxing will work inconsistently.",
+ "level": "WARN",
+ "group": "WM_DEBUG_ORIENTATION",
+ "at": "com\/android\/server\/wm\/DisplayRotation.java"
+ },
"948208142": {
"message": "Setting Activity.mLauncherTaskBehind to true. Activity=%s",
"level": "DEBUG",
@@ -4153,12 +4213,6 @@
"group": "WM_DEBUG_REMOTE_ANIMATIONS",
"at": "com\/android\/server\/wm\/RemoteAnimationController.java"
},
- "1903353011": {
- "message": "notifyAppStopped: %s",
- "level": "VERBOSE",
- "group": "WM_DEBUG_ADD_REMOVE",
- "at": "com\/android\/server\/wm\/ActivityRecord.java"
- },
"1912291550": {
"message": "Sleep still waiting to pause %s",
"level": "VERBOSE",
@@ -4219,6 +4273,12 @@
"group": "WM_DEBUG_REMOTE_ANIMATIONS",
"at": "com\/android\/server\/wm\/RemoteAnimationController.java"
},
+ "1967643923": {
+ "message": "Refershing activity for camera compatibility treatment, activityRecord=%s",
+ "level": "VERBOSE",
+ "group": "WM_DEBUG_STATES",
+ "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java"
+ },
"1967975839": {
"message": "Changing app %s visible=%b performLayout=%b",
"level": "VERBOSE",
diff --git a/data/keyboards/Vendor_054c_Product_0df2.kl b/data/keyboards/Vendor_054c_Product_0df2.kl
new file mode 100644
index 0000000..a47b310
--- /dev/null
+++ b/data/keyboards/Vendor_054c_Product_0df2.kl
@@ -0,0 +1,73 @@
+# 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.
+
+#
+# Sony Playstation(R) DualSense Edge Controller
+#
+
+# Only use this key layout if we have HID_PLAYSTATION!
+requires_kernel_config CONFIG_HID_PLAYSTATION
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+# Square
+key 0x134 BUTTON_X
+# Cross
+key 0x130 BUTTON_A
+# Circle
+key 0x131 BUTTON_B
+# Triangle
+key 0x133 BUTTON_Y
+
+key 0x136 BUTTON_L1
+key 0x137 BUTTON_R1
+key 0x138 BUTTON_L2
+key 0x139 BUTTON_R2
+
+# L2 axis
+axis 0x02 LTRIGGER
+# R2 axis
+axis 0x05 RTRIGGER
+
+# Left Analog Stick
+axis 0x00 X
+axis 0x01 Y
+# Right Analog Stick
+axis 0x03 Z
+axis 0x04 RZ
+
+# Left stick click
+key 0x13d BUTTON_THUMBL
+# Right stick click
+key 0x13e BUTTON_THUMBR
+
+# Hat
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Share / "half-sun"
+key 0x13a BUTTON_SELECT
+# Options / three horizontal lines
+key 0x13b BUTTON_START
+# PS key
+key 0x13c BUTTON_MODE
+
+# SENSORs
+sensor 0x00 ACCELEROMETER X
+sensor 0x01 ACCELEROMETER Y
+sensor 0x02 ACCELEROMETER Z
+sensor 0x03 GYROSCOPE X
+sensor 0x04 GYROSCOPE Y
+sensor 0x05 GYROSCOPE Z
diff --git a/data/keyboards/Vendor_054c_Product_0df2_fallback.kl b/data/keyboards/Vendor_054c_Product_0df2_fallback.kl
new file mode 100644
index 0000000..bfebb17
--- /dev/null
+++ b/data/keyboards/Vendor_054c_Product_0df2_fallback.kl
@@ -0,0 +1,75 @@
+# 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.
+
+#
+# Sony Playstation(R) DualSense Edge Controller
+#
+
+# Use this if HID_PLAYSTATION is not available
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+# Square
+key 304 BUTTON_X
+# Cross
+key 305 BUTTON_A
+# Circle
+key 306 BUTTON_B
+# Triangle
+key 307 BUTTON_Y
+
+key 308 BUTTON_L1
+key 309 BUTTON_R1
+key 310 BUTTON_L2
+key 311 BUTTON_R2
+
+# L2 axis
+axis 0x03 LTRIGGER
+# R2 axis
+axis 0x04 RTRIGGER
+
+# Left Analog Stick
+axis 0x00 X
+axis 0x01 Y
+# Right Analog Stick
+axis 0x02 Z
+axis 0x05 RZ
+
+# Left stick click
+key 314 BUTTON_THUMBL
+# Right stick click
+key 315 BUTTON_THUMBR
+
+# Hat
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Share / "half-sun"
+key 312 BUTTON_SELECT
+# Options / three horizontal lines
+key 313 BUTTON_START
+# PS key
+key 316 BUTTON_MODE
+
+# Touchpad press
+key 317 BUTTON_1
+
+# SENSORs
+sensor 0x00 ACCELEROMETER X
+sensor 0x01 ACCELEROMETER Y
+sensor 0x02 ACCELEROMETER Z
+sensor 0x03 GYROSCOPE X
+sensor 0x04 GYROSCOPE Y
+sensor 0x05 GYROSCOPE Z
diff --git a/graphics/java/android/graphics/BaseCanvas.java b/graphics/java/android/graphics/BaseCanvas.java
index e62ac46..00ffd09 100644
--- a/graphics/java/android/graphics/BaseCanvas.java
+++ b/graphics/java/android/graphics/BaseCanvas.java
@@ -668,12 +668,19 @@
}
/**
- * @hide
+ * Draws a mesh object to the screen.
+ *
+ * @param mesh {@link Mesh} object that will be drawn to the screen
+ * @param blendMode {@link BlendMode} used to blend mesh primitives with the Paint color/shader
+ * @param paint {@link Paint} used to provide a color/shader/blend mode.
*/
- public void drawMesh(Mesh mesh, BlendMode blendMode, Paint paint) {
+ public void drawMesh(@NonNull Mesh mesh, BlendMode blendMode, @NonNull Paint paint) {
if (!isHardwareAccelerated() && onHwFeatureInSwMode()) {
throw new RuntimeException("software rendering doesn't support meshes");
}
+ if (blendMode == null) {
+ blendMode = BlendMode.MODULATE;
+ }
nDrawMesh(this.mNativeCanvasWrapper, mesh.getNativeWrapperInstance(),
blendMode.getXfermode().porterDuffMode, paint.getNativeInstance());
}
diff --git a/graphics/java/android/graphics/BaseRecordingCanvas.java b/graphics/java/android/graphics/BaseRecordingCanvas.java
index eeff694..2ec4524 100644
--- a/graphics/java/android/graphics/BaseRecordingCanvas.java
+++ b/graphics/java/android/graphics/BaseRecordingCanvas.java
@@ -607,7 +607,10 @@
}
@Override
- public final void drawMesh(Mesh mesh, BlendMode blendMode, Paint paint) {
+ public final void drawMesh(@NonNull Mesh mesh, BlendMode blendMode, @NonNull Paint paint) {
+ if (blendMode == null) {
+ blendMode = BlendMode.MODULATE;
+ }
nDrawMesh(mNativeCanvasWrapper, mesh.getNativeWrapperInstance(),
blendMode.getXfermode().porterDuffMode, paint.getNativeInstance());
}
diff --git a/graphics/java/android/graphics/Bitmap.java b/graphics/java/android/graphics/Bitmap.java
index 318cd32..3c654d6 100644
--- a/graphics/java/android/graphics/Bitmap.java
+++ b/graphics/java/android/graphics/Bitmap.java
@@ -1810,10 +1810,15 @@
* {@code ColorSpace}.</p>
*
* @throws IllegalArgumentException If the specified color space is {@code null}, not
- * {@link ColorSpace.Model#RGB RGB}, has a transfer function that is not an
- * {@link ColorSpace.Rgb.TransferParameters ICC parametric curve}, or whose
- * components min/max values reduce the numerical range compared to the
- * previously assigned color space.
+ * {@link ColorSpace.Model#RGB RGB}, or whose components min/max values reduce
+ * the numerical range compared to the previously assigned color space.
+ * Prior to {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
+ * <code>IllegalArgumentException</code> will also be thrown
+ * if the specified color space has a transfer function that is not an
+ * {@link ColorSpace.Rgb.TransferParameters ICC parametric curve}. Starting from
+ * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, the color spaces with non
+ * ICC parametric curve transfer function are allowed.
+ * E.g., {@link ColorSpace.Named#BT2020_HLG BT2020_HLG}.
*
* @throws IllegalArgumentException If the {@code Config} (returned by {@link #getConfig()})
* is {@link Config#ALPHA_8}.
diff --git a/graphics/java/android/graphics/BitmapFactory.java b/graphics/java/android/graphics/BitmapFactory.java
index ef1e7bf..701e20c 100644
--- a/graphics/java/android/graphics/BitmapFactory.java
+++ b/graphics/java/android/graphics/BitmapFactory.java
@@ -161,11 +161,17 @@
* be thrown by the decode methods when setting a non-RGB color space
* such as {@link ColorSpace.Named#CIE_LAB Lab}.</p>
*
- * <p class="note">The specified color space's transfer function must be
+ * <p class="note">
+ * Prior to {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
+ * the specified color space's transfer function must be
* an {@link ColorSpace.Rgb.TransferParameters ICC parametric curve}. An
* <code>IllegalArgumentException</code> will be thrown by the decode methods
* if calling {@link ColorSpace.Rgb#getTransferParameters()} on the
- * specified color space returns null.</p>
+ * specified color space returns null.
+ *
+ * Starting from {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
+ * non ICC parametric curve transfer function is allowed.
+ * E.g., {@link ColorSpace.Named#BT2020_HLG BT2020_HLG}.</p>
*
* <p>After decode, the bitmap's color space is stored in
* {@link #outColorSpace}.</p>
@@ -458,7 +464,11 @@
throw new IllegalArgumentException("The destination color space must use the " +
"RGB color model");
}
- if (((ColorSpace.Rgb) opts.inPreferredColorSpace).getTransferParameters() == null) {
+ if (!opts.inPreferredColorSpace.equals(ColorSpace.get(ColorSpace.Named.BT2020_HLG))
+ && !opts.inPreferredColorSpace.equals(
+ ColorSpace.get(ColorSpace.Named.BT2020_PQ))
+ && ((ColorSpace.Rgb) opts.inPreferredColorSpace)
+ .getTransferParameters() == null) {
throw new IllegalArgumentException("The destination color space must use an " +
"ICC parametric transfer function");
}
diff --git a/graphics/java/android/graphics/ColorSpace.java b/graphics/java/android/graphics/ColorSpace.java
index 31df474..2427dec 100644
--- a/graphics/java/android/graphics/ColorSpace.java
+++ b/graphics/java/android/graphics/ColorSpace.java
@@ -199,6 +199,8 @@
private static final float[] SRGB_PRIMARIES = { 0.640f, 0.330f, 0.300f, 0.600f, 0.150f, 0.060f };
private static final float[] NTSC_1953_PRIMARIES = { 0.67f, 0.33f, 0.21f, 0.71f, 0.14f, 0.08f };
+ private static final float[] BT2020_PRIMARIES =
+ { 0.708f, 0.292f, 0.170f, 0.797f, 0.131f, 0.046f };
/**
* A gray color space does not have meaningful primaries, so we use this arbitrary set.
*/
@@ -208,6 +210,12 @@
private static final Rgb.TransferParameters SRGB_TRANSFER_PARAMETERS =
new Rgb.TransferParameters(1 / 1.055, 0.055 / 1.055, 1 / 12.92, 0.04045, 2.4);
+ private static final Rgb.TransferParameters BT2020_HLG_TRANSFER_PARAMETERS =
+ new Rgb.TransferParameters(2.0f, 2.0f, 1 / 0.17883277f,
+ 0.28466892f, 0.5599107f, 0.0f, -3.0f, true);
+ private static final Rgb.TransferParameters BT2020_PQ_TRANSFER_PARAMETERS =
+ new Rgb.TransferParameters(107 / 128.0f, 1.0f, 32 / 2523.0f,
+ 2413 / 128.0f, -2392 / 128.0f, 8192 / 1305.0f, -2.0f, true);
// See static initialization block next to #get(Named)
private static final ColorSpace[] sNamedColorSpaces = new ColorSpace[Named.values().length];
@@ -703,7 +711,29 @@
* <tr><td>Range</td><td colspan="4">\(L: [0.0, 100.0], a: [-128, 128], b: [-128, 128]\)</td></tr>
* </table>
*/
- CIE_LAB
+ CIE_LAB,
+ /**
+ * <p>{@link ColorSpace.Rgb RGB} color space BT.2100 standardized as
+ * Hybrid Log Gamma encoding.</p>
+ * <table summary="Color space definition">
+ * <tr><th>Property</th><th colspan="4">Value</th></tr>
+ * <tr><td>Name</td><td colspan="4">Hybrid Log Gamma encoding</td></tr>
+ * <tr><td>CIE standard illuminant</td><td colspan="4">D65</td></tr>
+ * <tr><td>Range</td><td colspan="4">\([0..1]\)</td></tr>
+ * </table>
+ */
+ BT2020_HLG,
+ /**
+ * <p>{@link ColorSpace.Rgb RGB} color space BT.2100 standardized as
+ * Perceptual Quantizer encoding.</p>
+ * <table summary="Color space definition">
+ * <tr><th>Property</th><th colspan="4">Value</th></tr>
+ * <tr><td>Name</td><td colspan="4">Perceptual Quantizer encoding</td></tr>
+ * <tr><td>CIE standard illuminant</td><td colspan="4">D65</td></tr>
+ * <tr><td>Range</td><td colspan="4">\([0..1]\)</td></tr>
+ * </table>
+ */
+ BT2020_PQ
// Update the initialization block next to #get(Named) when adding new values
}
@@ -1534,7 +1564,7 @@
sDataToColorSpaces.put(DataSpace.DATASPACE_BT709, Named.BT709.ordinal());
sNamedColorSpaces[Named.BT2020.ordinal()] = new ColorSpace.Rgb(
"Rec. ITU-R BT.2020-1",
- new float[] { 0.708f, 0.292f, 0.170f, 0.797f, 0.131f, 0.046f },
+ BT2020_PRIMARIES,
ILLUMINANT_D65,
null,
new Rgb.TransferParameters(1 / 1.0993, 0.0993 / 1.0993, 1 / 4.5, 0.08145, 1 / 0.45),
@@ -1616,6 +1646,70 @@
"Generic L*a*b*",
Named.CIE_LAB.ordinal()
);
+ sNamedColorSpaces[Named.BT2020_HLG.ordinal()] = new ColorSpace.Rgb(
+ "Hybrid Log Gamma encoding",
+ BT2020_PRIMARIES,
+ ILLUMINANT_D65,
+ null,
+ x -> transferHLGOETF(x),
+ x -> transferHLGEOTF(x),
+ 0.0f, 1.0f,
+ BT2020_HLG_TRANSFER_PARAMETERS,
+ Named.BT2020_HLG.ordinal()
+ );
+ sDataToColorSpaces.put(DataSpace.DATASPACE_BT2020_HLG, Named.BT2020_HLG.ordinal());
+ sNamedColorSpaces[Named.BT2020_PQ.ordinal()] = new ColorSpace.Rgb(
+ "Perceptual Quantizer encoding",
+ BT2020_PRIMARIES,
+ ILLUMINANT_D65,
+ null,
+ x -> transferST2048OETF(x),
+ x -> transferST2048EOTF(x),
+ 0.0f, 1.0f,
+ BT2020_PQ_TRANSFER_PARAMETERS,
+ Named.BT2020_PQ.ordinal()
+ );
+ sDataToColorSpaces.put(DataSpace.DATASPACE_BT2020_PQ, Named.BT2020_PQ.ordinal());
+ }
+
+ private static double transferHLGOETF(double x) {
+ double a = 0.17883277;
+ double b = 0.28466892;
+ double c = 0.55991073;
+ double r = 0.5;
+ return x > 1.0 ? a * Math.log(x - b) + c : r * Math.sqrt(x);
+ }
+
+ private static double transferHLGEOTF(double x) {
+ double a = 0.17883277;
+ double b = 0.28466892;
+ double c = 0.55991073;
+ double r = 0.5;
+ return x <= 0.5 ? (x * x) / (r * r) : Math.exp((x - c) / a + b);
+ }
+
+ private static double transferST2048OETF(double x) {
+ double m1 = (2610.0 / 4096.0) / 4.0;
+ double m2 = (2523.0 / 4096.0) * 128.0;
+ double c1 = (3424.0 / 4096.0);
+ double c2 = (2413.0 / 4096.0) * 32.0;
+ double c3 = (2392.0 / 4096.0) * 32.0;
+
+ double tmp = Math.pow(x, m1);
+ tmp = (c1 + c2 * tmp) / (1.0 + c3 * tmp);
+ return Math.pow(tmp, m2);
+ }
+
+ private static double transferST2048EOTF(double x) {
+ double m1 = (2610.0 / 4096.0) / 4.0;
+ double m2 = (2523.0 / 4096.0) * 128.0;
+ double c1 = (3424.0 / 4096.0);
+ double c2 = (2413.0 / 4096.0) * 32.0;
+ double c3 = (2392.0 / 4096.0) * 32.0;
+
+ double tmp = Math.pow(Math.min(Math.max(x, 0.0), 1.0), 1.0 / m2);
+ tmp = Math.max(tmp - c1, 0.0) / (c2 - c3 * tmp);
+ return Math.pow(tmp, 1.0 / m1);
}
// Reciprocal piecewise gamma response
@@ -2197,6 +2291,58 @@
/** Variable \(g\) in the equation of the EOTF described above. */
public final double g;
+ private TransferParameters(double a, double b, double c, double d, double e,
+ double f, double g, boolean nonCurveTransferParameters) {
+ // nonCurveTransferParameters correspondes to a "special" transfer function
+ if (!nonCurveTransferParameters) {
+ if (Double.isNaN(a) || Double.isNaN(b) || Double.isNaN(c)
+ || Double.isNaN(d) || Double.isNaN(e) || Double.isNaN(f)
+ || Double.isNaN(g)) {
+ throw new IllegalArgumentException("Parameters cannot be NaN");
+ }
+
+ // Next representable float after 1.0
+ // We use doubles here but the representation inside our native code
+ // is often floats
+ if (!(d >= 0.0 && d <= 1.0f + Math.ulp(1.0f))) {
+ throw new IllegalArgumentException(
+ "Parameter d must be in the range [0..1], " + "was " + d);
+ }
+
+ if (d == 0.0 && (a == 0.0 || g == 0.0)) {
+ throw new IllegalArgumentException(
+ "Parameter a or g is zero, the transfer function is constant");
+ }
+
+ if (d >= 1.0 && c == 0.0) {
+ throw new IllegalArgumentException(
+ "Parameter c is zero, the transfer function is constant");
+ }
+
+ if ((a == 0.0 || g == 0.0) && c == 0.0) {
+ throw new IllegalArgumentException("Parameter a or g is zero,"
+ + " and c is zero, the transfer function is constant");
+ }
+
+ if (c < 0.0) {
+ throw new IllegalArgumentException(
+ "The transfer function must be increasing");
+ }
+
+ if (a < 0.0 || g < 0.0) {
+ throw new IllegalArgumentException(
+ "The transfer function must be positive or increasing");
+ }
+ }
+ this.a = a;
+ this.b = b;
+ this.c = c;
+ this.d = d;
+ this.e = e;
+ this.f = f;
+ this.g = g;
+ }
+
/**
* <p>Defines the parameters for the ICC parametric curve type 3, as
* defined in ICC.1:2004-10, section 10.15.</p>
@@ -2219,7 +2365,7 @@
* @throws IllegalArgumentException If the parameters form an invalid transfer function
*/
public TransferParameters(double a, double b, double c, double d, double g) {
- this(a, b, c, d, 0.0, 0.0, g);
+ this(a, b, c, d, 0.0, 0.0, g, false);
}
/**
@@ -2238,51 +2384,7 @@
*/
public TransferParameters(double a, double b, double c, double d, double e,
double f, double g) {
-
- if (Double.isNaN(a) || Double.isNaN(b) || Double.isNaN(c) ||
- Double.isNaN(d) || Double.isNaN(e) || Double.isNaN(f) ||
- Double.isNaN(g)) {
- throw new IllegalArgumentException("Parameters cannot be NaN");
- }
-
- // Next representable float after 1.0
- // We use doubles here but the representation inside our native code is often floats
- if (!(d >= 0.0 && d <= 1.0f + Math.ulp(1.0f))) {
- throw new IllegalArgumentException("Parameter d must be in the range [0..1], " +
- "was " + d);
- }
-
- if (d == 0.0 && (a == 0.0 || g == 0.0)) {
- throw new IllegalArgumentException(
- "Parameter a or g is zero, the transfer function is constant");
- }
-
- if (d >= 1.0 && c == 0.0) {
- throw new IllegalArgumentException(
- "Parameter c is zero, the transfer function is constant");
- }
-
- if ((a == 0.0 || g == 0.0) && c == 0.0) {
- throw new IllegalArgumentException("Parameter a or g is zero," +
- " and c is zero, the transfer function is constant");
- }
-
- if (c < 0.0) {
- throw new IllegalArgumentException("The transfer function must be increasing");
- }
-
- if (a < 0.0 || g < 0.0) {
- throw new IllegalArgumentException("The transfer function must be " +
- "positive or increasing");
- }
-
- this.a = a;
- this.b = b;
- this.c = c;
- this.d = d;
- this.e = e;
- this.f = f;
- this.g = g;
+ this(a, b, c, d, e, f, g, false);
}
@SuppressWarnings("SimplifiableIfStatement")
@@ -2357,6 +2459,36 @@
private static native long nativeCreate(float a, float b, float c, float d,
float e, float f, float g, float[] xyz);
+ private static DoubleUnaryOperator generateOETF(TransferParameters function) {
+ boolean isNonCurveTransferParameters = function.equals(BT2020_HLG_TRANSFER_PARAMETERS)
+ || function.equals(BT2020_PQ_TRANSFER_PARAMETERS);
+ if (isNonCurveTransferParameters) {
+ return function.f == 0.0 && function.g < 0.0 ? x -> transferHLGOETF(x)
+ : x -> transferST2048OETF(x);
+ } else {
+ return function.e == 0.0 && function.f == 0.0
+ ? x -> rcpResponse(x, function.a, function.b,
+ function.c, function.d, function.g)
+ : x -> rcpResponse(x, function.a, function.b, function.c,
+ function.d, function.e, function.f, function.g);
+ }
+ }
+
+ private static DoubleUnaryOperator generateEOTF(TransferParameters function) {
+ boolean isNonCurveTransferParameters = function.equals(BT2020_HLG_TRANSFER_PARAMETERS)
+ || function.equals(BT2020_PQ_TRANSFER_PARAMETERS);
+ if (isNonCurveTransferParameters) {
+ return function.f == 0.0 && function.g < 0.0 ? x -> transferHLGEOTF(x)
+ : x -> transferST2048EOTF(x);
+ } else {
+ return function.e == 0.0 && function.f == 0.0
+ ? x -> response(x, function.a, function.b,
+ function.c, function.d, function.g)
+ : x -> response(x, function.a, function.b, function.c,
+ function.d, function.e, function.f, function.g);
+ }
+ }
+
/**
* <p>Creates a new RGB color space using a 3x3 column-major transform matrix.
* The transform matrix must convert from the RGB space to the profile connection
@@ -2553,16 +2685,8 @@
@NonNull TransferParameters function,
@IntRange(from = MIN_ID, to = MAX_ID) int id) {
this(name, primaries, whitePoint, transform,
- function.e == 0.0 && function.f == 0.0 ?
- x -> rcpResponse(x, function.a, function.b,
- function.c, function.d, function.g) :
- x -> rcpResponse(x, function.a, function.b, function.c,
- function.d, function.e, function.f, function.g),
- function.e == 0.0 && function.f == 0.0 ?
- x -> response(x, function.a, function.b,
- function.c, function.d, function.g) :
- x -> response(x, function.a, function.b, function.c,
- function.d, function.e, function.f, function.g),
+ generateOETF(function),
+ generateEOTF(function),
0.0f, 1.0f, function, id);
}
@@ -3063,7 +3187,12 @@
*/
@Nullable
public TransferParameters getTransferParameters() {
- return mTransferParameters;
+ if (mTransferParameters != null
+ && !mTransferParameters.equals(BT2020_PQ_TRANSFER_PARAMETERS)
+ && !mTransferParameters.equals(BT2020_HLG_TRANSFER_PARAMETERS)) {
+ return mTransferParameters;
+ }
+ return null;
}
@Override
diff --git a/graphics/java/android/graphics/ImageDecoder.java b/graphics/java/android/graphics/ImageDecoder.java
index 239621e..51f99ec 100644
--- a/graphics/java/android/graphics/ImageDecoder.java
+++ b/graphics/java/android/graphics/ImageDecoder.java
@@ -1682,11 +1682,16 @@
* {@link #decodeBitmap decodeBitmap} when setting a non-RGB color space
* such as {@link ColorSpace.Named#CIE_LAB Lab}.</p>
*
- * <p class="note">The specified color space's transfer function must be
+ * <p class="note">Prior to {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
+ * the specified color space's transfer function must be
* an {@link ColorSpace.Rgb.TransferParameters ICC parametric curve}. An
* <code>IllegalArgumentException</code> will be thrown by the decode methods
* if calling {@link ColorSpace.Rgb#getTransferParameters()} on the
- * specified color space returns null.</p>
+ * specified color space returns null.
+ * Starting from {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
+ * the color spaces with non ICC parametric curve transfer function are allowed.
+ * E.g., {@link ColorSpace.Named#BT2020_HLG BT2020_HLG}.
+ * </p>
*
* <p>Like all setters on ImageDecoder, this must be called inside
* {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
diff --git a/graphics/java/android/graphics/Mesh.java b/graphics/java/android/graphics/Mesh.java
index f32e0ee..a599b2c 100644
--- a/graphics/java/android/graphics/Mesh.java
+++ b/graphics/java/android/graphics/Mesh.java
@@ -16,6 +16,9 @@
package android.graphics;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+
import libcore.util.NativeAllocationRegistry;
import java.nio.Buffer;
@@ -25,22 +28,32 @@
* Class representing a mesh object.
*
* This class generates Mesh objects via the
- * {@link #make(MeshSpecification, Mode, Buffer, int, Rect)} and
- * {@link #makeIndexed(MeshSpecification, Mode, Buffer, int, ShortBuffer, Rect)} methods,
+ * {@link #make(MeshSpecification, int, Buffer, int, Rect)} and
+ * {@link #makeIndexed(MeshSpecification, int, Buffer, int, ShortBuffer, Rect)} methods,
* where a {@link MeshSpecification} is required along with various attributes for
* detailing the mesh object, including a mode, vertex buffer, optional index buffer, and bounds
- * for the mesh.
- *
- * @hide
+ * for the mesh. Once generated, a mesh object can be drawn through
+ * {@link Canvas#drawMesh(Mesh, BlendMode, Paint)}
*/
public class Mesh {
private long mNativeMeshWrapper;
private boolean mIsIndexed;
/**
- * Enum to determine how the mesh is represented.
+ * Determines how the mesh is represented and will be drawn.
*/
- public enum Mode {Triangles, TriangleStrip}
+ @IntDef({TRIANGLES, TRIANGLE_STRIP})
+ private @interface Mode {}
+
+ /**
+ * The mesh will be drawn with triangles without utilizing shared vertices.
+ */
+ public static final int TRIANGLES = 0;
+
+ /**
+ * The mesh will be drawn with triangles utilizing shared vertices.
+ */
+ public static final int TRIANGLE_STRIP = 1;
private static class MeshHolder {
public static final NativeAllocationRegistry MESH_SPECIFICATION_REGISTRY =
@@ -52,15 +65,24 @@
* Generates a {@link Mesh} object.
*
* @param meshSpec {@link MeshSpecification} used when generating the mesh.
- * @param mode {@link Mode} enum
- * @param vertexBuffer vertex buffer representing through {@link Buffer}.
- * @param vertexCount the number of vertices represented in the vertexBuffer.
+ * @param mode Determines what mode to draw the mesh in. Must be one of
+ * {@link Mesh#TRIANGLES} or {@link Mesh#TRIANGLE_STRIP}
+ * @param vertexBuffer vertex buffer representing through {@link Buffer}. This provides the data
+ * for all attributes provided within the meshSpec for every vertex. That
+ * is, a vertex buffer should be (attributes size * number of vertices) in
+ * length to be valid. Note that currently implementation will have a CPU
+ * backed buffer generated.
+ * @param vertexCount the number of vertices represented in the vertexBuffer and mesh.
* @param bounds bounds of the mesh object.
* @return a new Mesh object.
*/
- public static Mesh make(MeshSpecification meshSpec, Mode mode, Buffer vertexBuffer,
- int vertexCount, Rect bounds) {
- long nativeMesh = nativeMake(meshSpec.mNativeMeshSpec, mode.ordinal(), vertexBuffer,
+ @NonNull
+ public static Mesh make(@NonNull MeshSpecification meshSpec, @Mode int mode,
+ @NonNull Buffer vertexBuffer, int vertexCount, @NonNull Rect bounds) {
+ if (mode != TRIANGLES && mode != TRIANGLE_STRIP) {
+ throw new IllegalArgumentException("Invalid value passed in for mode parameter");
+ }
+ long nativeMesh = nativeMake(meshSpec.mNativeMeshSpec, mode, vertexBuffer,
vertexBuffer.isDirect(), vertexCount, vertexBuffer.position(), bounds.left,
bounds.top, bounds.right, bounds.bottom);
if (nativeMesh == 0) {
@@ -70,19 +92,32 @@
}
/**
- * Generates an indexed {@link Mesh} object.
+ * Generates a {@link Mesh} object.
*
* @param meshSpec {@link MeshSpecification} used when generating the mesh.
- * @param mode {@link Mode} enum
- * @param vertexBuffer vertex buffer representing through {@link Buffer}.
- * @param vertexCount the number of vertices represented in the vertexBuffer.
- * @param indexBuffer index buffer representing through {@link ShortBuffer}.
+ * @param mode Determines what mode to draw the mesh in. Must be one of
+ * {@link Mesh#TRIANGLES} or {@link Mesh#TRIANGLE_STRIP}
+ * @param vertexBuffer vertex buffer representing through {@link Buffer}. This provides the data
+ * for all attributes provided within the meshSpec for every vertex. That
+ * is, a vertex buffer should be (attributes size * number of vertices) in
+ * length to be valid. Note that currently implementation will have a CPU
+ * backed buffer generated.
+ * @param vertexCount the number of vertices represented in the vertexBuffer and mesh.
+ * @param indexBuffer index buffer representing through {@link ShortBuffer}. Indices are
+ * required to be 16 bits, so ShortBuffer is necessary. Note that
+ * currently implementation will have a CPU
+ * backed buffer generated.
* @param bounds bounds of the mesh object.
* @return a new Mesh object.
*/
- public static Mesh makeIndexed(MeshSpecification meshSpec, Mode mode, Buffer vertexBuffer,
- int vertexCount, ShortBuffer indexBuffer, Rect bounds) {
- long nativeMesh = nativeMakeIndexed(meshSpec.mNativeMeshSpec, mode.ordinal(), vertexBuffer,
+ @NonNull
+ public static Mesh makeIndexed(@NonNull MeshSpecification meshSpec, @Mode int mode,
+ @NonNull Buffer vertexBuffer, int vertexCount, @NonNull ShortBuffer indexBuffer,
+ @NonNull Rect bounds) {
+ if (mode != TRIANGLES && mode != TRIANGLE_STRIP) {
+ throw new IllegalArgumentException("Invalid value passed in for mode parameter");
+ }
+ long nativeMesh = nativeMakeIndexed(meshSpec.mNativeMeshSpec, mode, vertexBuffer,
vertexBuffer.isDirect(), vertexCount, vertexBuffer.position(), indexBuffer,
indexBuffer.isDirect(), indexBuffer.capacity(), indexBuffer.position(), bounds.left,
bounds.top, bounds.right, bounds.bottom);
@@ -93,36 +128,45 @@
}
/**
- * Sets the uniform color value corresponding to the shader assigned to the mesh.
+ * Sets the uniform color value corresponding to the shader assigned to the mesh. If the shader
+ * does not have a uniform with that name or if the uniform is declared with a type other than
+ * vec3 or vec4 and corresponding layout(color) annotation then an IllegalArgumentExcepton is
+ * thrown.
*
* @param uniformName name matching the color uniform declared in the shader program.
* @param color the provided sRGB color will be converted into the shader program's output
* colorspace and be available as a vec4 uniform in the program.
*/
- public void setColorUniform(String uniformName, int color) {
+ public void setColorUniform(@NonNull String uniformName, int color) {
setUniform(uniformName, Color.valueOf(color).getComponents(), true);
}
/**
- * Sets the uniform color value corresponding to the shader assigned to the mesh.
+ * Sets the uniform color value corresponding to the shader assigned to the mesh. If the shader
+ * does not have a uniform with that name or if the uniform is declared with a type other than
+ * vec3 or vec4 and corresponding layout(color) annotation then an IllegalArgumentExcepton is
+ * thrown.
*
* @param uniformName name matching the color uniform declared in the shader program.
* @param color the provided sRGB color will be converted into the shader program's output
* colorspace and be available as a vec4 uniform in the program.
*/
- public void setColorUniform(String uniformName, long color) {
+ public void setColorUniform(@NonNull String uniformName, long color) {
Color exSRGB = Color.valueOf(color).convert(ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB));
setUniform(uniformName, exSRGB.getComponents(), true);
}
/**
- * Sets the uniform color value corresponding to the shader assigned to the mesh.
+ * Sets the uniform color value corresponding to the shader assigned to the mesh. If the shader
+ * does not have a uniform with that name or if the uniform is declared with a type other than
+ * vec3 or vec4 and corresponding layout(color) annotation then an IllegalArgumentExcepton is
+ * thrown.
*
* @param uniformName name matching the color uniform declared in the shader program.
* @param color the provided sRGB color will be converted into the shader program's output
* colorspace and will be made available as a vec4 uniform in the program.
*/
- public void setColorUniform(String uniformName, Color color) {
+ public void setColorUniform(@NonNull String uniformName, @NonNull Color color) {
if (color == null) {
throw new NullPointerException("The color parameter must not be null");
}
@@ -132,28 +176,34 @@
}
/**
- * Sets the uniform color value corresponding to the shader assigned to the mesh.
+ * Sets the uniform value corresponding to the shader assigned to the mesh. If the shader does
+ * not have a uniform with that name or if the uniform is declared with a type other than a
+ * float or float[1] then an IllegalArgumentException is thrown.
*
* @param uniformName name matching the float uniform declared in the shader program.
* @param value float value corresponding to the float uniform with the given name.
*/
- public void setFloatUniform(String uniformName, float value) {
+ public void setFloatUniform(@NonNull String uniformName, float value) {
setFloatUniform(uniformName, value, 0.0f, 0.0f, 0.0f, 1);
}
/**
- * Sets the uniform color value corresponding to the shader assigned to the mesh.
+ * Sets the uniform value corresponding to the shader assigned to the mesh. If the shader does
+ * not have a uniform with that name or if the uniform is declared with a type other than a
+ * vec2 or float[2] then an IllegalArgumentException is thrown.
*
* @param uniformName name matching the float uniform declared in the shader program.
* @param value1 first float value corresponding to the float uniform with the given name.
* @param value2 second float value corresponding to the float uniform with the given name.
*/
- public void setFloatUniform(String uniformName, float value1, float value2) {
+ public void setFloatUniform(@NonNull String uniformName, float value1, float value2) {
setFloatUniform(uniformName, value1, value2, 0.0f, 0.0f, 2);
}
/**
- * Sets the uniform color value corresponding to the shader assigned to the mesh.
+ * Sets the uniform value corresponding to the shader assigned to the mesh. If the shader does
+ * not have a uniform with that name or if the uniform is declared with a type other than a
+ * vec3 or float[3] then an IllegalArgumentException is thrown.
*
* @param uniformName name matching the float uniform declared in the shader program.
* @param value1 first float value corresponding to the float uniform with the given name.
@@ -161,12 +211,15 @@
* @param value3 third float value corresponding to the float unifiform with the given
* name.
*/
- public void setFloatUniform(String uniformName, float value1, float value2, float value3) {
+ public void setFloatUniform(
+ @NonNull String uniformName, float value1, float value2, float value3) {
setFloatUniform(uniformName, value1, value2, value3, 0.0f, 3);
}
/**
- * Sets the uniform color value corresponding to the shader assigned to the mesh.
+ * Sets the uniform value corresponding to the shader assigned to the mesh. If the shader does
+ * not have a uniform with that name or if the uniform is declared with a type other than a
+ * vec4 or float[4] then an IllegalArgumentException is thrown.
*
* @param uniformName name matching the float uniform declared in the shader program.
* @param value1 first float value corresponding to the float uniform with the given name.
@@ -175,17 +228,20 @@
* @param value4 fourth float value corresponding to the float uniform with the given name.
*/
public void setFloatUniform(
- String uniformName, float value1, float value2, float value3, float value4) {
+ @NonNull String uniformName, float value1, float value2, float value3, float value4) {
setFloatUniform(uniformName, value1, value2, value3, value4, 4);
}
/**
- * Sets the uniform color value corresponding to the shader assigned to the mesh.
+ * Sets the uniform value corresponding to the shader assigned to the mesh. If the shader does
+ * not have a uniform with that name or if the uniform is declared with a type other than a
+ * float (for N=1), vecN, or float[N], where N is the length of the values param, then an
+ * IllegalArgumentException is thrown.
*
* @param uniformName name matching the float uniform declared in the shader program.
* @param values float value corresponding to the vec4 float uniform with the given name.
*/
- public void setFloatUniform(String uniformName, float[] values) {
+ public void setFloatUniform(@NonNull String uniformName, @NonNull float[] values) {
setUniform(uniformName, values, false);
}
@@ -210,40 +266,48 @@
}
/**
- * Sets the uniform color value corresponding to the shader assigned to the mesh.
+ * Sets the uniform value corresponding to the shader assigned to the mesh. If the shader does
+ * not have a uniform with that name or if the uniform is declared with a type other than int
+ * or int[1] then an IllegalArgumentException is thrown.
*
* @param uniformName name matching the int uniform delcared in the shader program.
* @param value value corresponding to the int uniform with the given name.
*/
- public void setIntUniform(String uniformName, int value) {
+ public void setIntUniform(@NonNull String uniformName, int value) {
setIntUniform(uniformName, value, 0, 0, 0, 1);
}
/**
- * Sets the uniform color value corresponding to the shader assigned to the mesh.
+ * Sets the uniform value corresponding to the shader assigned to the mesh. If the shader does
+ * not have a uniform with that name or if the uniform is declared with a type other than ivec2
+ * or int[2] then an IllegalArgumentException is thrown.
*
* @param uniformName name matching the int uniform delcared in the shader program.
* @param value1 first value corresponding to the int uniform with the given name.
* @param value2 second value corresponding to the int uniform with the given name.
*/
- public void setIntUniform(String uniformName, int value1, int value2) {
+ public void setIntUniform(@NonNull String uniformName, int value1, int value2) {
setIntUniform(uniformName, value1, value2, 0, 0, 2);
}
/**
- * Sets the uniform color value corresponding to the shader assigned to the mesh.
+ * Sets the uniform value corresponding to the shader assigned to the mesh. If the shader does
+ * not have a uniform with that name or if the uniform is declared with a type other than ivec3
+ * or int[3] then an IllegalArgumentException is thrown.
*
* @param uniformName name matching the int uniform delcared in the shader program.
* @param value1 first value corresponding to the int uniform with the given name.
* @param value2 second value corresponding to the int uniform with the given name.
* @param value3 third value corresponding to the int uniform with the given name.
*/
- public void setIntUniform(String uniformName, int value1, int value2, int value3) {
+ public void setIntUniform(@NonNull String uniformName, int value1, int value2, int value3) {
setIntUniform(uniformName, value1, value2, value3, 0, 3);
}
/**
- * Sets the uniform color value corresponding to the shader assigned to the mesh.
+ * Sets the uniform value corresponding to the shader assigned to the mesh. If the shader does
+ * not have a uniform with that name or if the uniform is declared with a type other than ivec4
+ * or int[4] then an IllegalArgumentException is thrown.
*
* @param uniformName name matching the int uniform delcared in the shader program.
* @param value1 first value corresponding to the int uniform with the given name.
@@ -251,17 +315,21 @@
* @param value3 third value corresponding to the int uniform with the given name.
* @param value4 fourth value corresponding to the int uniform with the given name.
*/
- public void setIntUniform(String uniformName, int value1, int value2, int value3, int value4) {
+ public void setIntUniform(
+ @NonNull String uniformName, int value1, int value2, int value3, int value4) {
setIntUniform(uniformName, value1, value2, value3, value4, 4);
}
/**
- * Sets the uniform color value corresponding to the shader assigned to the mesh.
+ * Sets the uniform value corresponding to the shader assigned to the mesh. If the shader does
+ * not have a uniform with that name or if the uniform is declared with a type other than an
+ * int (for N=1), ivecN, or int[N], where N is the length of the values param, then an
+ * IllegalArgumentException is thrown.
*
* @param uniformName name matching the int uniform delcared in the shader program.
* @param values int values corresponding to the vec4 int uniform with the given name.
*/
- public void setIntUniform(String uniformName, int[] values) {
+ public void setIntUniform(@NonNull String uniformName, @NonNull int[] values) {
if (uniformName == null) {
throw new NullPointerException("The uniformName parameter must not be null");
}
diff --git a/graphics/java/android/graphics/MeshSpecification.java b/graphics/java/android/graphics/MeshSpecification.java
index 45c13af..52b4002 100644
--- a/graphics/java/android/graphics/MeshSpecification.java
+++ b/graphics/java/android/graphics/MeshSpecification.java
@@ -17,9 +17,12 @@
package android.graphics;
import android.annotation.IntDef;
+import android.annotation.NonNull;
import libcore.util.NativeAllocationRegistry;
+import java.util.List;
+
/**
* Class responsible for holding specifications for {@link Mesh} creations. This class
* generates a {@link MeshSpecification} via the Make method, where multiple parameters to set up
@@ -35,41 +38,78 @@
*
* These should be kept in mind when generating a mesh specification, as exceeding them will
* lead to errors.
- *
- * @hide
*/
public class MeshSpecification {
long mNativeMeshSpec;
/**
- * Constants for {@link #make(Attribute[], int, Varying[], String, String, ColorSpace, int)}
- * to determine alpha type
+ * Constants for {@link #make(List, int, List, String, String)}
+ * to determine alpha type. Describes how to interpret the alpha component of a pixel.
*/
@IntDef({UNKNOWN, OPAQUE, PREMUL, UNPREMULT})
- public @interface AlphaType {
- }
+ private @interface AlphaType {}
+ /**
+ * uninitialized.
+ */
public static final int UNKNOWN = 0;
+
+ /**
+ * Pixel is opaque.
+ */
public static final int OPAQUE = 1;
+
+ /**
+ * Pixel components are premultiplied by alpha.
+ */
public static final int PREMUL = 2;
+
+ /**
+ * Pixel components are independent of alpha.
+ */
public static final int UNPREMULT = 3;
/**
* Constants for {@link Attribute} and {@link Varying} for determining the data type.
*/
@IntDef({FLOAT, FLOAT2, FLOAT3, FLOAT4, UBYTE4})
- public @interface Type {
- }
+ private @interface Type {}
+ /**
+ * Represents one float. Its equivalent shader type is float.
+ */
public static final int FLOAT = 0;
+
+ /**
+ * Represents two floats. Its equivalent shader type is float2.
+ */
public static final int FLOAT2 = 1;
+
+ /**
+ * Represents three floats. Its equivalent shader type is float3.
+ */
public static final int FLOAT3 = 2;
+
+ /**
+ * Represents four floats. Its equivalent shader type is float4.
+ */
public static final int FLOAT4 = 3;
+
+ /**
+ * Represents four bytes. Its equivalent shader type is half4.
+ */
public static final int UBYTE4 = 4;
/**
* Data class to represent a single attribute in a shader. Note that type parameter must be
* one of {@link #FLOAT}, {@link #FLOAT2}, {@link #FLOAT3}, {@link #FLOAT4}, or {@link #UBYTE4}.
+ *
+ * Note that offset is the offset in number of bytes. For example, if we had two attributes
+ *
+ * Float3 att1
+ * Float att2
+ *
+ * att1 would have an offset of 0, while att2 would have an offset of 12 bytes.
*/
public static class Attribute {
@Type
@@ -77,7 +117,7 @@
private int mOffset;
private String mName;
- public Attribute(@Type int type, int offset, String name) {
+ public Attribute(@Type int type, int offset, @NonNull String name) {
mType = type;
mOffset = offset;
mName = name;
@@ -93,7 +133,7 @@
private int mType;
private String mName;
- public Varying(@Type int type, String name) {
+ public Varying(@Type int type, @NonNull String name) {
mType = type;
mName = name;
}
@@ -106,20 +146,28 @@
}
/**
- * Creates a {@link MeshSpecification} object.
+ * Creates a {@link MeshSpecification} object for use within {@link Mesh}.
*
* @param attributes list of attributes represented by {@link Attribute}. Can hold a max of
* 8.
- * @param vertexStride length of vertex stride. Max of 1024 is accepted.
+ * @param vertexStride length of vertex stride in bytes. This should be the size of a single
+ * vertex' attributes. Max of 1024 is accepted.
* @param varyings List of varyings represented by {@link Varying}. Can hold a max of 6.
- * @param vertexShader vertex shader to be supplied to the mesh.
- * @param fragmentShader fragment shader to be suppied to the mesh.
+ * Note that `position` is provided by default, does not need to be
+ * provided in the list, and does not count towards
+ * the 6 varyings allowed.
+ * @param vertexShader vertex shader to be supplied to the mesh. Ensure that the position
+ * varying is set within the shader to get proper results.
+ * @param fragmentShader fragment shader to be supplied to the mesh.
* @return {@link MeshSpecification} object for use when creating {@link Mesh}
*/
- public static MeshSpecification make(Attribute[] attributes, int vertexStride,
- Varying[] varyings, String vertexShader, String fragmentShader) {
- long nativeMeshSpec =
- nativeMake(attributes, vertexStride, varyings, vertexShader, fragmentShader);
+ @NonNull
+ public static MeshSpecification make(@NonNull List<Attribute> attributes, int vertexStride,
+ @NonNull List<Varying> varyings, @NonNull String vertexShader,
+ @NonNull String fragmentShader) {
+ long nativeMeshSpec = nativeMake(attributes.toArray(new Attribute[attributes.size()]),
+ vertexStride, varyings.toArray(new Varying[varyings.size()]), vertexShader,
+ fragmentShader);
if (nativeMeshSpec == 0) {
throw new IllegalArgumentException("MeshSpecification construction failed");
}
@@ -131,17 +179,24 @@
*
* @param attributes list of attributes represented by {@link Attribute}. Can hold a max of
* 8.
- * @param vertexStride length of vertex stride. Max of 1024 is accepted.
- * @param varyings List of varyings represented by {@link Varying}. Can hold a max of
- * 6.
- * @param vertexShader vertex shader to be supplied to the mesh.
+ * @param vertexStride length of vertex stride in bytes. This should be the size of a single
+ * vertex' attributes. Max of 1024 is accepted.
+ * @param varyings List of varyings represented by {@link Varying}. Can hold a max of 6.
+ * Note that `position` is provided by default, does not need to be
+ * provided in the list, and does not count towards
+ * the 6 varyings allowed.
+ * @param vertexShader vertex shader to be supplied to the mesh. Ensure that the position
+ * varying is set within the shader to get proper results.
* @param fragmentShader fragment shader to be supplied to the mesh.
* @param colorSpace {@link ColorSpace} to tell what color space to work in.
* @return {@link MeshSpecification} object for use when creating {@link Mesh}
*/
- public static MeshSpecification make(Attribute[] attributes, int vertexStride,
- Varying[] varyings, String vertexShader, String fragmentShader, ColorSpace colorSpace) {
- long nativeMeshSpec = nativeMakeWithCS(attributes, vertexStride, varyings, vertexShader,
+ @NonNull
+ public static MeshSpecification make(@NonNull List<Attribute> attributes, int vertexStride,
+ @NonNull List<Varying> varyings, @NonNull String vertexShader,
+ @NonNull String fragmentShader, @NonNull ColorSpace colorSpace) {
+ long nativeMeshSpec = nativeMakeWithCS(attributes.toArray(new Attribute[attributes.size()]),
+ vertexStride, varyings.toArray(new Varying[varyings.size()]), vertexShader,
fragmentShader, colorSpace.getNativeInstance());
if (nativeMeshSpec == 0) {
throw new IllegalArgumentException("MeshSpecification construction failed");
@@ -154,20 +209,33 @@
*
* @param attributes list of attributes represented by {@link Attribute}. Can hold a max of
* 8.
- * @param vertexStride length of vertex stride. Max of 1024 is accepted.
+ * @param vertexStride length of vertex stride in bytes. This should be the size of a single
+ * vertex' attributes. Max of 1024 is accepted.
* @param varyings List of varyings represented by {@link Varying}. Can hold a max of 6.
- * @param vertexShader vertex shader code to be supplied to the mesh.
- * @param fragmentShader fragment shader code to be suppied to the mesh.
+ * Note that `position` is provided by default, does not need to be
+ * provided in the list, and does not count towards
+ * the 6 varyings allowed.
+ * @param vertexShader vertex shader to be supplied to the mesh. Ensure that the position
+ * varying is set within the shader to get proper results.
+ * @param fragmentShader fragment shader to be supplied to the mesh.
* @param colorSpace {@link ColorSpace} to tell what color space to work in.
* @param alphaType Describes how to interpret the alpha component for a pixel. Must be
- * one of {@link AlphaType} values.
+ * one of
+ * {@link MeshSpecification#UNKNOWN},
+ * {@link MeshSpecification#OPAQUE},
+ * {@link MeshSpecification#PREMUL}, or
+ * {@link MeshSpecification#UNPREMULT}
* @return {@link MeshSpecification} object for use when creating {@link Mesh}
*/
- public static MeshSpecification make(Attribute[] attributes, int vertexStride,
- Varying[] varyings, String vertexShader, String fragmentShader, ColorSpace colorSpace,
+ @NonNull
+ public static MeshSpecification make(@NonNull List<Attribute> attributes, int vertexStride,
+ @NonNull List<Varying> varyings, @NonNull String vertexShader,
+ @NonNull String fragmentShader, @NonNull ColorSpace colorSpace,
@AlphaType int alphaType) {
- long nativeMeshSpec = nativeMakeWithAlpha(attributes, vertexStride, varyings, vertexShader,
- fragmentShader, colorSpace.getNativeInstance(), alphaType);
+ long nativeMeshSpec =
+ nativeMakeWithAlpha(attributes.toArray(new Attribute[attributes.size()]),
+ vertexStride, varyings.toArray(new Varying[varyings.size()]), vertexShader,
+ fragmentShader, colorSpace.getNativeInstance(), alphaType);
if (nativeMeshSpec == 0) {
throw new IllegalArgumentException("MeshSpecification construction failed");
}
diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml
index 23db233..774f6c6 100644
--- a/libs/WindowManager/Shell/res/values/config.xml
+++ b/libs/WindowManager/Shell/res/values/config.xml
@@ -115,4 +115,10 @@
<!-- Components support to launch multiple instances into split-screen -->
<string-array name="config_appsSupportMultiInstancesSplit">
</string-array>
+
+ <!-- Whether the extended restart dialog is enabled -->
+ <bool name="config_letterboxIsRestartDialogEnabled">false</bool>
+
+ <!-- Whether the additional education about reachability is enabled -->
+ <bool name="config_letterboxIsReachabilityEducationEnabled">false</bool>
</resources>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java
new file mode 100644
index 0000000..4f33a71
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java
@@ -0,0 +1,119 @@
+/*
+ * 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.compatui;
+
+import android.content.Context;
+import android.provider.DeviceConfig;
+
+import androidx.annotation.NonNull;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.dagger.WMSingleton;
+
+import javax.inject.Inject;
+
+/**
+ * Configuration flags for the CompatUX implementation
+ */
+@WMSingleton
+public class CompatUIConfiguration implements DeviceConfig.OnPropertiesChangedListener {
+
+ static final String KEY_ENABLE_LETTERBOX_RESTART_DIALOG = "enable_letterbox_restart_dialog";
+
+ static final String KEY_ENABLE_LETTERBOX_REACHABILITY_EDUCATION =
+ "enable_letterbox_reachability_education";
+
+ // Whether the extended restart dialog is enabled
+ private boolean mIsRestartDialogEnabled;
+
+ // Whether the additional education about reachability is enabled
+ private boolean mIsReachabilityEducationEnabled;
+
+ // Whether the extended restart dialog is enabled
+ private boolean mIsRestartDialogOverrideEnabled;
+
+ // Whether the additional education about reachability is enabled
+ private boolean mIsReachabilityEducationOverrideEnabled;
+
+ // Whether the extended restart dialog is allowed from backend
+ private boolean mIsLetterboxRestartDialogAllowed;
+
+ // Whether the additional education about reachability is allowed from backend
+ private boolean mIsLetterboxReachabilityEducationAllowed;
+
+ @Inject
+ public CompatUIConfiguration(Context context, @ShellMainThread ShellExecutor mainExecutor) {
+ mIsRestartDialogEnabled = context.getResources().getBoolean(
+ R.bool.config_letterboxIsRestartDialogEnabled);
+ mIsReachabilityEducationEnabled = context.getResources().getBoolean(
+ R.bool.config_letterboxIsReachabilityEducationEnabled);
+ mIsLetterboxRestartDialogAllowed = DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_WINDOW_MANAGER, KEY_ENABLE_LETTERBOX_RESTART_DIALOG, false);
+ mIsLetterboxReachabilityEducationAllowed = DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_WINDOW_MANAGER, KEY_ENABLE_LETTERBOX_REACHABILITY_EDUCATION,
+ false);
+ DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_APP_COMPAT, mainExecutor,
+ this);
+ }
+
+ /**
+ * @return {@value true} if the restart dialog is enabled.
+ */
+ boolean isRestartDialogEnabled() {
+ return mIsRestartDialogOverrideEnabled || (mIsRestartDialogEnabled
+ && mIsLetterboxRestartDialogAllowed);
+ }
+
+ /**
+ * Enables/Disables the restart education dialog
+ */
+ void setIsRestartDialogOverrideEnabled(boolean enabled) {
+ mIsRestartDialogOverrideEnabled = enabled;
+ }
+
+ /**
+ * @return {@value true} if the reachability education is enabled.
+ */
+ boolean isReachabilityEducationEnabled() {
+ return mIsReachabilityEducationOverrideEnabled || (mIsReachabilityEducationEnabled
+ && mIsLetterboxReachabilityEducationAllowed);
+ }
+
+ /**
+ * Enables/Disables the reachability education
+ */
+ void setIsReachabilityEducationOverrideEnabled(boolean enabled) {
+ mIsReachabilityEducationOverrideEnabled = enabled;
+ }
+
+ @Override
+ public void onPropertiesChanged(@NonNull DeviceConfig.Properties properties) {
+ // TODO(b/263349751): Update flag and default value to true
+ if (properties.getKeyset().contains(KEY_ENABLE_LETTERBOX_RESTART_DIALOG)) {
+ mIsLetterboxRestartDialogAllowed = DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_WINDOW_MANAGER, KEY_ENABLE_LETTERBOX_RESTART_DIALOG,
+ false);
+ }
+ if (properties.getKeyset().contains(KEY_ENABLE_LETTERBOX_REACHABILITY_EDUCATION)) {
+ mIsLetterboxReachabilityEducationAllowed = DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_WINDOW_MANAGER,
+ KEY_ENABLE_LETTERBOX_REACHABILITY_EDUCATION, false);
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIShellCommandHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIShellCommandHandler.java
new file mode 100644
index 0000000..4fb18e2
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIShellCommandHandler.java
@@ -0,0 +1,103 @@
+/*
+ * 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.compatui;
+
+import com.android.wm.shell.dagger.WMSingleton;
+import com.android.wm.shell.sysui.ShellCommandHandler;
+
+import java.io.PrintWriter;
+import java.util.function.Consumer;
+
+import javax.inject.Inject;
+
+/**
+ * Handles the shell commands for the CompatUX.
+ *
+ * <p> Use with {@code adb shell dumpsys activity service SystemUIService WMShell compatui
+ * <command>}.
+ */
+@WMSingleton
+public final class CompatUIShellCommandHandler implements
+ ShellCommandHandler.ShellCommandActionHandler {
+
+ private final CompatUIConfiguration mCompatUIConfiguration;
+ private final ShellCommandHandler mShellCommandHandler;
+
+ @Inject
+ public CompatUIShellCommandHandler(ShellCommandHandler shellCommandHandler,
+ CompatUIConfiguration compatUIConfiguration) {
+ mShellCommandHandler = shellCommandHandler;
+ mCompatUIConfiguration = compatUIConfiguration;
+ }
+
+ void onInit() {
+ mShellCommandHandler.addCommandCallback("compatui", this, this);
+ }
+
+ @Override
+ public boolean onShellCommand(String[] args, PrintWriter pw) {
+ if (args.length != 2) {
+ pw.println("Invalid command: " + args[0]);
+ return false;
+ }
+ switch (args[0]) {
+ case "restartDialogEnabled":
+ return invokeOrError(args[1], pw,
+ mCompatUIConfiguration::setIsRestartDialogOverrideEnabled);
+ case "reachabilityEducationEnabled":
+ return invokeOrError(args[1], pw,
+ mCompatUIConfiguration::setIsReachabilityEducationOverrideEnabled);
+ default:
+ pw.println("Invalid command: " + args[0]);
+ return false;
+ }
+ }
+
+ @Override
+ public void printShellCommandHelp(PrintWriter pw, String prefix) {
+ pw.println(prefix + "restartDialogEnabled [0|false|1|true]");
+ pw.println(prefix + " Enable/Disable the restart education dialog for Size Compat Mode");
+ pw.println(prefix + "reachabilityEducationEnabled [0|false|1|true]");
+ pw.println(prefix
+ + " Enable/Disable the restart education dialog for letterbox reachability");
+ pw.println(prefix + " Disable the restart education dialog for letterbox reachability");
+ }
+
+ private static boolean invokeOrError(String input, PrintWriter pw,
+ Consumer<Boolean> setter) {
+ Boolean asBoolean = strToBoolean(input);
+ if (asBoolean == null) {
+ pw.println("Error: expected true, 1, false, 0.");
+ return false;
+ }
+ setter.accept(asBoolean);
+ return true;
+ }
+
+ // Converts a String to boolean if possible or it returns null otherwise
+ private static Boolean strToBoolean(String str) {
+ switch(str) {
+ case "1":
+ case "true":
+ return true;
+ case "0":
+ case "false":
+ return false;
+ }
+ return null;
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/DialogAnimationController.java
similarity index 85%
rename from libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduAnimationController.java
rename to libs/WindowManager/Shell/src/com/android/wm/shell/compatui/DialogAnimationController.java
index 3061eab..7475fea 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/DialogAnimationController.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.wm.shell.compatui.letterboxedu;
+package com.android.wm.shell.compatui;
import static com.android.internal.R.styleable.WindowAnimation_windowEnterAnimation;
import static com.android.internal.R.styleable.WindowAnimation_windowExitAnimation;
@@ -38,10 +38,15 @@
import com.android.internal.policy.TransitionAnimation;
/**
- * Controls the enter/exit animations of the letterbox education.
+ * Controls the enter/exit a dialog.
+ *
+ * @param <T> The {@link DialogContainerSupplier} to use
*/
-class LetterboxEduAnimationController {
- private static final String TAG = "LetterboxEduAnimation";
+public class DialogAnimationController<T extends DialogContainerSupplier> {
+
+ // The alpha of a background is a number between 0 (fully transparent) to 255 (fully opaque).
+ // 204 is simply 255 * 0.8.
+ static final int BACKGROUND_DIM_ALPHA = 204;
// If shell transitions are enabled, startEnterAnimation will be called after all transitions
// have finished, and therefore the start delay should be shorter.
@@ -49,6 +54,7 @@
private final TransitionAnimation mTransitionAnimation;
private final String mPackageName;
+ private final String mTag;
@AnyRes
private final int mAnimStyleResId;
@@ -57,23 +63,24 @@
@Nullable
private Animator mBackgroundDimAnimator;
- LetterboxEduAnimationController(Context context) {
- mTransitionAnimation = new TransitionAnimation(context, /* debug= */ false, TAG);
+ public DialogAnimationController(Context context, String tag) {
+ mTransitionAnimation = new TransitionAnimation(context, /* debug= */ false, tag);
mAnimStyleResId = (new ContextThemeWrapper(context,
android.R.style.ThemeOverlay_Material_Dialog).getTheme()).obtainStyledAttributes(
com.android.internal.R.styleable.Window).getResourceId(
com.android.internal.R.styleable.Window_windowAnimationStyle, 0);
mPackageName = context.getPackageName();
+ mTag = tag;
}
/**
* Starts both background dim fade-in animation and the dialog enter animation.
*/
- void startEnterAnimation(@NonNull LetterboxEduDialogLayout layout, Runnable endCallback) {
+ public void startEnterAnimation(@NonNull T layout, Runnable endCallback) {
// Cancel any previous animation if it's still running.
cancelAnimation();
- final View dialogContainer = layout.getDialogContainer();
+ final View dialogContainer = layout.getDialogContainerView();
mDialogAnimation = loadAnimation(WindowAnimation_windowEnterAnimation);
if (mDialogAnimation == null) {
endCallback.run();
@@ -86,8 +93,8 @@
endCallback.run();
}));
- mBackgroundDimAnimator = getAlphaAnimator(layout.getBackgroundDim(),
- /* endAlpha= */ LetterboxEduDialogLayout.BACKGROUND_DIM_ALPHA,
+ mBackgroundDimAnimator = getAlphaAnimator(layout.getBackgroundDimDrawable(),
+ /* endAlpha= */ BACKGROUND_DIM_ALPHA,
mDialogAnimation.getDuration());
mBackgroundDimAnimator.addListener(getDimAnimatorListener());
@@ -101,11 +108,11 @@
/**
* Starts both the background dim fade-out animation and the dialog exit animation.
*/
- void startExitAnimation(@NonNull LetterboxEduDialogLayout layout, Runnable endCallback) {
+ public void startExitAnimation(@NonNull T layout, Runnable endCallback) {
// Cancel any previous animation if it's still running.
cancelAnimation();
- final View dialogContainer = layout.getDialogContainer();
+ final View dialogContainer = layout.getDialogContainerView();
mDialogAnimation = loadAnimation(WindowAnimation_windowExitAnimation);
if (mDialogAnimation == null) {
endCallback.run();
@@ -119,8 +126,8 @@
endCallback.run();
}));
- mBackgroundDimAnimator = getAlphaAnimator(layout.getBackgroundDim(), /* endAlpha= */ 0,
- mDialogAnimation.getDuration());
+ mBackgroundDimAnimator = getAlphaAnimator(layout.getBackgroundDimDrawable(),
+ /* endAlpha= */ 0, mDialogAnimation.getDuration());
mBackgroundDimAnimator.addListener(getDimAnimatorListener());
dialogContainer.startAnimation(mDialogAnimation);
@@ -130,7 +137,7 @@
/**
* Cancels all animations and resets the state of the controller.
*/
- void cancelAnimation() {
+ public void cancelAnimation() {
if (mDialogAnimation != null) {
mDialogAnimation.cancel();
mDialogAnimation = null;
@@ -145,7 +152,7 @@
Animation animation = mTransitionAnimation.loadAnimationAttr(mPackageName, mAnimStyleResId,
animAttr, /* translucent= */ false);
if (animation == null) {
- Log.e(TAG, "Failed to load animation " + animAttr);
+ Log.e(mTag, "Failed to load animation " + animAttr);
}
return animation;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/DialogContainerSupplier.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/DialogContainerSupplier.java
new file mode 100644
index 0000000..7eea446
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/DialogContainerSupplier.java
@@ -0,0 +1,36 @@
+/*
+ * 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.compatui;
+
+import android.graphics.drawable.Drawable;
+import android.view.View;
+
+/**
+ * A component which can provide a {@link View} to use as a container for a Dialog
+ */
+public interface DialogContainerSupplier {
+
+ /**
+ * @return The {@link View} to use as a container for a Dialog
+ */
+ View getDialogContainerView();
+
+ /**
+ * @return The {@link Drawable} to use as background of the dialog.
+ */
+ Drawable getBackgroundDimDrawable();
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayout.java
index 2e0b09e..9232f36 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayout.java
@@ -26,6 +26,7 @@
import androidx.constraintlayout.widget.ConstraintLayout;
import com.android.wm.shell.R;
+import com.android.wm.shell.compatui.DialogContainerSupplier;
/**
* Container for Letterbox Education Dialog and background dim.
@@ -33,11 +34,7 @@
* <p>This layout should fill the entire task and the background around the dialog acts as the
* background dim which dismisses the dialog when clicked.
*/
-class LetterboxEduDialogLayout extends ConstraintLayout {
-
- // The alpha of a background is a number between 0 (fully transparent) to 255 (fully opaque).
- // 204 is simply 255 * 0.8.
- static final int BACKGROUND_DIM_ALPHA = 204;
+class LetterboxEduDialogLayout extends ConstraintLayout implements DialogContainerSupplier {
private View mDialogContainer;
private TextView mDialogTitle;
@@ -60,18 +57,20 @@
super(context, attrs, defStyleAttr, defStyleRes);
}
- View getDialogContainer() {
+ @Override
+ public View getDialogContainerView() {
return mDialogContainer;
}
+ @Override
+ public Drawable getBackgroundDimDrawable() {
+ return mBackgroundDim;
+ }
+
TextView getDialogTitle() {
return mDialogTitle;
}
- Drawable getBackgroundDim() {
- return mBackgroundDim;
- }
-
/**
* Register a callback for the dismiss button and background dim.
*
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java
index 867d0ef..c14c009 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java
@@ -37,6 +37,7 @@
import com.android.wm.shell.common.DockStateReader;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.compatui.CompatUIWindowManagerAbstract;
+import com.android.wm.shell.compatui.DialogAnimationController;
import com.android.wm.shell.transition.Transitions;
/**
@@ -63,7 +64,7 @@
*/
private final SharedPreferences mSharedPreferences;
- private final LetterboxEduAnimationController mAnimationController;
+ private final DialogAnimationController<LetterboxEduDialogLayout> mAnimationController;
private final Transitions mTransitions;
@@ -96,14 +97,17 @@
DisplayLayout displayLayout, Transitions transitions,
Runnable onDismissCallback, DockStateReader dockStateReader) {
this(context, taskInfo, syncQueue, taskListener, displayLayout, transitions,
- onDismissCallback, new LetterboxEduAnimationController(context), dockStateReader);
+ onDismissCallback,
+ new DialogAnimationController<>(context, /* tag */ "LetterboxEduWindowManager"),
+ dockStateReader);
}
@VisibleForTesting
LetterboxEduWindowManager(Context context, TaskInfo taskInfo,
SyncTransactionQueue syncQueue, ShellTaskOrganizer.TaskListener taskListener,
DisplayLayout displayLayout, Transitions transitions, Runnable onDismissCallback,
- LetterboxEduAnimationController animationController, DockStateReader dockStateReader) {
+ DialogAnimationController<LetterboxEduDialogLayout> animationController,
+ DockStateReader dockStateReader) {
super(context, taskInfo, syncQueue, taskListener, displayLayout);
mTransitions = transitions;
mOnDismissCallback = onDismissCallback;
@@ -160,7 +164,7 @@
if (mLayout == null) {
return;
}
- final View dialogContainer = mLayout.getDialogContainer();
+ final View dialogContainer = mLayout.getDialogContainerView();
MarginLayoutParams marginParams = (MarginLayoutParams) dialogContainer.getLayoutParams();
final Rect taskBounds = getTaskBounds();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 701a3a4..d3b9fa5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -93,7 +93,7 @@
import com.android.wm.shell.unfold.animation.UnfoldTaskAnimator;
import com.android.wm.shell.unfold.qualifier.UnfoldShellTransition;
import com.android.wm.shell.unfold.qualifier.UnfoldTransition;
-import com.android.wm.shell.windowdecor.CaptionWindowDecorViewModel;
+import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel;
import com.android.wm.shell.windowdecor.WindowDecorViewModel;
import java.util.ArrayList;
@@ -192,7 +192,7 @@
SyncTransactionQueue syncQueue,
Optional<DesktopModeController> desktopModeController,
Optional<DesktopTasksController> desktopTasksController) {
- return new CaptionWindowDecorViewModel(
+ return new DesktopModeWindowDecorViewModel(
context,
mainHandler,
mainChoreographer,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java
index 6b59e31..d7cb490 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java
@@ -16,8 +16,6 @@
package com.android.wm.shell.unfold;
-import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
-
import android.annotation.NonNull;
import android.app.ActivityManager.RunningTaskInfo;
import android.app.TaskInfo;
@@ -56,6 +54,12 @@
private final SparseArray<SurfaceControl> mTaskSurfaces = new SparseArray<>();
private final SparseArray<UnfoldTaskAnimator> mAnimatorsByTaskId = new SparseArray<>();
+ /**
+ * Indicates whether we're in stage change process. This should be set to {@code true} in
+ * {@link #onStateChangeStarted()} and {@code false} in {@link #onStateChangeFinished()}.
+ */
+ private boolean mIsInStageChange;
+
public UnfoldAnimationController(
@NonNull ShellInit shellInit,
@NonNull TransactionPool transactionPool,
@@ -123,7 +127,7 @@
animator.onTaskChanged(taskInfo);
} else {
// Became inapplicable
- resetTask(animator, taskInfo);
+ maybeResetTask(animator, taskInfo);
animator.onTaskVanished(taskInfo);
mAnimatorsByTaskId.remove(taskInfo.taskId);
}
@@ -154,7 +158,7 @@
final boolean isCurrentlyApplicable = animator != null;
if (isCurrentlyApplicable) {
- resetTask(animator, taskInfo);
+ maybeResetTask(animator, taskInfo);
animator.onTaskVanished(taskInfo);
mAnimatorsByTaskId.remove(taskInfo.taskId);
}
@@ -166,6 +170,7 @@
return;
}
+ mIsInStageChange = true;
SurfaceControl.Transaction transaction = null;
for (int i = 0; i < mAnimators.size(); i++) {
final UnfoldTaskAnimator animator = mAnimators.get(i);
@@ -219,11 +224,12 @@
transaction.apply();
mTransactionPool.release(transaction);
+ mIsInStageChange = false;
}
- private void resetTask(UnfoldTaskAnimator animator, TaskInfo taskInfo) {
- if (taskInfo.getWindowingMode() == WINDOWING_MODE_PINNED) {
- // PiP task has its own cleanup path, ignore surface reset to avoid conflict.
+ private void maybeResetTask(UnfoldTaskAnimator animator, TaskInfo taskInfo) {
+ if (!mIsInStageChange) {
+ // No need to resetTask if there is no ongoing state change.
return;
}
final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
similarity index 92%
rename from libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
rename to libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index 299284f..b500f5f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -61,12 +61,12 @@
/**
* View model for the window decoration with a caption and shadows. Works with
- * {@link CaptionWindowDecoration}.
+ * {@link DesktopModeWindowDecoration}.
*/
-public class CaptionWindowDecorViewModel implements WindowDecorViewModel {
- private static final String TAG = "CaptionViewModel";
- private final CaptionWindowDecoration.Factory mCaptionWindowDecorFactory;
+public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
+ private static final String TAG = "DesktopModeWindowDecorViewModel";
+ private final DesktopModeWindowDecoration.Factory mDesktopModeWindowDecorFactory;
private final ActivityTaskManager mActivityTaskManager;
private final ShellTaskOrganizer mTaskOrganizer;
private final Context mContext;
@@ -81,11 +81,12 @@
private SparseArray<EventReceiver> mEventReceiversByDisplay = new SparseArray<>();
- private final SparseArray<CaptionWindowDecoration> mWindowDecorByTaskId = new SparseArray<>();
+ private final SparseArray<DesktopModeWindowDecoration> mWindowDecorByTaskId =
+ new SparseArray<>();
private final DragStartListenerImpl mDragStartListener = new DragStartListenerImpl();
private InputMonitorFactory mInputMonitorFactory;
- public CaptionWindowDecorViewModel(
+ public DesktopModeWindowDecorViewModel(
Context context,
Handler mainHandler,
Choreographer mainChoreographer,
@@ -103,12 +104,12 @@
syncQueue,
desktopModeController,
desktopTasksController,
- new CaptionWindowDecoration.Factory(),
+ new DesktopModeWindowDecoration.Factory(),
new InputMonitorFactory());
}
@VisibleForTesting
- CaptionWindowDecorViewModel(
+ DesktopModeWindowDecorViewModel(
Context context,
Handler mainHandler,
Choreographer mainChoreographer,
@@ -117,7 +118,7 @@
SyncTransactionQueue syncQueue,
Optional<DesktopModeController> desktopModeController,
Optional<DesktopTasksController> desktopTasksController,
- CaptionWindowDecoration.Factory captionWindowDecorFactory,
+ DesktopModeWindowDecoration.Factory desktopModeWindowDecorFactory,
InputMonitorFactory inputMonitorFactory) {
mContext = context;
mMainHandler = mainHandler;
@@ -129,7 +130,7 @@
mDesktopModeController = desktopModeController;
mDesktopTasksController = desktopTasksController;
- mCaptionWindowDecorFactory = captionWindowDecorFactory;
+ mDesktopModeWindowDecorFactory = desktopModeWindowDecorFactory;
mInputMonitorFactory = inputMonitorFactory;
}
@@ -151,7 +152,7 @@
@Override
public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
- final CaptionWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId);
+ final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId);
if (decoration == null) return;
@@ -170,7 +171,7 @@
SurfaceControl taskSurface,
SurfaceControl.Transaction startT,
SurfaceControl.Transaction finishT) {
- final CaptionWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId);
+ final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId);
if (!shouldShowWindowDecor(taskInfo)) {
if (decoration != null) {
@@ -191,7 +192,7 @@
RunningTaskInfo taskInfo,
SurfaceControl.Transaction startT,
SurfaceControl.Transaction finishT) {
- final CaptionWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId);
+ final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId);
if (decoration == null) return;
decoration.relayout(taskInfo, startT, finishT);
@@ -199,7 +200,7 @@
@Override
public void destroyWindowDecoration(RunningTaskInfo taskInfo) {
- final CaptionWindowDecoration decoration =
+ final DesktopModeWindowDecoration decoration =
mWindowDecorByTaskId.removeReturnOld(taskInfo.taskId);
if (decoration == null) return;
@@ -232,7 +233,7 @@
@Override
public void onClick(View v) {
- CaptionWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
+ DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
final int id = v.getId();
if (id == R.id.close_window) {
WindowContainerTransaction wct = new WindowContainerTransaction();
@@ -373,7 +374,7 @@
boolean handled = false;
if (event instanceof MotionEvent) {
handled = true;
- CaptionWindowDecorViewModel.this
+ DesktopModeWindowDecorViewModel.this
.handleReceivedMotionEvent((MotionEvent) event, mInputMonitor);
}
finishInputEvent(event, handled);
@@ -433,7 +434,7 @@
*/
private void handleReceivedMotionEvent(MotionEvent ev, InputMonitor inputMonitor) {
if (DesktopModeStatus.isProto2Enabled()) {
- CaptionWindowDecoration focusedDecor = getFocusedDecor();
+ DesktopModeWindowDecoration focusedDecor = getFocusedDecor();
if (focusedDecor == null
|| focusedDecor.mTaskInfo.getWindowingMode() != WINDOWING_MODE_FREEFORM) {
handleCaptionThroughStatusBar(ev);
@@ -460,7 +461,7 @@
private void handleEventOutsideFocusedCaption(MotionEvent ev) {
int action = ev.getActionMasked();
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
- CaptionWindowDecoration focusedDecor = getFocusedDecor();
+ DesktopModeWindowDecoration focusedDecor = getFocusedDecor();
if (focusedDecor == null) {
return;
}
@@ -480,7 +481,7 @@
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN: {
// Begin drag through status bar if applicable.
- CaptionWindowDecoration focusedDecor = getFocusedDecor();
+ DesktopModeWindowDecoration focusedDecor = getFocusedDecor();
if (focusedDecor != null) {
boolean dragFromStatusBarAllowed = false;
if (DesktopModeStatus.isProto2Enabled()) {
@@ -499,7 +500,7 @@
break;
}
case MotionEvent.ACTION_UP: {
- CaptionWindowDecoration focusedDecor = getFocusedDecor();
+ DesktopModeWindowDecoration focusedDecor = getFocusedDecor();
if (focusedDecor == null) {
mTransitionDragActive = false;
return;
@@ -529,11 +530,11 @@
}
@Nullable
- private CaptionWindowDecoration getFocusedDecor() {
+ private DesktopModeWindowDecoration getFocusedDecor() {
int size = mWindowDecorByTaskId.size();
- CaptionWindowDecoration focusedDecor = null;
+ DesktopModeWindowDecoration focusedDecor = null;
for (int i = 0; i < size; i++) {
- CaptionWindowDecoration decor = mWindowDecorByTaskId.valueAt(i);
+ DesktopModeWindowDecoration decor = mWindowDecorByTaskId.valueAt(i);
if (decor != null && decor.isFocused()) {
focusedDecor = decor;
break;
@@ -571,13 +572,13 @@
SurfaceControl taskSurface,
SurfaceControl.Transaction startT,
SurfaceControl.Transaction finishT) {
- CaptionWindowDecoration oldDecoration = mWindowDecorByTaskId.get(taskInfo.taskId);
+ DesktopModeWindowDecoration oldDecoration = mWindowDecorByTaskId.get(taskInfo.taskId);
if (oldDecoration != null) {
// close the old decoration if it exists to avoid two window decorations being added
oldDecoration.close();
}
- final CaptionWindowDecoration windowDecoration =
- mCaptionWindowDecorFactory.create(
+ final DesktopModeWindowDecoration windowDecoration =
+ mDesktopModeWindowDecorFactory.create(
mContext,
mDisplayController,
mTaskOrganizer,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
similarity index 97%
rename from libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
rename to libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index f7c7a87..467f374 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -43,12 +43,12 @@
/**
* Defines visuals and behaviors of a window decoration of a caption bar and shadows. It works with
- * {@link CaptionWindowDecorViewModel}. The caption bar contains a handle, back button, and close
- * button.
+ * {@link DesktopModeWindowDecorViewModel}. The caption bar contains a handle, back button, and
+ * close button.
*
* The shadow's thickness is 20dp when the window is in focus and 5dp when the window isn't.
*/
-public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearLayout> {
+public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLinearLayout> {
private final Handler mHandler;
private final Choreographer mChoreographer;
private final SyncTransactionQueue mSyncQueue;
@@ -69,7 +69,7 @@
private AdditionalWindow mHandleMenu;
- CaptionWindowDecoration(
+ DesktopModeWindowDecoration(
Context context,
DisplayController displayController,
ShellTaskOrganizer taskOrganizer,
@@ -424,7 +424,7 @@
static class Factory {
- CaptionWindowDecoration create(
+ DesktopModeWindowDecoration create(
Context context,
DisplayController displayController,
ShellTaskOrganizer taskOrganizer,
@@ -433,7 +433,7 @@
Handler handler,
Choreographer choreographer,
SyncTransactionQueue syncQueue) {
- return new CaptionWindowDecoration(
+ return new DesktopModeWindowDecoration(
context,
displayController,
taskOrganizer,
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt
index 0160f18..73671db 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt
@@ -85,7 +85,18 @@
@Test
fun splitScreenDividerKeepVisible() = flicker.layerKeepVisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
- @Presubmit @Test fun primaryAppLayerKeepVisible() = flicker.layerKeepVisible(primaryApp)
+ @Presubmit
+ @Test
+ fun primaryAppLayerKeepVisible() {
+ Assume.assumeFalse(isShellTransitionsEnabled)
+ flicker.layerKeepVisible(primaryApp)
+ }
+
+ @FlakyTest(bugId = 263213649)
+ @Test fun primaryAppLayerKeepVisible_ShellTransit() {
+ Assume.assumeTrue(isShellTransitionsEnabled)
+ flicker.layerKeepVisible(primaryApp)
+ }
@Presubmit
@Test
@@ -99,17 +110,7 @@
}
}
- @Presubmit
- @Test fun primaryAppWindowKeepVisible() {
- Assume.assumeFalse(isShellTransitionsEnabled)
- flicker.appWindowKeepVisible(primaryApp)
- }
-
- @FlakyTest(bugId = 263213649)
- @Test fun primaryAppWindowKeepVisible_ShellTransit() {
- Assume.assumeTrue(isShellTransitionsEnabled)
- flicker.appWindowKeepVisible(primaryApp)
- }
+ @Presubmit @Test fun primaryAppWindowKeepVisible() = flicker.appWindowKeepVisible(primaryApp)
@Presubmit
@Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayoutTest.java
index 1dee88c..a58620d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayoutTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayoutTest.java
@@ -68,11 +68,11 @@
@Test
public void testOnFinishInflate() {
- assertEquals(mLayout.getDialogContainer(),
+ assertEquals(mLayout.getDialogContainerView(),
mLayout.findViewById(R.id.letterbox_education_dialog_container));
assertEquals(mLayout.getDialogTitle(),
mLayout.findViewById(R.id.letterbox_education_dialog_title));
- assertEquals(mLayout.getBackgroundDim(), mLayout.getBackground());
+ assertEquals(mLayout.getBackgroundDimDrawable(), mLayout.getBackground());
assertEquals(mLayout.getBackground().getAlpha(), 0);
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java
index 16517c0..14190f1 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java
@@ -56,6 +56,7 @@
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.DockStateReader;
import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.compatui.DialogAnimationController;
import com.android.wm.shell.transition.Transitions;
import org.junit.After;
@@ -98,7 +99,7 @@
@Captor
private ArgumentCaptor<Runnable> mRunOnIdleCaptor;
- @Mock private LetterboxEduAnimationController mAnimationController;
+ @Mock private DialogAnimationController<LetterboxEduDialogLayout> mAnimationController;
@Mock private SyncTransactionQueue mSyncTransactionQueue;
@Mock private ShellTaskOrganizer.TaskListener mTaskListener;
@Mock private SurfaceControlViewHost mViewHost;
@@ -366,7 +367,7 @@
assertThat(params.width).isEqualTo(expectedWidth);
assertThat(params.height).isEqualTo(expectedHeight);
MarginLayoutParams dialogParams =
- (MarginLayoutParams) layout.getDialogContainer().getLayoutParams();
+ (MarginLayoutParams) layout.getDialogContainerView().getLayoutParams();
int verticalMargin = (int) mContext.getResources().getDimension(
R.dimen.letterbox_education_dialog_margin);
assertThat(dialogParams.topMargin).isEqualTo(verticalMargin + expectedExtraTopMargin);
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/DesktopModeWindowDecorViewModelTests.java
similarity index 81%
rename from libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModelTests.java
rename to libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.java
index 4875832..a5e3a2e 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/DesktopModeWindowDecorViewModelTests.java
@@ -60,14 +60,14 @@
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
-/** Tests of {@link CaptionWindowDecorViewModel} */
+/** Tests of {@link DesktopModeWindowDecorViewModel} */
@SmallTest
-public class CaptionWindowDecorViewModelTests extends ShellTestCase {
+public class DesktopModeWindowDecorViewModelTests extends ShellTestCase {
- private static final String TAG = "CaptionWindowDecorViewModelTests";
+ private static final String TAG = "DesktopModeWindowDecorViewModelTests";
- @Mock private CaptionWindowDecoration mCaptionWindowDecoration;
- @Mock private CaptionWindowDecoration.Factory mCaptionWindowDecorFactory;
+ @Mock private DesktopModeWindowDecoration mDesktopModeWindowDecoration;
+ @Mock private DesktopModeWindowDecoration.Factory mDesktopModeWindowDecorFactory;
@Mock private Handler mMainHandler;
@Mock private Choreographer mMainChoreographer;
@@ -79,17 +79,17 @@
@Mock private InputMonitor mInputMonitor;
@Mock private InputManager mInputManager;
- @Mock private CaptionWindowDecorViewModel.InputMonitorFactory mMockInputMonitorFactory;
+ @Mock private DesktopModeWindowDecorViewModel.InputMonitorFactory mMockInputMonitorFactory;
private final List<InputManager> mMockInputManagers = new ArrayList<>();
- private CaptionWindowDecorViewModel mCaptionWindowDecorViewModel;
+ private DesktopModeWindowDecorViewModel mDesktopModeWindowDecorViewModel;
@Before
public void setUp() {
mMockInputManagers.add(mInputManager);
- mCaptionWindowDecorViewModel =
- new CaptionWindowDecorViewModel(
+ mDesktopModeWindowDecorViewModel =
+ new DesktopModeWindowDecorViewModel(
mContext,
mMainHandler,
mMainChoreographer,
@@ -98,12 +98,12 @@
mSyncQueue,
Optional.of(mDesktopModeController),
Optional.of(mDesktopTasksController),
- mCaptionWindowDecorFactory,
+ mDesktopModeWindowDecorFactory,
mMockInputMonitorFactory
);
- doReturn(mCaptionWindowDecoration)
- .when(mCaptionWindowDecorFactory)
+ doReturn(mDesktopModeWindowDecoration)
+ .when(mDesktopModeWindowDecorFactory)
.create(any(), any(), any(), any(), any(), any(), any(), any());
when(mMockInputMonitorFactory.create(any(), any())).thenReturn(mInputMonitor);
@@ -123,13 +123,15 @@
final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class);
final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class);
- mCaptionWindowDecorViewModel.onTaskOpening(taskInfo, surfaceControl, startT, finishT);
+ mDesktopModeWindowDecorViewModel.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);
+ mDesktopModeWindowDecorViewModel.onTaskChanging(
+ taskInfo, surfaceControl, startT, finishT);
});
- verify(mCaptionWindowDecorFactory)
+ verify(mDesktopModeWindowDecorFactory)
.create(
mContext,
mDisplayController,
@@ -139,7 +141,7 @@
mMainHandler,
mMainChoreographer,
mSyncQueue);
- verify(mCaptionWindowDecoration).close();
+ verify(mDesktopModeWindowDecoration).close();
}
@Test
@@ -153,14 +155,16 @@
final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class);
taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_UNDEFINED);
- mCaptionWindowDecorViewModel.onTaskChanging(taskInfo, surfaceControl, startT, finishT);
+ mDesktopModeWindowDecorViewModel.onTaskChanging(
+ taskInfo, surfaceControl, startT, finishT);
taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_STANDARD);
- mCaptionWindowDecorViewModel.onTaskChanging(taskInfo, surfaceControl, startT, finishT);
+ mDesktopModeWindowDecorViewModel.onTaskChanging(
+ taskInfo, surfaceControl, startT, finishT);
});
- verify(mCaptionWindowDecorFactory, times(1))
+ verify(mDesktopModeWindowDecorFactory, times(1))
.create(
mContext,
mDisplayController,
@@ -183,9 +187,10 @@
final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class);
final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class);
- mCaptionWindowDecorViewModel.onTaskOpening(taskInfo, surfaceControl, startT, finishT);
+ mDesktopModeWindowDecorViewModel.onTaskOpening(
+ taskInfo, surfaceControl, startT, finishT);
- mCaptionWindowDecorViewModel.destroyWindowDecoration(taskInfo);
+ mDesktopModeWindowDecorViewModel.destroyWindowDecoration(taskInfo);
});
verify(mMockInputMonitorFactory).create(any(), any());
verify(mInputMonitor).dispose();
@@ -214,13 +219,14 @@
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,
+ mDesktopModeWindowDecorViewModel.onTaskOpening(taskInfo, surfaceControl, startT,
+ finishT);
+ mDesktopModeWindowDecorViewModel.onTaskOpening(secondTaskInfo, surfaceControl,
startT, finishT);
- mCaptionWindowDecorViewModel.onTaskOpening(thirdTaskInfo, surfaceControl,
+ mDesktopModeWindowDecorViewModel.onTaskOpening(thirdTaskInfo, surfaceControl,
startT, finishT);
- mCaptionWindowDecorViewModel.destroyWindowDecoration(thirdTaskInfo);
- mCaptionWindowDecorViewModel.destroyWindowDecoration(taskInfo);
+ mDesktopModeWindowDecorViewModel.destroyWindowDecoration(thirdTaskInfo);
+ mDesktopModeWindowDecorViewModel.destroyWindowDecoration(taskInfo);
});
verify(mMockInputMonitorFactory, times(2)).create(any(), any());
verify(mInputMonitor, times(1)).dispose();
diff --git a/libs/hwui/AndroidTest.xml b/libs/hwui/AndroidTest.xml
index 911315f..75f61f5 100644
--- a/libs/hwui/AndroidTest.xml
+++ b/libs/hwui/AndroidTest.xml
@@ -21,6 +21,7 @@
<option name="push" value="hwuimacro->/data/local/tmp/benchmarktest/hwuimacro" />
</target_preparer>
<option name="test-suite-tag" value="apct" />
+ <option name="not-shardable" value="true" />
<test class="com.android.tradefed.testtype.GTest" >
<option name="native-test-device-path" value="/data/local/tmp/nativetest" />
<option name="module-name" value="hwui_unit_tests" />
diff --git a/libs/hwui/jni/Graphics.cpp b/libs/hwui/jni/Graphics.cpp
index 6a3bc8f..c835849 100644
--- a/libs/hwui/jni/Graphics.cpp
+++ b/libs/hwui/jni/Graphics.cpp
@@ -576,14 +576,22 @@
LOG_ALWAYS_FATAL_IF(!decodeColorSpace->toXYZD50(&xyzMatrix));
skcms_TransferFunction transferParams;
- // We can only handle numerical transfer functions at the moment
- LOG_ALWAYS_FATAL_IF(!decodeColorSpace->isNumericalTransferFn(&transferParams));
+ decodeColorSpace->transferFn(&transferParams);
+ auto res = skcms_TransferFunction_getType(&transferParams);
+ LOG_ALWAYS_FATAL_IF(res == skcms_TFType_HLGinvish || res == skcms_TFType_Invalid);
- jobject params = env->NewObject(gTransferParameters_class,
- gTransferParameters_constructorMethodID,
- transferParams.a, transferParams.b, transferParams.c,
- transferParams.d, transferParams.e, transferParams.f,
- transferParams.g);
+ jobject params;
+ if (res == skcms_TFType_PQish || res == skcms_TFType_HLGish) {
+ params = env->NewObject(gTransferParameters_class, gTransferParameters_constructorMethodID,
+ transferParams.a, transferParams.b, transferParams.c,
+ transferParams.d, transferParams.e, transferParams.f,
+ transferParams.g, true);
+ } else {
+ params = env->NewObject(gTransferParameters_class, gTransferParameters_constructorMethodID,
+ transferParams.a, transferParams.b, transferParams.c,
+ transferParams.d, transferParams.e, transferParams.f,
+ transferParams.g, false);
+ }
jfloatArray xyzArray = env->NewFloatArray(9);
jfloat xyz[9] = {
@@ -808,8 +816,8 @@
gTransferParameters_class = MakeGlobalRefOrDie(env, FindClassOrDie(env,
"android/graphics/ColorSpace$Rgb$TransferParameters"));
- gTransferParameters_constructorMethodID = GetMethodIDOrDie(env, gTransferParameters_class,
- "<init>", "(DDDDDDD)V");
+ gTransferParameters_constructorMethodID =
+ GetMethodIDOrDie(env, gTransferParameters_class, "<init>", "(DDDDDDDZ)V");
gFontMetrics_class = FindClassOrDie(env, "android/graphics/Paint$FontMetrics");
gFontMetrics_class = MakeGlobalRefOrDie(env, gFontMetrics_class);
diff --git a/libs/hwui/jni/Mesh.cpp b/libs/hwui/jni/Mesh.cpp
index 7b9a93f..3aac48d 100644
--- a/libs/hwui/jni/Mesh.cpp
+++ b/libs/hwui/jni/Mesh.cpp
@@ -44,10 +44,16 @@
sk_sp<SkMesh::VertexBuffer> skVertexBuffer =
genVertexBuffer(env, vertexBuffer, vertexCount * skMeshSpec->stride(), isDirect);
auto skRect = SkRect::MakeLTRB(left, top, right, bottom);
- auto mesh = SkMesh::Make(skMeshSpec, SkMesh::Mode(mode), skVertexBuffer, vertexCount,
- vertexOffset, nullptr, skRect)
- .mesh;
- auto meshPtr = std::make_unique<MeshWrapper>(MeshWrapper{mesh, MeshUniformBuilder(skMeshSpec)});
+ auto meshResult = SkMesh::Make(
+ skMeshSpec, SkMesh::Mode(mode), skVertexBuffer, vertexCount, vertexOffset,
+ SkData::MakeWithCopy(skMeshSpec->uniforms().data(), skMeshSpec->uniformSize()), skRect);
+
+ if (!meshResult.error.isEmpty()) {
+ jniThrowException(env, "java/lang/IllegalArgumentException", meshResult.error.c_str());
+ }
+
+ auto meshPtr = std::make_unique<MeshWrapper>(
+ MeshWrapper{meshResult.mesh, MeshUniformBuilder(skMeshSpec)});
return reinterpret_cast<jlong>(meshPtr.release());
}
@@ -61,11 +67,17 @@
sk_sp<SkMesh::IndexBuffer> skIndexBuffer =
genIndexBuffer(env, indexBuffer, indexCount * gIndexByteSize, isIndexDirect);
auto skRect = SkRect::MakeLTRB(left, top, right, bottom);
- auto mesh = SkMesh::MakeIndexed(skMeshSpec, SkMesh::Mode(mode), skVertexBuffer, vertexCount,
- vertexOffset, skIndexBuffer, indexCount, indexOffset, nullptr,
- skRect)
- .mesh;
- auto meshPtr = std::make_unique<MeshWrapper>(MeshWrapper{mesh, MeshUniformBuilder(skMeshSpec)});
+
+ auto meshResult = SkMesh::MakeIndexed(
+ skMeshSpec, SkMesh::Mode(mode), skVertexBuffer, vertexCount, vertexOffset,
+ skIndexBuffer, indexCount, indexOffset,
+ SkData::MakeWithCopy(skMeshSpec->uniforms().data(), skMeshSpec->uniformSize()), skRect);
+
+ if (!meshResult.error.isEmpty()) {
+ jniThrowException(env, "java/lang/IllegalArgumentException", meshResult.error.c_str());
+ }
+ auto meshPtr = std::make_unique<MeshWrapper>(
+ MeshWrapper{meshResult.mesh, MeshUniformBuilder(skMeshSpec)});
return reinterpret_cast<jlong>(meshPtr.release());
}
@@ -139,22 +151,22 @@
}
}
-static void updateFloatUniforms(JNIEnv* env, jobject, jlong uniBuilder, jstring uniformName,
+static void updateFloatUniforms(JNIEnv* env, jobject, jlong meshWrapper, jstring uniformName,
jfloat value1, jfloat value2, jfloat value3, jfloat value4,
jint count) {
- auto* builder = reinterpret_cast<MeshUniformBuilder*>(uniBuilder);
+ auto* wrapper = reinterpret_cast<MeshWrapper*>(meshWrapper);
ScopedUtfChars name(env, uniformName);
const float values[4] = {value1, value2, value3, value4};
- nativeUpdateFloatUniforms(env, builder, name.c_str(), values, count, false);
+ nativeUpdateFloatUniforms(env, &wrapper->builder, name.c_str(), values, count, false);
}
-static void updateFloatArrayUniforms(JNIEnv* env, jobject, jlong uniBuilder, jstring jUniformName,
+static void updateFloatArrayUniforms(JNIEnv* env, jobject, jlong meshWrapper, jstring jUniformName,
jfloatArray jvalues, jboolean isColor) {
- auto builder = reinterpret_cast<MeshUniformBuilder*>(uniBuilder);
+ auto wrapper = reinterpret_cast<MeshWrapper*>(meshWrapper);
ScopedUtfChars name(env, jUniformName);
AutoJavaFloatArray autoValues(env, jvalues, 0, kRO_JNIAccess);
- nativeUpdateFloatUniforms(env, builder, name.c_str(), autoValues.ptr(), autoValues.length(),
- isColor);
+ nativeUpdateFloatUniforms(env, &wrapper->builder, name.c_str(), autoValues.ptr(),
+ autoValues.length(), isColor);
}
static void nativeUpdateIntUniforms(JNIEnv* env, MeshUniformBuilder* builder,
@@ -171,20 +183,21 @@
}
}
-static void updateIntUniforms(JNIEnv* env, jobject, jlong uniBuilder, jstring uniformName,
+static void updateIntUniforms(JNIEnv* env, jobject, jlong meshWrapper, jstring uniformName,
jint value1, jint value2, jint value3, jint value4, jint count) {
- auto builder = reinterpret_cast<MeshUniformBuilder*>(uniBuilder);
+ auto wrapper = reinterpret_cast<MeshWrapper*>(meshWrapper);
ScopedUtfChars name(env, uniformName);
const int values[4] = {value1, value2, value3, value4};
- nativeUpdateIntUniforms(env, builder, name.c_str(), values, count);
+ nativeUpdateIntUniforms(env, &wrapper->builder, name.c_str(), values, count);
}
-static void updateIntArrayUniforms(JNIEnv* env, jobject, jlong uniBuilder, jstring uniformName,
+static void updateIntArrayUniforms(JNIEnv* env, jobject, jlong meshWrapper, jstring uniformName,
jintArray values) {
- auto builder = reinterpret_cast<MeshUniformBuilder*>(uniBuilder);
+ auto wrapper = reinterpret_cast<MeshWrapper*>(meshWrapper);
ScopedUtfChars name(env, uniformName);
AutoJavaIntArray autoValues(env, values, 0);
- nativeUpdateIntUniforms(env, builder, name.c_str(), autoValues.ptr(), autoValues.length());
+ nativeUpdateIntUniforms(env, &wrapper->builder, name.c_str(), autoValues.ptr(),
+ autoValues.length());
}
static void MeshWrapper_destroy(MeshWrapper* wrapper) {
diff --git a/libs/hwui/jni/Mesh.h b/libs/hwui/jni/Mesh.h
index aa014a5..7a73f2d 100644
--- a/libs/hwui/jni/Mesh.h
+++ b/libs/hwui/jni/Mesh.h
@@ -239,6 +239,7 @@
explicit MeshUniformBuilder(sk_sp<SkMeshSpecification> meshSpec) {
fMeshSpec = sk_sp(meshSpec);
+ fUniforms = (SkData::MakeZeroInitialized(meshSpec->uniformSize()));
}
sk_sp<SkData> fUniforms;
diff --git a/libs/hwui/jni/MeshSpecification.cpp b/libs/hwui/jni/MeshSpecification.cpp
index 619a3ed..ae9792d 100644
--- a/libs/hwui/jni/MeshSpecification.cpp
+++ b/libs/hwui/jni/MeshSpecification.cpp
@@ -78,7 +78,6 @@
auto meshSpecResult = SkMeshSpecification::Make(attributes, vertexStride, varyings,
SkString(skVertexShader.c_str()),
SkString(skFragmentShader.c_str()));
-
if (meshSpecResult.specification.get() == nullptr) {
jniThrowException(env, "java/lang/IllegalArgumentException", meshSpecResult.error.c_str());
}
diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp
index 64839d0..78ae5cf 100644
--- a/libs/hwui/renderthread/CanvasContext.cpp
+++ b/libs/hwui/renderthread/CanvasContext.cpp
@@ -475,6 +475,7 @@
void CanvasContext::notifyFramePending() {
ATRACE_CALL();
mRenderThread.pushBackFrameCallback(this);
+ sendLoadResetHint();
}
void CanvasContext::draw() {
diff --git a/location/java/android/location/GnssMeasurementsEvent.java b/location/java/android/location/GnssMeasurementsEvent.java
index a8b0dc2..8b96974 100644
--- a/location/java/android/location/GnssMeasurementsEvent.java
+++ b/location/java/android/location/GnssMeasurementsEvent.java
@@ -37,11 +37,14 @@
* Events are delivered to registered instances of {@link Callback}.
*/
public final class GnssMeasurementsEvent implements Parcelable {
+ private final int mFlag;
private final GnssClock mClock;
private final List<GnssMeasurement> mMeasurements;
private final List<GnssAutomaticGainControl> mGnssAgcs;
private final boolean mIsFullTracking;
+ private static final int HAS_FULL_TRACKING = 1;
+
/**
* Used for receiving GNSS satellite measurements from the GNSS engine.
* Each measurement contains raw and computed data identifying a satellite.
@@ -123,10 +126,12 @@
/**
* Create a {@link GnssMeasurementsEvent} instance with a full list of parameters.
*/
- private GnssMeasurementsEvent(@NonNull GnssClock clock,
+ private GnssMeasurementsEvent(int flag,
+ @NonNull GnssClock clock,
@NonNull List<GnssMeasurement> measurements,
@NonNull List<GnssAutomaticGainControl> agcs,
boolean isFullTracking) {
+ mFlag = flag;
mMeasurements = measurements;
mGnssAgcs = agcs;
mClock = clock;
@@ -168,22 +173,32 @@
*
* False indicates that the GNSS chipset may optimize power via duty cycling, constellations and
* frequency limits, etc.
+ *
+ * <p>The value is only available if {@link #hasFullTracking()} is {@code true}.
*/
- public boolean getIsFullTracking() {
+ public boolean isFullTracking() {
return mIsFullTracking;
}
+ /**
+ * Return {@code true} if {@link #isFullTracking()} is available, {@code false} otherwise.
+ */
+ public boolean hasFullTracking() {
+ return (mFlag & HAS_FULL_TRACKING) == HAS_FULL_TRACKING;
+ }
+
public static final @android.annotation.NonNull Creator<GnssMeasurementsEvent> CREATOR =
new Creator<GnssMeasurementsEvent>() {
@Override
public GnssMeasurementsEvent createFromParcel(Parcel in) {
+ int flag = in.readInt();
GnssClock clock = in.readParcelable(getClass().getClassLoader(),
android.location.GnssClock.class);
List<GnssMeasurement> measurements = in.createTypedArrayList(GnssMeasurement.CREATOR);
List<GnssAutomaticGainControl> agcs = in.createTypedArrayList(
GnssAutomaticGainControl.CREATOR);
boolean isFullTracking = in.readBoolean();
- return new GnssMeasurementsEvent(clock, measurements, agcs, isFullTracking);
+ return new GnssMeasurementsEvent(flag, clock, measurements, agcs, isFullTracking);
}
@Override
@@ -199,6 +214,7 @@
@Override
public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeInt(mFlag);
parcel.writeParcelable(mClock, flags);
parcel.writeTypedList(mMeasurements);
parcel.writeTypedList(mGnssAgcs);
@@ -211,13 +227,16 @@
builder.append(mClock);
builder.append(' ').append(mMeasurements.toString());
builder.append(' ').append(mGnssAgcs.toString());
- builder.append(" isFullTracking=").append(mIsFullTracking);
+ if (hasFullTracking()) {
+ builder.append(" isFullTracking=").append(mIsFullTracking);
+ }
builder.append("]");
return builder.toString();
}
/** Builder for {@link GnssMeasurementsEvent} */
public static final class Builder {
+ private int mFlag;
private GnssClock mClock;
private List<GnssMeasurement> mMeasurements;
private List<GnssAutomaticGainControl> mGnssAgcs;
@@ -237,10 +256,11 @@
* {@link GnssMeasurementsEvent}.
*/
public Builder(@NonNull GnssMeasurementsEvent event) {
+ mFlag = event.mFlag;
mClock = event.getClock();
mMeasurements = (List<GnssMeasurement>) event.getMeasurements();
mGnssAgcs = (List<GnssAutomaticGainControl>) event.getGnssAutomaticGainControls();
- mIsFullTracking = event.getIsFullTracking();
+ mIsFullTracking = event.isFullTracking();
}
/**
@@ -313,15 +333,26 @@
* and frequency limits, etc.
*/
@NonNull
- public Builder setIsFullTracking(boolean isFullTracking) {
+ public Builder setFullTracking(boolean isFullTracking) {
+ mFlag |= HAS_FULL_TRACKING;
mIsFullTracking = isFullTracking;
return this;
}
+ /**
+ * Clears the full tracking mode indicator.
+ */
+ @NonNull
+ public Builder clearFullTracking() {
+ mFlag &= ~HAS_FULL_TRACKING;
+ return this;
+ }
+
/** Builds a {@link GnssMeasurementsEvent} instance as specified by this builder. */
@NonNull
public GnssMeasurementsEvent build() {
- return new GnssMeasurementsEvent(mClock, mMeasurements, mGnssAgcs, mIsFullTracking);
+ return new GnssMeasurementsEvent(mFlag, mClock, mMeasurements, mGnssAgcs,
+ mIsFullTracking);
}
}
}
diff --git a/media/java/android/media/AudioDescriptor.java b/media/java/android/media/AudioDescriptor.java
index df648be..85a653c 100644
--- a/media/java/android/media/AudioDescriptor.java
+++ b/media/java/android/media/AudioDescriptor.java
@@ -41,11 +41,21 @@
* The Extended Display Identification Data (EDID) standard for a short audio descriptor.
*/
public static final int STANDARD_EDID = 1;
+ /**
+ * The standard for a Speaker Allocation Data Block (SADB).
+ */
+ public static final int STANDARD_SADB = 2;
+ /**
+ * The standard for a Vendor-Specific Audio Data Block (VSADB).
+ */
+ public static final int STANDARD_VSADB = 3;
/** @hide */
@IntDef({
STANDARD_NONE,
STANDARD_EDID,
+ STANDARD_SADB,
+ STANDARD_VSADB,
})
@Retention(RetentionPolicy.SOURCE)
public @interface AudioDescriptorStandard {}
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index b0609ec..19610a93 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -16,6 +16,10 @@
package android.media;
+import static android.companion.virtual.VirtualDeviceManager.DEVICE_ID_DEFAULT;
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO;
+
import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.IntRange;
@@ -34,6 +38,7 @@
import android.bluetooth.BluetoothCodecConfig;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeAudioCodecConfig;
+import android.companion.virtual.VirtualDeviceManager;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
import android.compat.annotation.UnsupportedAppUsage;
@@ -100,6 +105,7 @@
private Context mOriginalContext;
private Context mApplicationContext;
+ private @Nullable VirtualDeviceManager mVirtualDeviceManager; // Lazy initialized.
private long mVolumeKeyUpTime;
private static final String TAG = "AudioManager";
private static final boolean DEBUG = false;
@@ -858,6 +864,14 @@
return sService;
}
+ private VirtualDeviceManager getVirtualDeviceManager() {
+ if (mVirtualDeviceManager != null) {
+ return mVirtualDeviceManager;
+ }
+ mVirtualDeviceManager = getContext().getSystemService(VirtualDeviceManager.class);
+ return mVirtualDeviceManager;
+ }
+
/**
* Sends a simulated key event for a media button.
* To simulate a key press, you must first send a KeyEvent built with a
@@ -1827,8 +1841,7 @@
* {@link #setPreferredDeviceForStrategy(AudioProductStrategy, AudioDeviceAttributes)}
* {@link #setPreferredDevicesForStrategy(AudioProductStrategy, List<AudioDeviceAttributes>)}
* @param strategy the strategy to query
- * @return the preferred device for that strategy, or null if none was ever set or if the
- * strategy is invalid
+ * @return list of the preferred devices for that strategy
*/
@SystemApi
@RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
@@ -1845,6 +1858,76 @@
/**
* @hide
+ * Set a device as non-default for a given strategy, i.e. the audio routing to be avoided by
+ * this audio strategy.
+ * <p>Use
+ * {@link #removeDeviceAsNonDefaultForStrategy(AudioProductStrategy, AudioDeviceAttributes)}
+ * to cancel setting this preference for this strategy.</p>
+ * @param strategy the audio strategy whose routing will be affected
+ * @param device the audio device to not route to when available
+ * @return true if the operation was successful, false otherwise
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+ public boolean setDeviceAsNonDefaultForStrategy(@NonNull AudioProductStrategy strategy,
+ @NonNull AudioDeviceAttributes device) {
+ Objects.requireNonNull(strategy);
+ Objects.requireNonNull(device);
+ try {
+ final int status =
+ getService().setDeviceAsNonDefaultForStrategy(strategy.getId(), device);
+ return status == AudioSystem.SUCCESS;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ * Removes the audio device(s) from the non-default device list previously set with
+ * {@link #setDeviceAsNonDefaultForStrategy(AudioProductStrategy, AudioDeviceAttributes)}
+ * @param strategy the audio strategy whose routing will be affected
+ * @param device the audio device to remove from the non-default device list
+ * @return true if the operation was successful, false otherwise (invalid strategy, or no
+ * device set for example)
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+ public boolean removeDeviceAsNonDefaultForStrategy(@NonNull AudioProductStrategy strategy,
+ @NonNull AudioDeviceAttributes device) {
+ Objects.requireNonNull(strategy);
+ Objects.requireNonNull(device);
+ try {
+ final int status =
+ getService().removeDeviceAsNonDefaultForStrategy(strategy.getId(), device);
+ return status == AudioSystem.SUCCESS;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ * Gets the audio device(s) from the non-default device list previously set with
+ * {@link #setDeviceAsNonDefaultForStrategy(AudioProductStrategy, AudioDeviceAttributes)}
+ * @param strategy the audio strategy to query
+ * @return list of non-default devices for the strategy
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+ @NonNull
+ public List<AudioDeviceAttributes> getNonDefaultDevicesForStrategy(
+ @NonNull AudioProductStrategy strategy) {
+ Objects.requireNonNull(strategy);
+ try {
+ return getService().getNonDefaultDevicesForStrategy(strategy.getId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
* Interface to be notified of changes in the preferred audio device set for a given audio
* strategy.
* <p>Note that this listener will only be invoked whenever
@@ -1878,9 +1961,11 @@
* Interface to be notified of changes in the preferred audio devices set for a given audio
* strategy.
* <p>Note that this listener will only be invoked whenever
- * {@link #setPreferredDeviceForStrategy(AudioProductStrategy, AudioDeviceAttributes)} or
- * {@link #setPreferredDevicesForStrategy(AudioProductStrategy, List<AudioDeviceAttributes>)}
- * {@link #removePreferredDeviceForStrategy(AudioProductStrategy)} causes a change in
+ * {@link #setPreferredDeviceForStrategy(AudioProductStrategy, AudioDeviceAttributes)},
+ * {@link #setPreferredDevicesForStrategy(AudioProductStrategy, List<AudioDeviceAttributes>)},
+ * {@link #setDeviceAsNonDefaultForStrategy(AudioProductStrategy, AudioDeviceAttributes)},
+ * {@link #removeDeviceAsNonDefaultForStrategy(AudioProductStrategy, AudioDeviceAttributes)}
+ * or {@link #removePreferredDeviceForStrategy(AudioProductStrategy)} causes a change in
* preferred device(s). It will not be invoked directly after registration with
* {@link #addOnPreferredDevicesForStrategyChangedListener(
* Executor, OnPreferredDevicesForStrategyChangedListener)}
@@ -1888,7 +1973,6 @@
* @see #setPreferredDeviceForStrategy(AudioProductStrategy, AudioDeviceAttributes)
* @see #setPreferredDevicesForStrategy(AudioProductStrategy, List)
* @see #removePreferredDeviceForStrategy(AudioProductStrategy)
- * @see #getPreferredDeviceForStrategy(AudioProductStrategy)
* @see #getPreferredDevicesForStrategy(AudioProductStrategy)
*/
@SystemApi
@@ -1952,30 +2036,9 @@
throws SecurityException {
Objects.requireNonNull(executor);
Objects.requireNonNull(listener);
- synchronized (mPrefDevListenerLock) {
- if (hasPrefDevListener(listener)) {
- throw new IllegalArgumentException(
- "attempt to call addOnPreferredDevicesForStrategyChangedListener() "
- + "on a previously registered listener");
- }
- // lazy initialization of the list of strategy-preferred device listener
- if (mPrefDevListeners == null) {
- mPrefDevListeners = new ArrayList<>();
- }
- final int oldCbCount = mPrefDevListeners.size();
- mPrefDevListeners.add(new PrefDevListenerInfo(listener, executor));
- if (oldCbCount == 0 && mPrefDevListeners.size() > 0) {
- // register binder for callbacks
- if (mPrefDevDispatcherStub == null) {
- mPrefDevDispatcherStub = new StrategyPreferredDevicesDispatcherStub();
- }
- try {
- getService().registerStrategyPreferredDevicesDispatcher(mPrefDevDispatcherStub);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
- }
- }
+ mPrefDevListenerMgr.addListener(
+ executor, listener, "addOnPreferredDevicesForStrategyChangedListener",
+ () -> new StrategyPreferredDevicesDispatcherStub());
}
/**
@@ -1988,106 +2051,145 @@
public void removeOnPreferredDevicesForStrategyChangedListener(
@NonNull OnPreferredDevicesForStrategyChangedListener listener) {
Objects.requireNonNull(listener);
- synchronized (mPrefDevListenerLock) {
- if (!removePrefDevListener(listener)) {
- throw new IllegalArgumentException(
- "attempt to call removeOnPreferredDeviceForStrategyChangedListener() "
- + "on an unregistered listener");
- }
- if (mPrefDevListeners.size() == 0) {
- // unregister binder for callbacks
- try {
- getService().unregisterStrategyPreferredDevicesDispatcher(
- mPrefDevDispatcherStub);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- } finally {
- mPrefDevDispatcherStub = null;
- mPrefDevListeners = null;
- }
- }
- }
+ mPrefDevListenerMgr.removeListener(
+ listener, "removeOnPreferredDevicesForStrategyChangedListener");
}
-
- private final Object mPrefDevListenerLock = new Object();
/**
- * List of listeners for preferred device for strategy and their associated Executor.
- * List is lazy-initialized on first registration
+ * @hide
+ * Interface to be notified of changes in the non-default audio devices set for a given audio
+ * strategy.
+ * <p>Note that this listener will only be invoked whenever
+ * {@link #setPreferredDeviceForStrategy(AudioProductStrategy, AudioDeviceAttributes)},
+ * {@link #setPreferredDevicesForStrategy(AudioProductStrategy, List<AudioDeviceAttributes>)},
+ * {@link #setDeviceAsNonDefaultForStrategy(AudioProductStrategy, AudioDeviceAttributes)},
+ * {@link #removeDeviceAsNonDefaultForStrategy(AudioProductStrategy, AudioDeviceAttributes)}
+ * or {@link #removePreferredDeviceForStrategy(AudioProductStrategy)} causes a change in
+ * non-default device(s). It will not be invoked directly after registration with
+ * {@link #addOnNonDefaultDevicesForStrategyChangedListener(
+ * Executor, OnNonDefaultDevicesForStrategyChangedListener)}
+ * to indicate which strategies had preferred devices at the time of registration.</p>
+ * @see #setDeviceAsNonDefaultForStrategy(AudioProductStrategy, AudioDeviceAttributes)
+ * @see #removeDeviceAsNonDefaultForStrategy(AudioProductStrategy, AudioDeviceAttributes)
*/
- @GuardedBy("mPrefDevListenerLock")
- private @Nullable ArrayList<PrefDevListenerInfo> mPrefDevListeners;
-
- private static class PrefDevListenerInfo {
- final @NonNull OnPreferredDevicesForStrategyChangedListener mListener;
- final @NonNull Executor mExecutor;
- PrefDevListenerInfo(OnPreferredDevicesForStrategyChangedListener listener, Executor exe) {
- mListener = listener;
- mExecutor = exe;
- }
+ @SystemApi
+ public interface OnNonDefaultDevicesForStrategyChangedListener {
+ /**
+ * Called on the listener to indicate that the non-default audio devices for the given
+ * strategy has changed.
+ * @param strategy the {@link AudioProductStrategy} whose non-default device changed
+ * @param devices a list of newly set non-default audio devices
+ */
+ void onNonDefaultDevicesForStrategyChanged(@NonNull AudioProductStrategy strategy,
+ @NonNull List<AudioDeviceAttributes> devices);
}
- @GuardedBy("mPrefDevListenerLock")
- private StrategyPreferredDevicesDispatcherStub mPrefDevDispatcherStub;
+ /**
+ * @hide
+ * Adds a listener for being notified of changes to the non-default audio devices for
+ * strategies.
+ * @param executor
+ * @param listener
+ * @throws SecurityException if the caller doesn't hold the required permission
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+ public void addOnNonDefaultDevicesForStrategyChangedListener(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OnNonDefaultDevicesForStrategyChangedListener listener)
+ throws SecurityException {
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(listener);
+
+ mNonDefDevListenerMgr.addListener(
+ executor, listener, "addOnNonDefaultDevicesForStrategyChangedListener",
+ () -> new StrategyNonDefaultDevicesDispatcherStub());
+ }
+
+ /**
+ * @hide
+ * Removes a previously added listener of changes to the non-default audio device for
+ * strategies.
+ * @param listener
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+ public void removeOnNonDefaultDevicesForStrategyChangedListener(
+ @NonNull OnNonDefaultDevicesForStrategyChangedListener listener) {
+ Objects.requireNonNull(listener);
+ mNonDefDevListenerMgr.removeListener(
+ listener, "removeOnNonDefaultDevicesForStrategyChangedListener");
+ }
+
+ /**
+ * Manages the OnPreferredDevicesForStrategyChangedListener listeners and the
+ * StrategyPreferredDevicesDispatcherStub
+ */
+ private final CallbackUtil.LazyListenerManager<OnPreferredDevicesForStrategyChangedListener>
+ mPrefDevListenerMgr = new CallbackUtil.LazyListenerManager();
+
+ /**
+ * Manages the OnNonDefaultDevicesForStrategyChangedListener listeners and the
+ * StrategyNonDefaultDevicesDispatcherStub
+ */
+ private final CallbackUtil.LazyListenerManager<OnNonDefaultDevicesForStrategyChangedListener>
+ mNonDefDevListenerMgr = new CallbackUtil.LazyListenerManager();
private final class StrategyPreferredDevicesDispatcherStub
- extends IStrategyPreferredDevicesDispatcher.Stub {
+ extends IStrategyPreferredDevicesDispatcher.Stub
+ implements CallbackUtil.DispatcherStub {
@Override
public void dispatchPrefDevicesChanged(int strategyId,
@NonNull List<AudioDeviceAttributes> devices) {
- // make a shallow copy of listeners so callback is not executed under lock
- final ArrayList<PrefDevListenerInfo> prefDevListeners;
- synchronized (mPrefDevListenerLock) {
- if (mPrefDevListeners == null || mPrefDevListeners.size() == 0) {
- return;
- }
- prefDevListeners = (ArrayList<PrefDevListenerInfo>) mPrefDevListeners.clone();
- }
final AudioProductStrategy strategy =
AudioProductStrategy.getAudioProductStrategyWithId(strategyId);
- final long ident = Binder.clearCallingIdentity();
+
+ mPrefDevListenerMgr.callListeners(
+ (listener) -> listener.onPreferredDevicesForStrategyChanged(strategy, devices));
+ }
+
+ @Override
+ public void register(boolean register) {
try {
- for (PrefDevListenerInfo info : prefDevListeners) {
- info.mExecutor.execute(() ->
- info.mListener.onPreferredDevicesForStrategyChanged(strategy, devices));
+ if (register) {
+ getService().registerStrategyPreferredDevicesDispatcher(this);
+ } else {
+ getService().unregisterStrategyPreferredDevicesDispatcher(this);
}
- } finally {
- Binder.restoreCallingIdentity(ident);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
}
}
}
- @GuardedBy("mPrefDevListenerLock")
- private @Nullable PrefDevListenerInfo getPrefDevListenerInfo(
- OnPreferredDevicesForStrategyChangedListener listener) {
- if (mPrefDevListeners == null) {
- return null;
+ private final class StrategyNonDefaultDevicesDispatcherStub
+ extends IStrategyNonDefaultDevicesDispatcher.Stub
+ implements CallbackUtil.DispatcherStub {
+
+ @Override
+ public void dispatchNonDefDevicesChanged(int strategyId,
+ @NonNull List<AudioDeviceAttributes> devices) {
+ final AudioProductStrategy strategy =
+ AudioProductStrategy.getAudioProductStrategyWithId(strategyId);
+
+ mNonDefDevListenerMgr.callListeners(
+ (listener) -> listener.onNonDefaultDevicesForStrategyChanged(
+ strategy, devices));
}
- for (PrefDevListenerInfo info : mPrefDevListeners) {
- if (info.mListener == listener) {
- return info;
+
+ @Override
+ public void register(boolean register) {
+ try {
+ if (register) {
+ getService().registerStrategyNonDefaultDevicesDispatcher(this);
+ } else {
+ getService().unregisterStrategyNonDefaultDevicesDispatcher(this);
+ }
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
}
}
- return null;
- }
-
- @GuardedBy("mPrefDevListenerLock")
- private boolean hasPrefDevListener(OnPreferredDevicesForStrategyChangedListener listener) {
- return getPrefDevListenerInfo(listener) != null;
- }
-
- @GuardedBy("mPrefDevListenerLock")
- /**
- * @return true if the listener was removed from the list
- */
- private boolean removePrefDevListener(OnPreferredDevicesForStrategyChangedListener listener) {
- final PrefDevListenerInfo infoToRemove = getPrefDevListenerInfo(listener);
- if (infoToRemove != null) {
- mPrefDevListeners.remove(infoToRemove);
- return true;
- }
- return false;
}
//====================================================================
@@ -3635,11 +3737,15 @@
* whether sounds are heard or not.
* @hide
*/
- public void playSoundEffect(@SystemSoundEffect int effectType, int userId) {
+ public void playSoundEffect(@SystemSoundEffect int effectType, int userId) {
if (effectType < 0 || effectType >= NUM_SOUND_EFFECTS) {
return;
}
+ if (delegateSoundEffectToVdm(effectType)) {
+ return;
+ }
+
final IAudioService service = getService();
try {
service.playSoundEffect(effectType, userId);
@@ -3657,11 +3763,15 @@
* NOTE: This version is for applications that have their own
* settings panel for enabling and controlling volume.
*/
- public void playSoundEffect(@SystemSoundEffect int effectType, float volume) {
+ public void playSoundEffect(@SystemSoundEffect int effectType, float volume) {
if (effectType < 0 || effectType >= NUM_SOUND_EFFECTS) {
return;
}
+ if (delegateSoundEffectToVdm(effectType)) {
+ return;
+ }
+
final IAudioService service = getService();
try {
service.playSoundEffectVolume(effectType, volume);
@@ -3671,6 +3781,28 @@
}
/**
+ * Checks whether this {@link AudioManager} instance is asociated with {@link VirtualDevice}
+ * configured with custom device policy for audio. If there is such device, request to play
+ * sound effect is forwarded to {@link VirtualDeviceManager}.
+ *
+ * @param effectType - The type of sound effect.
+ * @return true if the request was forwarded to {@link VirtualDeviceManager} instance,
+ * false otherwise.
+ */
+ private boolean delegateSoundEffectToVdm(@SystemSoundEffect int effectType) {
+ int deviceId = getContext().getDeviceId();
+ if (deviceId != DEVICE_ID_DEFAULT) {
+ VirtualDeviceManager vdm = getVirtualDeviceManager();
+ if (vdm != null && vdm.getDevicePolicy(deviceId, POLICY_TYPE_AUDIO)
+ != DEVICE_POLICY_DEFAULT) {
+ vdm.playSoundEffect(deviceId, effectType);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
* Load Sound effects.
* This method must be called when sound effects are enabled.
*/
@@ -8773,6 +8905,55 @@
}
}
+ /**
+ * Requests if the implementation supports controlling the latency modes
+ * over the Bluetooth A2DP or LE Audio links.
+ *
+ * @return true if supported, false otherwise
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+ public boolean supportsBluetoothVariableLatency() {
+ try {
+ return getService().supportsBluetoothVariableLatency();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Enables or disables the variable Bluetooth latency control mechanism in the
+ * audio framework and the audio HAL. This does not apply to the latency mode control
+ * on the spatializer output as this is a built-in feature.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+ public void setBluetoothVariableLatencyEnabled(boolean enabled) {
+ try {
+ getService().setBluetoothVariableLatencyEnabled(enabled);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Indicates if the variable Bluetooth latency control mechanism is enabled or disabled.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+ public boolean isBluetoothVariableLatencyEnabled() {
+ try {
+ return getService().isBluetoothVariableLatencyEnabled();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
//====================================================================
// Mute await connection
diff --git a/media/java/android/media/AudioPlaybackConfiguration.java b/media/java/android/media/AudioPlaybackConfiguration.java
index 59a0f7b..f64e5cc 100644
--- a/media/java/android/media/AudioPlaybackConfiguration.java
+++ b/media/java/android/media/AudioPlaybackConfiguration.java
@@ -32,6 +32,8 @@
import android.os.RemoteException;
import android.util.Log;
+import com.android.internal.annotations.GuardedBy;
+
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -178,6 +180,11 @@
* Used to update the mute state of a player through its port id
*/
public static final int PLAYER_UPDATE_MUTED = 7;
+ /**
+ * @hide
+ * Used to update the spatialization status and format of a player through its port id
+ */
+ public static final int PLAYER_UPDATE_FORMAT = 8;
/** @hide */
@IntDef({
@@ -190,6 +197,7 @@
PLAYER_UPDATE_DEVICE_ID,
PLAYER_UPDATE_PORT_ID,
PLAYER_UPDATE_MUTED,
+ PLAYER_UPDATE_FORMAT,
})
@Retention(RetentionPolicy.SOURCE)
public @interface PlayerState {}
@@ -206,6 +214,7 @@
case PLAYER_UPDATE_DEVICE_ID: return "PLAYER_UPDATE_DEVICE_ID";
case PLAYER_UPDATE_PORT_ID: return "PLAYER_UPDATE_PORT_ID";
case PLAYER_UPDATE_MUTED: return "PLAYER_UPDATE_MUTED";
+ case PLAYER_UPDATE_FORMAT: return "PLAYER_UPDATE_FORMAT";
default:
return "invalid state " + state;
}
@@ -213,6 +222,27 @@
/**
* @hide
+ * Used to update the spatialization status of a player through its port ID. Must be kept in
+ * sync with frameworks/native/include/audiomanager/AudioManager.h
+ */
+ public static final String EXTRA_PLAYER_EVENT_SPATIALIZED =
+ "android.media.extra.PLAYER_EVENT_SPATIALIZED";
+ /**
+ * @hide
+ * Used to update the sample rate of a player through its port ID. Must be kept in sync with
+ * frameworks/native/include/audiomanager/AudioManager.h
+ */
+ public static final String EXTRA_PLAYER_EVENT_SAMPLE_RATE =
+ "android.media.extra.PLAYER_EVENT_SAMPLE_RATE";
+ /**
+ * @hide
+ * Used to update the channel mask of a player through its port ID. Must be kept in sync with
+ * frameworks/native/include/audiomanager/AudioManager.h
+ */
+ public static final String EXTRA_PLAYER_EVENT_CHANNEL_MASK =
+ "android.media.extra.PLAYER_EVENT_CHANNEL_MASK";
+ /**
+ * @hide
* Used to update the mute state of a player through its port ID. Must be kept in sync with
* frameworks/native/include/audiomanager/AudioManager.h
*/
@@ -284,10 +314,16 @@
private int mPlayerState;
private AudioAttributes mPlayerAttr; // never null
+ // lock for updateable properties
+ private final Object mUpdateablePropLock = new Object();
+
+ @GuardedBy("mUpdateablePropLock")
private int mDeviceId;
-
+ @GuardedBy("mUpdateablePropLock")
private int mSessionId;
-
+ @GuardedBy("mUpdateablePropLock")
+ private @NonNull FormatInfo mFormatInfo;
+ @GuardedBy("mUpdateablePropLock")
@PlayerMuteEvent private int mMutedState;
/**
@@ -320,6 +356,7 @@
mIPlayerShell = null;
}
mSessionId = pic.mSessionId;
+ mFormatInfo = FormatInfo.DEFAULT;
}
/**
@@ -333,13 +370,23 @@
}
}
+ // sets the fields that are updateable and require synchronization
+ private void setUpdateableFields(int deviceId, int sessionId, int mutedState, FormatInfo format)
+ {
+ synchronized (mUpdateablePropLock) {
+ mDeviceId = deviceId;
+ mSessionId = sessionId;
+ mMutedState = mutedState;
+ mFormatInfo = format;
+ }
+ }
// Note that this method is called server side, so no "privileged" information is ever sent
// to a client that is not supposed to have access to it.
/**
* @hide
* Creates a copy of the playback configuration that is stripped of any data enabling
* identification of which application it is associated with ("anonymized").
- * @param toSanitize
+ * @param in the instance to copy from
*/
public static AudioPlaybackConfiguration anonymizedCopy(AudioPlaybackConfiguration in) {
final AudioPlaybackConfiguration anonymCopy = new AudioPlaybackConfiguration(in.mPlayerIId);
@@ -357,14 +404,16 @@
builder.setUsage(in.mPlayerAttr.getUsage());
}
anonymCopy.mPlayerAttr = builder.build();
- anonymCopy.mDeviceId = in.mDeviceId;
// anonymized data
- anonymCopy.mMutedState = 0;
anonymCopy.mPlayerType = PLAYER_TYPE_UNKNOWN;
anonymCopy.mClientUid = PLAYER_UPID_INVALID;
anonymCopy.mClientPid = PLAYER_UPID_INVALID;
anonymCopy.mIPlayerShell = null;
- anonymCopy.mSessionId = AudioSystem.AUDIO_SESSION_ALLOCATE;
+ anonymCopy.setUpdateableFields(
+ /*deviceId*/ PLAYER_DEVICEID_INVALID,
+ /*sessionId*/ AudioSystem.AUDIO_SESSION_ALLOCATE,
+ /*mutedState*/ 0,
+ FormatInfo.DEFAULT);
return anonymCopy;
}
@@ -401,10 +450,14 @@
* @return the audio playback device or null if the device is not available at the time of query
*/
public @Nullable AudioDeviceInfo getAudioDeviceInfo() {
- if (mDeviceId == PLAYER_DEVICEID_INVALID) {
+ final int deviceId;
+ synchronized (mUpdateablePropLock) {
+ deviceId = mDeviceId;
+ }
+ if (deviceId == PLAYER_DEVICEID_INVALID) {
return null;
}
- return AudioManager.getDeviceForPortId(mDeviceId, AudioManager.GET_DEVICES_OUTPUTS);
+ return AudioManager.getDeviceForPortId(deviceId, AudioManager.GET_DEVICES_OUTPUTS);
}
/**
@@ -415,7 +468,9 @@
*/
@SystemApi
public @IntRange(from = 0) int getSessionId() {
- return mSessionId;
+ synchronized (mUpdateablePropLock) {
+ return mSessionId;
+ }
}
/**
@@ -428,7 +483,9 @@
@SystemApi
@RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
public boolean isMuted() {
- return mMutedState != 0;
+ synchronized (mUpdateablePropLock) {
+ return mMutedState != 0;
+ }
}
/**
@@ -440,7 +497,9 @@
@SystemApi
@RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
@PlayerMuteEvent public int getMutedBy() {
- return mMutedState;
+ synchronized (mUpdateablePropLock) {
+ return mMutedState;
+ }
}
/**
@@ -500,6 +559,43 @@
/**
* @hide
+ * Return whether this player's output is spatialized
+ * @return true if spatialized, false if not or playback hasn't started
+ */
+ @SystemApi
+ public boolean isSpatialized() {
+ synchronized (mUpdateablePropLock) {
+ return mFormatInfo.mIsSpatialized;
+ }
+ }
+
+ /**
+ * @hide
+ * Return the sample rate in Hz of the content being played.
+ * @return the sample rate in Hertz, or 0 if playback hasn't started
+ */
+ @SystemApi
+ public @IntRange(from = 0) int getSampleRate() {
+ synchronized (mUpdateablePropLock) {
+ return mFormatInfo.mSampleRate;
+ }
+ }
+
+ /**
+ * @hide
+ * Return the player's channel mask
+ * @return the channel mask, or 0 if playback hasn't started. See {@link AudioFormat} and
+ * the definitions for the <code>CHANNEL_OUT_*</code> values used for the mask's bitfield
+ */
+ @SystemApi
+ public int getChannelMask() {
+ synchronized (mUpdateablePropLock) {
+ return (AudioFormat.convertNativeChannelMaskToOutMask(mFormatInfo.mNativeChannelMask));
+ }
+ }
+
+ /**
+ * @hide
* @return the IPlayer interface for the associated player
*/
IPlayer getIPlayer() {
@@ -527,9 +623,11 @@
* @param sessionId the audio session ID
*/
public boolean handleSessionIdEvent(int sessionId) {
- final boolean changed = sessionId != mSessionId;
- mSessionId = sessionId;
- return changed;
+ synchronized (mUpdateablePropLock) {
+ final boolean changed = sessionId != mSessionId;
+ mSessionId = sessionId;
+ return changed;
+ }
}
/**
@@ -539,9 +637,25 @@
* @return true if the state changed, false otherwise
*/
public boolean handleMutedEvent(@PlayerMuteEvent int mutedState) {
- final boolean changed = mMutedState != mutedState;
- mMutedState = mutedState;
- return changed;
+ synchronized (mUpdateablePropLock) {
+ final boolean changed = mMutedState != mutedState;
+ mMutedState = mutedState;
+ return changed;
+ }
+ }
+
+ /**
+ * @hide
+ * Handle a change of playback format
+ * @param fi the latest format information
+ * @return true if the format changed, false otherwise
+ */
+ public boolean handleFormatEvent(@NonNull FormatInfo fi) {
+ synchronized (mUpdateablePropLock) {
+ final boolean changed = !mFormatInfo.equals(fi);
+ mFormatInfo = fi;
+ return changed;
+ }
}
/**
@@ -558,7 +672,7 @@
*/
public boolean handleStateEvent(int event, int deviceId) {
boolean changed = false;
- synchronized (this) {
+ synchronized (mUpdateablePropLock) {
// Do not update if it is only device id update
if (event != PLAYER_UPDATE_DEVICE_ID) {
@@ -647,8 +761,10 @@
@Override
public int hashCode() {
- return Objects.hash(mPlayerIId, mDeviceId, mMutedState, mPlayerType, mClientUid, mClientPid,
- mSessionId);
+ synchronized (mUpdateablePropLock) {
+ return Objects.hash(mPlayerIId, mDeviceId, mMutedState, mPlayerType, mClientUid,
+ mClientPid, mSessionId);
+ }
}
@Override
@@ -658,20 +774,23 @@
@Override
public void writeToParcel(Parcel dest, int flags) {
- dest.writeInt(mPlayerIId);
- dest.writeInt(mDeviceId);
- dest.writeInt(mMutedState);
- dest.writeInt(mPlayerType);
- dest.writeInt(mClientUid);
- dest.writeInt(mClientPid);
- dest.writeInt(mPlayerState);
- mPlayerAttr.writeToParcel(dest, 0);
- final IPlayerShell ips;
- synchronized (this) {
- ips = mIPlayerShell;
+ synchronized (mUpdateablePropLock) {
+ dest.writeInt(mPlayerIId);
+ dest.writeInt(mDeviceId);
+ dest.writeInt(mMutedState);
+ dest.writeInt(mPlayerType);
+ dest.writeInt(mClientUid);
+ dest.writeInt(mClientPid);
+ dest.writeInt(mPlayerState);
+ mPlayerAttr.writeToParcel(dest, 0);
+ final IPlayerShell ips;
+ synchronized (this) {
+ ips = mIPlayerShell;
+ }
+ dest.writeStrongInterface(ips == null ? null : ips.getIPlayer());
+ dest.writeInt(mSessionId);
+ mFormatInfo.writeToParcel(dest, 0);
}
- dest.writeStrongInterface(ips == null ? null : ips.getIPlayer());
- dest.writeInt(mSessionId);
}
private AudioPlaybackConfiguration(Parcel in) {
@@ -686,6 +805,7 @@
final IPlayer p = IPlayer.Stub.asInterface(in.readStrongBinder());
mIPlayerShell = (p == null) ? null : new IPlayerShell(null, p);
mSessionId = in.readInt();
+ mFormatInfo = FormatInfo.CREATOR.createFromParcel(in);
}
@Override
@@ -707,35 +827,39 @@
@Override
public String toString() {
StringBuilder apcToString = new StringBuilder();
- apcToString.append("AudioPlaybackConfiguration piid:").append(mPlayerIId).append(
- " deviceId:").append(mDeviceId).append(" type:").append(
- toLogFriendlyPlayerType(mPlayerType)).append(" u/pid:").append(mClientUid).append(
- "/").append(mClientPid).append(" state:").append(
- toLogFriendlyPlayerState(mPlayerState)).append(" attr:").append(mPlayerAttr).append(
- " sessionId:").append(mSessionId).append(" mutedState:");
- if (mMutedState == 0) {
- apcToString.append("none ");
- } else {
- if ((mMutedState & MUTED_BY_MASTER) != 0) {
- apcToString.append("master ");
+ synchronized (mUpdateablePropLock) {
+ apcToString.append("AudioPlaybackConfiguration piid:").append(mPlayerIId).append(
+ " deviceId:").append(mDeviceId).append(" type:").append(
+ toLogFriendlyPlayerType(mPlayerType)).append(" u/pid:").append(
+ mClientUid).append(
+ "/").append(mClientPid).append(" state:").append(
+ toLogFriendlyPlayerState(mPlayerState)).append(" attr:").append(
+ mPlayerAttr).append(
+ " sessionId:").append(mSessionId).append(" mutedState:");
+ if (mMutedState == 0) {
+ apcToString.append("none ");
+ } else {
+ if ((mMutedState & MUTED_BY_MASTER) != 0) {
+ apcToString.append("master ");
+ }
+ if ((mMutedState & MUTED_BY_STREAM_VOLUME) != 0) {
+ apcToString.append("streamVolume ");
+ }
+ if ((mMutedState & MUTED_BY_STREAM_MUTED) != 0) {
+ apcToString.append("streamMute ");
+ }
+ if ((mMutedState & MUTED_BY_APP_OPS) != 0) {
+ apcToString.append("appOps ");
+ }
+ if ((mMutedState & MUTED_BY_CLIENT_VOLUME) != 0) {
+ apcToString.append("clientVolume ");
+ }
+ if ((mMutedState & MUTED_BY_VOLUME_SHAPER) != 0) {
+ apcToString.append("volumeShaper ");
+ }
}
- if ((mMutedState & MUTED_BY_STREAM_VOLUME) != 0) {
- apcToString.append("streamVolume ");
- }
- if ((mMutedState & MUTED_BY_STREAM_MUTED) != 0) {
- apcToString.append("streamMute ");
- }
- if ((mMutedState & MUTED_BY_APP_OPS) != 0) {
- apcToString.append("appOps ");
- }
- if ((mMutedState & MUTED_BY_CLIENT_VOLUME) != 0) {
- apcToString.append("clientVolume ");
- }
- if ((mMutedState & MUTED_BY_VOLUME_SHAPER) != 0) {
- apcToString.append("volumeShaper ");
- }
+ apcToString.append(" ").append(mFormatInfo);
}
-
return apcToString.toString();
}
@@ -788,6 +912,79 @@
}
//=====================================================================
+
+ /**
+ * @hide
+ * Class to store sample rate, channel mask, and spatialization status
+ */
+ public static final class FormatInfo implements Parcelable {
+ static final FormatInfo DEFAULT = new FormatInfo(
+ /*spatialized*/ false, /*channel mask*/ 0, /*sample rate*/ 0);
+ final boolean mIsSpatialized;
+ final int mNativeChannelMask;
+ final int mSampleRate;
+
+ public FormatInfo(boolean isSpatialized, int nativeChannelMask, int sampleRate) {
+ mIsSpatialized = isSpatialized;
+ mNativeChannelMask = nativeChannelMask;
+ mSampleRate = sampleRate;
+ }
+
+ @Override
+ public String toString() {
+ return "FormatInfo{"
+ + "isSpatialized=" + mIsSpatialized
+ + ", channelMask=0x" + Integer.toHexString(mNativeChannelMask)
+ + ", sampleRate=" + mSampleRate
+ + '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof FormatInfo)) return false;
+ FormatInfo that = (FormatInfo) o;
+ return mIsSpatialized == that.mIsSpatialized
+ && mNativeChannelMask == that.mNativeChannelMask
+ && mSampleRate == that.mSampleRate;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mIsSpatialized, mNativeChannelMask, mSampleRate);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@androidx.annotation.NonNull Parcel dest, int flags) {
+ dest.writeBoolean(mIsSpatialized);
+ dest.writeInt(mNativeChannelMask);
+ dest.writeInt(mSampleRate);
+ }
+
+ private FormatInfo(Parcel in) {
+ this(
+ in.readBoolean(), // isSpatialized
+ in.readInt(), // channelMask
+ in.readInt() // sampleRate
+ );
+ }
+
+ public static final @NonNull Parcelable.Creator<FormatInfo> CREATOR =
+ new Parcelable.Creator<FormatInfo>() {
+ public FormatInfo createFromParcel(Parcel p) {
+ return new FormatInfo(p);
+ }
+ public FormatInfo[] newArray(int size) {
+ return new FormatInfo[size];
+ }
+ };
+ }
+ //=====================================================================
// Utilities
/** @hide */
diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java
index 3e0d657..9339c3d 100644
--- a/media/java/android/media/AudioSystem.java
+++ b/media/java/android/media/AudioSystem.java
@@ -2063,12 +2063,46 @@
/**
* @hide
+ * Remove device as role for product strategy.
+ * @param strategy the id of the strategy to configure
+ * @param role the role of the devices
+ * @param devices the list of devices to be removed as role for the given strategy
+ * @return {@link #SUCCESS} if successfully set
+ */
+ public static int removeDevicesRoleForStrategy(
+ int strategy, int role, @NonNull List<AudioDeviceAttributes> devices) {
+ if (devices.isEmpty()) {
+ return BAD_VALUE;
+ }
+ int[] types = new int[devices.size()];
+ String[] addresses = new String[devices.size()];
+ for (int i = 0; i < devices.size(); ++i) {
+ types[i] = devices.get(i).getInternalType();
+ addresses[i] = devices.get(i).getAddress();
+ }
+ return removeDevicesRoleForStrategy(strategy, role, types, addresses);
+ }
+
+ /**
+ * @hide
* Remove devices as role for the strategy
* @param strategy the id of the strategy to configure
* @param role the role of the devices
+ * @param types all device types
+ * @param addresses all device addresses
+ * @return {@link #SUCCESS} if successfully removed
+ */
+ public static native int removeDevicesRoleForStrategy(
+ int strategy, int role, @NonNull int[] types, @NonNull String[] addresses);
+
+ /**
+ * @hide
+ * Remove all devices as role for the strategy
+ * @param strategy the id of the strategy to configure
+ * @param role the role of the devices
* @return {@link #SUCCESS} if successfully removed
*/
- public static native int removeDevicesRoleForStrategy(int strategy, int role);
+ public static native int clearDevicesRoleForStrategy(int strategy, int role);
/**
* @hide
@@ -2476,4 +2510,30 @@
*/
public static native int clearPreferredMixerAttributes(
@NonNull AudioAttributes attributes, int portId, int uid);
+
+
+ /**
+ * Requests if the implementation supports controlling the latency modes
+ * over the Bluetooth A2DP or LE Audio links.
+ *
+ * @return true if supported, false otherwise
+ *
+ * @hide
+ */
+ public static native boolean supportsBluetoothVariableLatency();
+
+ /**
+ * Enables or disables the variable Bluetooth latency control mechanism in the
+ * audio framework and the audio HAL. This does not apply to the latency mode control
+ * on the spatializer output as this is a built-in feature.
+ *
+ * @hide
+ */
+ public static native int setBluetoothVariableLatencyEnabled(boolean enabled);
+
+ /**
+ * Indicates if the variable Bluetooth latency control mechanism is enabled or disabled.
+ * @hide
+ */
+ public static native boolean isBluetoothVariableLatencyEnabled();
}
diff --git a/media/java/android/media/CallbackUtil.java b/media/java/android/media/CallbackUtil.java
index 2b5fd25..f0280da 100644
--- a/media/java/android/media/CallbackUtil.java
+++ b/media/java/android/media/CallbackUtil.java
@@ -183,7 +183,7 @@
if (!removeListener(listener, listeners)) {
throw new IllegalArgumentException("attempt to call " + methodName
- + "on an unregistered listener");
+ + " on an unregistered listener");
}
if (listeners.size() == 0) {
unregisterStub.accept(dispatchStub);
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index 05a0b86..4b36237 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -42,6 +42,7 @@
import android.media.IRecordingConfigDispatcher;
import android.media.IRingtonePlayer;
import android.media.IStrategyPreferredDevicesDispatcher;
+import android.media.IStrategyNonDefaultDevicesDispatcher;
import android.media.ISpatializerCallback;
import android.media.ISpatializerHeadTrackerAvailableCallback;
import android.media.ISpatializerHeadTrackingModeCallback;
@@ -330,7 +331,8 @@
boolean isCallScreeningModeSupported();
- int setPreferredDevicesForStrategy(in int strategy, in List<AudioDeviceAttributes> device);
+ @EnforcePermission("MODIFY_AUDIO_ROUTING")
+ int setPreferredDevicesForStrategy(in int strategy, in List<AudioDeviceAttributes> devices);
@EnforcePermission("MODIFY_AUDIO_ROUTING")
int removePreferredDevicesForStrategy(in int strategy);
@@ -338,6 +340,15 @@
@EnforcePermission("MODIFY_AUDIO_ROUTING")
List<AudioDeviceAttributes> getPreferredDevicesForStrategy(in int strategy);
+ @EnforcePermission("MODIFY_AUDIO_ROUTING")
+ int setDeviceAsNonDefaultForStrategy(in int strategy, in AudioDeviceAttributes device);
+
+ @EnforcePermission("MODIFY_AUDIO_ROUTING")
+ int removeDeviceAsNonDefaultForStrategy(in int strategy, in AudioDeviceAttributes device);
+
+ @EnforcePermission("MODIFY_AUDIO_ROUTING")
+ List<AudioDeviceAttributes> getNonDefaultDevicesForStrategy(in int strategy);
+
List<AudioDeviceAttributes> getDevicesForAttributes(in AudioAttributes attributes);
List<AudioDeviceAttributes> getDevicesForAttributesUnprotected(in AudioAttributes attributes);
@@ -351,6 +362,12 @@
oneway void unregisterStrategyPreferredDevicesDispatcher(
IStrategyPreferredDevicesDispatcher dispatcher);
+ void registerStrategyNonDefaultDevicesDispatcher(
+ IStrategyNonDefaultDevicesDispatcher dispatcher);
+
+ oneway void unregisterStrategyNonDefaultDevicesDispatcher(
+ IStrategyNonDefaultDevicesDispatcher dispatcher);
+
oneway void setRttEnabled(in boolean rttEnabled);
@EnforcePermission("MODIFY_AUDIO_ROUTING")
@@ -587,4 +604,16 @@
IPreferredMixerAttributesDispatcher dispatcher);
oneway void unregisterPreferredMixerAttributesDispatcher(
IPreferredMixerAttributesDispatcher dispatcher);
+
+ @EnforcePermission("MODIFY_AUDIO_ROUTING")
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)")
+ boolean supportsBluetoothVariableLatency();
+
+ @EnforcePermission("MODIFY_AUDIO_ROUTING")
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)")
+ void setBluetoothVariableLatencyEnabled(boolean enabled);
+
+ @EnforcePermission("MODIFY_AUDIO_ROUTING")
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)")
+ boolean isBluetoothVariableLatencyEnabled();
}
diff --git a/media/java/android/media/IStrategyNonDefaultDevicesDispatcher.aidl b/media/java/android/media/IStrategyNonDefaultDevicesDispatcher.aidl
new file mode 100644
index 0000000..59239cb
--- /dev/null
+++ b/media/java/android/media/IStrategyNonDefaultDevicesDispatcher.aidl
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import android.media.AudioDeviceAttributes;
+
+/**
+ * AIDL for AudioService to signal non-daefault devices updates for audio strategies.
+ *
+ * {@hide}
+ */
+oneway interface IStrategyNonDefaultDevicesDispatcher {
+
+ void dispatchNonDefDevicesChanged(int strategyId, in List<AudioDeviceAttributes> devices);
+
+}
+
diff --git a/media/java/android/media/RouteListingPreference.java b/media/java/android/media/RouteListingPreference.java
index 6557277..6a5b290 100644
--- a/media/java/android/media/RouteListingPreference.java
+++ b/media/java/android/media/RouteListingPreference.java
@@ -17,6 +17,7 @@
package android.media;
import android.annotation.IntDef;
+import android.annotation.IntRange;
import android.annotation.NonNull;
import android.os.Parcel;
import android.os.Parcelable;
@@ -87,6 +88,9 @@
* Returns true if the application would like media route listing to use the system's ordering
* strategy, or false if the application would like route listing to respect the ordering
* obtained from {@link #getItems()}.
+ *
+ * <p>The system's ordering strategy is implementation-dependent, but may take into account each
+ * route's recency or frequency of use in order to rank them.
*/
public boolean getUseSystemOrdering() {
return mUseSystemOrdering;
@@ -195,19 +199,22 @@
@NonNull private final String mRouteId;
@Flags private final int mFlags;
@DisableReason private final int mDisableReason;
+ private final int mSessionParticipantCount;
private Item(@NonNull Builder builder) {
mRouteId = builder.mRouteId;
mFlags = builder.mFlags;
mDisableReason = builder.mDisableReason;
+ mSessionParticipantCount = builder.mSessionParticipantCount;
}
private Item(Parcel in) {
- String routeId = in.readString();
- Preconditions.checkArgument(!TextUtils.isEmpty(routeId));
- mRouteId = routeId;
+ mRouteId = in.readString();
+ Preconditions.checkArgument(!TextUtils.isEmpty(mRouteId));
mFlags = in.readInt();
mDisableReason = in.readInt();
+ mSessionParticipantCount = in.readInt();
+ Preconditions.checkArgument(mSessionParticipantCount >= 0);
}
/** Returns the id of the route that corresponds to this route listing preference item. */
@@ -241,6 +248,17 @@
return mDisableReason;
}
+ /**
+ * Returns a non-negative number of participants in the ongoing session (if any) on the
+ * corresponding route.
+ *
+ * <p>The system ignores this value if zero, or if {@link #getFlags()} does not include
+ * {@link #FLAG_ONGOING_SESSION}.
+ */
+ public int getSessionParticipantCount() {
+ return mSessionParticipantCount;
+ }
+
// Item Parcelable implementation.
@Override
@@ -253,6 +271,7 @@
dest.writeString(mRouteId);
dest.writeInt(mFlags);
dest.writeInt(mDisableReason);
+ dest.writeInt(mSessionParticipantCount);
}
// Equals and hashCode.
@@ -268,12 +287,13 @@
Item item = (Item) other;
return mRouteId.equals(item.mRouteId)
&& mFlags == item.mFlags
- && mDisableReason == item.mDisableReason;
+ && mDisableReason == item.mDisableReason
+ && mSessionParticipantCount == item.mSessionParticipantCount;
}
@Override
public int hashCode() {
- return Objects.hash(mRouteId, mFlags, mDisableReason);
+ return Objects.hash(mRouteId, mFlags, mDisableReason, mSessionParticipantCount);
}
/** Builder for {@link Item}. */
@@ -282,6 +302,7 @@
private final String mRouteId;
private int mFlags;
private int mDisableReason;
+ private int mSessionParticipantCount;
/**
* Constructor.
@@ -308,6 +329,17 @@
return this;
}
+ /** See {@link Item#getSessionParticipantCount()}. */
+ @NonNull
+ public Builder setSessionParticipantCount(
+ @IntRange(from = 0) int sessionParticipantCount) {
+ Preconditions.checkArgument(
+ sessionParticipantCount >= 0,
+ "sessionParticipantCount must be non-negative.");
+ mSessionParticipantCount = sessionParticipantCount;
+ return this;
+ }
+
/** Creates and returns a new {@link Item} with the given parameters. */
@NonNull
public Item build() {
diff --git a/media/java/android/media/audio/common/AidlConversion.java b/media/java/android/media/audio/common/AidlConversion.java
index 490809c..65fd51b 100644
--- a/media/java/android/media/audio/common/AidlConversion.java
+++ b/media/java/android/media/audio/common/AidlConversion.java
@@ -585,6 +585,10 @@
switch (standard) {
case AudioDescriptor.STANDARD_EDID:
return AudioStandard.EDID;
+ case AudioDescriptor.STANDARD_SADB:
+ return AudioStandard.SADB;
+ case AudioDescriptor.STANDARD_VSADB:
+ return AudioStandard.VSADB;
case AudioDescriptor.STANDARD_NONE:
default:
return AudioStandard.NONE;
@@ -599,6 +603,10 @@
switch (standard) {
case AudioStandard.EDID:
return AudioDescriptor.STANDARD_EDID;
+ case AudioStandard.SADB:
+ return AudioDescriptor.STANDARD_SADB;
+ case AudioStandard.VSADB:
+ return AudioDescriptor.STANDARD_VSADB;
case AudioStandard.NONE:
default:
return AudioDescriptor.STANDARD_NONE;
diff --git a/media/java/android/media/projection/IMediaProjectionCallback.aidl b/media/java/android/media/projection/IMediaProjectionCallback.aidl
index 2c8de2e..147d74c 100644
--- a/media/java/android/media/projection/IMediaProjectionCallback.aidl
+++ b/media/java/android/media/projection/IMediaProjectionCallback.aidl
@@ -20,4 +20,5 @@
oneway interface IMediaProjectionCallback {
void onStop();
void onCapturedContentResize(int width, int height);
+ void onCapturedContentVisibilityChanged(boolean isVisible);
}
diff --git a/media/java/android/media/projection/IMediaProjectionManager.aidl b/media/java/android/media/projection/IMediaProjectionManager.aidl
index a63d02b..99d1f8d 100644
--- a/media/java/android/media/projection/IMediaProjectionManager.aidl
+++ b/media/java/android/media/projection/IMediaProjectionManager.aidl
@@ -48,7 +48,11 @@
void notifyActiveProjectionCapturedContentResized(int width, int height);
@JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
- + ".permission.MANAGE_MEDIA_PROJECTION)")
+ + ".permission.MANAGE_MEDIA_PROJECTION)")
+ void notifyActiveProjectionCapturedContentVisibilityChanged(boolean isVisible);
+
+ @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
+ + ".permission.MANAGE_MEDIA_PROJECTION)")
void addCallback(IMediaProjectionWatcherCallback callback);
@JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
diff --git a/media/java/android/media/projection/MediaProjection.java b/media/java/android/media/projection/MediaProjection.java
index 3dfff1f..985ac3c 100644
--- a/media/java/android/media/projection/MediaProjection.java
+++ b/media/java/android/media/projection/MediaProjection.java
@@ -66,13 +66,20 @@
}
}
- /** Register a listener to receive notifications about when the {@link
- * MediaProjection} changes state.
+ /**
+ * Register a listener to receive notifications about when the {@link MediaProjection} or
+ * captured content changes state.
+ * <p>
+ * The callback should be registered before invoking
+ * {@link #createVirtualDisplay(String, int, int, int, int, Surface, VirtualDisplay.Callback,
+ * Handler)}
+ * to ensure that any notifications on the callback are not missed.
+ * </p>
*
* @param callback The callback to call.
- * @param handler The handler on which the callback should be invoked, or
- * null if the callback should be invoked on the calling thread's looper.
- *
+ * @param handler The handler on which the callback should be invoked, or
+ * null if the callback should be invoked on the calling thread's looper.
+ * @throws IllegalArgumentException If the given callback is null.
* @see #unregisterCallback
*/
public void registerCallback(Callback callback, Handler handler) {
@@ -85,10 +92,11 @@
mCallbacks.put(callback, new CallbackRecord(callback, handler));
}
- /** Unregister a MediaProjection listener.
+ /**
+ * Unregister a {@link MediaProjection} listener.
*
* @param callback The callback to unregister.
- *
+ * @throws IllegalArgumentException If the given callback is null.
* @see #registerCallback
*/
public void unregisterCallback(Callback callback) {
@@ -283,6 +291,34 @@
* }</pre>
*/
public void onCapturedContentResize(int width, int height) { }
+
+ /**
+ * Indicates the visibility of the captured region has changed. Called immediately after
+ * capture begins with the initial visibility state, and when visibility changes. Provides
+ * the app with accurate state for presenting its own UI. The application can take advantage
+ * of this by showing or hiding the captured content, based on if the captured region is
+ * currently visible to the user.
+ * <p>
+ * For example, if the user elected to capture a single app (from the activity shown from
+ * {@link MediaProjectionManager#createScreenCaptureIntent()}), the callback may be
+ * triggered for the following reasons:
+ * <ul>
+ * <li>
+ * The captured region may become visible ({@code isVisible} with value
+ * {@code true}), because the captured app is at least partially visible. This may
+ * happen if the captured app was previously covered by another app. The other app
+ * moves to show at least some portion of the captured app.
+ * </li>
+ * <li>
+ * The captured region may become invisible ({@code isVisible} with value
+ * {@code false}) if it is entirely hidden. This may happen if the captured app is
+ * entirely covered by another app, or the user navigates away from the captured
+ * app.
+ * </li>
+ * </ul>
+ * </p>
+ */
+ public void onCapturedContentVisibilityChanged(boolean isVisible) { }
}
private final class MediaProjectionCallback extends IMediaProjectionCallback.Stub {
@@ -299,6 +335,13 @@
cbr.onCapturedContentResize(width, height);
}
}
+
+ @Override
+ public void onCapturedContentVisibilityChanged(boolean isVisible) {
+ for (CallbackRecord cbr : mCallbacks.values()) {
+ cbr.onCapturedContentVisibilityChanged(isVisible);
+ }
+ }
}
private final static class CallbackRecord {
@@ -322,5 +365,9 @@
public void onCapturedContentResize(int width, int height) {
mHandler.post(() -> mCallback.onCapturedContentResize(width, height));
}
+
+ public void onCapturedContentVisibilityChanged(boolean isVisible) {
+ mHandler.post(() -> mCallback.onCapturedContentVisibilityChanged(isVisible));
+ }
}
}
diff --git a/media/java/android/media/tv/TvInputInfo.java b/media/java/android/media/tv/TvInputInfo.java
index e60d537..667a9ae 100644
--- a/media/java/android/media/tv/TvInputInfo.java
+++ b/media/java/android/media/tv/TvInputInfo.java
@@ -946,6 +946,10 @@
id = generateInputId(componentName, mTvInputHardwareInfo);
type = sHardwareTypeToTvInputType.get(mTvInputHardwareInfo.getType(), TYPE_TUNER);
isHardwareInput = true;
+ if (mTvInputHardwareInfo.getType() == TvInputHardwareInfo.TV_INPUT_TYPE_HDMI) {
+ mHdmiDeviceInfo = HdmiDeviceInfo.hardwarePort(
+ HdmiDeviceInfo.PATH_INVALID, mTvInputHardwareInfo.getHdmiPortId());
+ }
} else {
id = generateInputId(componentName);
type = TYPE_TUNER;
diff --git a/media/java/android/media/tv/TvRecordingInfo.aidl b/media/java/android/media/tv/TvRecordingInfo.aidl
new file mode 100644
index 0000000..64cf3c3
--- /dev/null
+++ b/media/java/android/media/tv/TvRecordingInfo.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.tv;
+
+parcelable TvRecordingInfo;
\ No newline at end of file
diff --git a/media/java/android/media/tv/TvRecordingInfo.java b/media/java/android/media/tv/TvRecordingInfo.java
new file mode 100644
index 0000000..8de42f3
--- /dev/null
+++ b/media/java/android/media/tv/TvRecordingInfo.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.tv;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+/**
+ @hide
+ */
+public final class TvRecordingInfo implements Parcelable {
+ /*
+ * Indicates that getTvRecordingInfoList should return scheduled recordings.
+ */
+ public static final int RECORDING_SCHEDULED = 1;
+ /*
+ * Indicates that getTvRecordingInfoList should return in-progress recordings.
+ */
+ public static final int RECORDING_IN_PROGRESS = 2;
+ /*
+ * Indicates that getTvRecordingInfoList should return all recordings.
+ */
+ public static final int RECORDING_ALL = 3;
+ /**
+ * Indicates which recordings should be returned by getTvRecordingList
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = { "RECORDING_" }, value = {
+ RECORDING_SCHEDULED,
+ RECORDING_IN_PROGRESS,
+ RECORDING_ALL,
+ })
+ public @interface TvRecordingListType {}
+
+ private String mRecordingId;
+ private int mStartPadding;
+ private int mEndPadding;
+ private int mRepeatDays;
+ private String mName;
+ private String mDescription;
+ private int mScheduledStartTime;
+ private int mScheduledDuration;
+ private Uri mChannelUri;
+ private Uri mProgramId;
+ private List<String> mParentalRatings;
+ private String mRecordingUri;
+ private int mRecordingStartTime;
+ private int mRecordingDuration;
+
+ public TvRecordingInfo(
+ @NonNull String recordingId, @NonNull int startPadding, @NonNull int endPadding,
+ @NonNull int repeatDays, @NonNull int scheduledStartTime,
+ @NonNull int scheduledDuration, @NonNull Uri channelUri, @Nullable Uri programId,
+ @NonNull List<String> parentalRatings, @NonNull String recordingUri,
+ @NonNull int recordingStartTime, @NonNull int recordingDuration) {
+ mRecordingId = recordingId;
+ mStartPadding = startPadding;
+ mEndPadding = endPadding;
+ mRepeatDays = repeatDays;
+ mScheduledStartTime = scheduledStartTime;
+ mScheduledDuration = scheduledDuration;
+ mChannelUri = channelUri;
+ mScheduledDuration = scheduledDuration;
+ mChannelUri = channelUri;
+ mProgramId = programId;
+ mParentalRatings = parentalRatings;
+ mRecordingUri = recordingUri;
+ mRecordingStartTime = recordingStartTime;
+ mRecordingDuration = recordingDuration;
+ }
+ @NonNull
+ public String getRecordingId() {
+ return mRecordingId;
+ }
+ @NonNull
+ public int getStartPadding() {
+ return mStartPadding;
+ }
+ @NonNull
+ public int getEndPadding() {
+ return mEndPadding;
+ }
+ @NonNull
+ public int getRepeatDays() {
+ return mRepeatDays;
+ }
+ @NonNull
+ public String getName() {
+ return mName;
+ }
+ @NonNull
+ public void setName(String name) {
+ mName = name;
+ }
+ @NonNull
+ public String getDescription() {
+ return mDescription;
+ }
+ @NonNull
+ public void setDescription(String description) {
+ mDescription = description;
+ }
+ @NonNull
+ public int getScheduledStartTime() {
+ return mScheduledStartTime;
+ }
+ @NonNull
+ public int getScheduledDuration() {
+ return mScheduledDuration;
+ }
+ @NonNull
+ public Uri getChannelUri() {
+ return mChannelUri;
+ }
+ @Nullable
+ public Uri getProgramId() {
+ return mProgramId;
+ }
+ @NonNull
+ public List<String> getParentalRatings() {
+ return mParentalRatings;
+ }
+ @NonNull
+ public String getRecordingUri() {
+ return mRecordingUri;
+ }
+ @NonNull
+ public int getRecordingStartTime() {
+ return mRecordingStartTime;
+ }
+ @NonNull
+ public int getRecordingDuration() {
+ return mRecordingDuration;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Used to package this object into a {@link Parcel}.
+ *
+ * @param dest The {@link Parcel} to be written.
+ * @param flags The flags used for parceling.
+ */
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeString(mRecordingId);
+ dest.writeInt(mStartPadding);
+ dest.writeInt(mEndPadding);
+ dest.writeInt(mRepeatDays);
+ dest.writeString(mName);
+ dest.writeString(mDescription);
+ dest.writeInt(mScheduledStartTime);
+ dest.writeInt(mScheduledDuration);
+ dest.writeString(mChannelUri == null ? null : mChannelUri.toString());
+ dest.writeString(mProgramId == null ? null : mProgramId.toString());
+ dest.writeStringList(mParentalRatings);
+ dest.writeString(mRecordingUri);
+ dest.writeInt(mRecordingDuration);
+ dest.writeInt(mRecordingStartTime);
+ }
+
+ private TvRecordingInfo(Parcel in) {
+ mRecordingId = in.readString();
+ mStartPadding = in.readInt();
+ mEndPadding = in.readInt();
+ mRepeatDays = in.readInt();
+ mName = in.readString();
+ mDescription = in.readString();
+ mScheduledStartTime = in.readInt();
+ mScheduledDuration = in.readInt();
+ mChannelUri = Uri.parse(in.readString());
+ mProgramId = Uri.parse(in.readString());
+ in.readStringList(mParentalRatings);
+ mRecordingUri = in.readString();
+ mRecordingDuration = in.readInt();
+ mRecordingStartTime = in.readInt();
+ }
+
+
+ public static final @android.annotation.NonNull Parcelable.Creator<TvRecordingInfo> CREATOR =
+ new Parcelable.Creator<>() {
+ @Override
+ @NonNull
+ public TvRecordingInfo createFromParcel(Parcel in) {
+ return new TvRecordingInfo(in);
+ }
+
+ @Override
+ @NonNull
+ public TvRecordingInfo[] newArray(int size) {
+ return new TvRecordingInfo[size];
+ }
+ };
+}
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
index 98357fc..537e711 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
@@ -20,6 +20,7 @@
import android.media.tv.AdBuffer;
import android.media.tv.AdRequest;
import android.media.tv.BroadcastInfoRequest;
+import android.media.tv.TvRecordingInfo;
import android.net.Uri;
import android.os.Bundle;
import android.view.InputChannel;
@@ -48,6 +49,9 @@
void onRequestCurrentTvInputId(int seq);
void onRequestStartRecording(in Uri programUri, int seq);
void onRequestStopRecording(in String recordingId, int seq);
+ void onSetTvRecordingInfo(in String recordingId, in TvRecordingInfo recordingInfo, int seq);
+ void onRequestTvRecordingInfo(in String recordingId, int seq);
+ void onRequestTvRecordingInfoList(in int type, int seq);
void onRequestSigning(
in String id, in String algorithm, in String alias, in byte[] data, int seq);
void onAdRequest(in AdRequest request, int Seq);
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl
index 8bfceee..3b272daa 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl
@@ -21,6 +21,7 @@
import android.media.tv.AdResponse;
import android.media.tv.BroadcastInfoResponse;
import android.media.tv.TvTrackInfo;
+import android.media.tv.TvRecordingInfo;
import android.media.tv.interactive.AppLinkInfo;
import android.media.tv.interactive.ITvInteractiveAppClient;
import android.media.tv.interactive.ITvInteractiveAppManagerCallback;
@@ -52,6 +53,9 @@
void sendCurrentTvInputId(in IBinder sessionToken, in String inputId, int userId);
void sendSigningResult(in IBinder sessionToken, in String signingId, in byte[] result,
int userId);
+ void sendTvRecordingInfo(in IBinder sessionToken, in TvRecordingInfo recordingInfo, int userId);
+ void sendTvRecordingInfoList(in IBinder sessionToken,
+ in List<TvRecordingInfo> recordingInfoList, int userId);
void notifyError(in IBinder sessionToken, in String errMsg, in Bundle params, int userId);
void createSession(in ITvInteractiveAppClient client, in String iAppServiceId, int type,
int seq, int userId);
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl
index 1953117..bc09cea 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl
@@ -23,6 +23,7 @@
import android.media.tv.AdResponse;
import android.media.tv.BroadcastInfoResponse;
import android.media.tv.TvTrackInfo;
+import android.media.tv.TvRecordingInfo;
import android.os.Bundle;
import android.view.Surface;
@@ -44,6 +45,8 @@
void sendTrackInfoList(in List<TvTrackInfo> tracks);
void sendCurrentTvInputId(in String inputId);
void sendSigningResult(in String signingId, in byte[] result);
+ void sendTvRecordingInfo(in TvRecordingInfo recordingInfo);
+ void sendTvRecordingInfoList(in List<TvRecordingInfo> recordingInfoList);
void notifyError(in String errMsg, in Bundle params);
void release();
void notifyTuned(in Uri channelUri);
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
index cd4f410..c5dbd19 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
@@ -20,6 +20,7 @@
import android.media.tv.AdBuffer;
import android.media.tv.AdRequest;
import android.media.tv.BroadcastInfoRequest;
+import android.media.tv.TvRecordingInfo;
import android.media.tv.interactive.ITvInteractiveAppSession;
import android.net.Uri;
import android.os.Bundle;
@@ -47,6 +48,9 @@
void onRequestCurrentTvInputId();
void onRequestStartRecording(in Uri programUri);
void onRequestStopRecording(in String recordingId);
+ void onSetTvRecordingInfo(in String recordingId, in TvRecordingInfo recordingInfo);
+ void onRequestTvRecordingInfo(in String recordingId);
+ void onRequestTvRecordingInfoList(in int type);
void onRequestSigning(in String id, in String algorithm, in String alias, in byte[] data);
void onAdRequest(in AdRequest request);
}
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java
index b646326..af031dc 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java
@@ -24,6 +24,7 @@
import android.media.tv.AdResponse;
import android.media.tv.BroadcastInfoResponse;
import android.media.tv.TvContentRating;
+import android.media.tv.TvRecordingInfo;
import android.media.tv.TvTrackInfo;
import android.media.tv.interactive.TvInteractiveAppService.Session;
import android.net.Uri;
@@ -85,6 +86,8 @@
private static final int DO_NOTIFY_RECORDING_STARTED = 30;
private static final int DO_NOTIFY_RECORDING_STOPPED = 31;
private static final int DO_NOTIFY_AD_BUFFER_CONSUMED = 32;
+ private static final int DO_SEND_RECORDING_INFO = 33;
+ private static final int DO_SEND_RECORDING_INFO_LIST = 34;
private final HandlerCaller mCaller;
private Session mSessionImpl;
@@ -168,6 +171,14 @@
mSessionImpl.sendCurrentTvInputId((String) msg.obj);
break;
}
+ case DO_SEND_RECORDING_INFO: {
+ mSessionImpl.sendTvRecordingInfo((TvRecordingInfo) msg.obj);
+ break;
+ }
+ case DO_SEND_RECORDING_INFO_LIST: {
+ mSessionImpl.sendTvRecordingInfoList((List<TvRecordingInfo>) msg.obj);
+ break;
+ }
case DO_NOTIFY_RECORDING_STARTED: {
mSessionImpl.notifyRecordingStarted((String) msg.obj);
break;
@@ -338,6 +349,18 @@
}
@Override
+ public void sendTvRecordingInfo(@Nullable TvRecordingInfo recordingInfo) {
+ mCaller.executeOrSendMessage(
+ mCaller.obtainMessageO(DO_SEND_RECORDING_INFO, recordingInfo));
+ }
+
+ @Override
+ public void sendTvRecordingInfoList(@Nullable List<TvRecordingInfo> recordingInfoList) {
+ mCaller.executeOrSendMessage(
+ mCaller.obtainMessageO(DO_SEND_RECORDING_INFO_LIST, recordingInfoList));
+ }
+
+ @Override
public void sendSigningResult(@NonNull String signingId, @NonNull byte[] result) {
mCaller.executeOrSendMessage(
mCaller.obtainMessageOO(DO_SEND_SIGNING_RESULT, signingId, result));
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
index c57efc8..fa60b66 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
@@ -30,6 +30,7 @@
import android.media.tv.BroadcastInfoResponse;
import android.media.tv.TvContentRating;
import android.media.tv.TvInputManager;
+import android.media.tv.TvRecordingInfo;
import android.media.tv.TvTrackInfo;
import android.net.Uri;
import android.os.Bundle;
@@ -512,6 +513,43 @@
}
@Override
+ public void onSetTvRecordingInfo(String recordingId, TvRecordingInfo recordingInfo,
+ int seq) {
+ synchronized (mSessionCallbackRecordMap) {
+ SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+ if (record == null) {
+ Log.e(TAG, "Callback not found for seq " + seq);
+ return;
+ }
+ record.postSetTvRecordingInfo(recordingId, recordingInfo);
+ }
+ }
+
+ @Override
+ public void onRequestTvRecordingInfo(String recordingId, int seq) {
+ synchronized (mSessionCallbackRecordMap) {
+ SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+ if (record == null) {
+ Log.e(TAG, "Callback not found for seq " + seq);
+ return;
+ }
+ record.postRequestTvRecordingInfo(recordingId);
+ }
+ }
+
+ @Override
+ public void onRequestTvRecordingInfoList(int type, int seq) {
+ synchronized (mSessionCallbackRecordMap) {
+ SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+ if (record == null) {
+ Log.e(TAG, "Callback not found for seq " + seq);
+ return;
+ }
+ record.postRequestTvRecordingInfoList(type);
+ }
+ }
+
+ @Override
public void onRequestSigning(
String id, String algorithm, String alias, byte[] data, int seq) {
synchronized (mSessionCallbackRecordMap) {
@@ -1072,6 +1110,30 @@
}
}
+ void sendTvRecordingInfo(@Nullable TvRecordingInfo recordingInfo) {
+ if (mToken == null) {
+ Log.w(TAG, "The session has been already released");
+ return;
+ }
+ try {
+ mService.sendTvRecordingInfo(mToken, recordingInfo, mUserId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ void sendTvRecordingInfoList(@Nullable List<TvRecordingInfo> recordingInfoList) {
+ if (mToken == null) {
+ Log.w(TAG, "The session has been already released");
+ return;
+ }
+ try {
+ mService.sendTvRecordingInfoList(mToken, recordingInfoList, mUserId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
void notifyRecordingStarted(String recordingId) {
if (mToken == null) {
Log.w(TAG, "The session has been already released");
@@ -1799,6 +1861,33 @@
});
}
+ void postRequestTvRecordingInfo(String recordingId) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mSessionCallback.onRequestTvRecordingInfo(mSession, recordingId);
+ }
+ });
+ }
+
+ void postRequestTvRecordingInfoList(int type) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mSessionCallback.onRequestTvRecordingInfoList(mSession, type);
+ }
+ });
+ }
+
+ void postSetTvRecordingInfo(String recordingId, TvRecordingInfo recordingInfo) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mSessionCallback.onSetTvRecordingInfo(mSession, recordingId, recordingInfo);
+ }
+ });
+ }
+
void postAdRequest(final AdRequest request) {
mHandler.post(new Runnable() {
@Override
@@ -1973,6 +2062,40 @@
/**
* This is called when
+ * {@link TvInteractiveAppService.Session#setTvRecordingInfo(String, TvRecordingInfo)} is
+ * called.
+ *
+ * @param session A {@link TvInteractiveAppService.Session} associated with this callback.
+ * @param recordingId The recordingId of the recording which will have the info set.
+ * @param recordingInfo The recording info to set to the recording.
+ */
+ public void onSetTvRecordingInfo(Session session, String recordingId,
+ TvRecordingInfo recordingInfo) {
+ }
+
+ /**
+ * This is called when {@link TvInteractiveAppService.Session#requestTvRecordingInfo} is
+ * called.
+ *
+ * @param session A {@link TvInteractiveAppService.Session} associated with this callback.
+ * @param recordingId The recordingId of the recording to be stopped.
+ */
+ public void onRequestTvRecordingInfo(Session session, String recordingId) {
+ }
+
+ /**
+ * This is called when {@link TvInteractiveAppService.Session#requestTvRecordingInfoList} is
+ * called.
+ *
+ * @param session A {@link TvInteractiveAppService.Session} associated with this callback.
+ * @param type The type of recordings to return
+ */
+ public void onRequestTvRecordingInfoList(Session session,
+ @TvRecordingInfo.TvRecordingListType int type) {
+ }
+
+ /**
+ * This is called when
* {@link TvInteractiveAppService.Session#requestSigning(String, String, String, byte[])} is
* called.
*
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppService.java b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
index 4ed7ca5..1fa0aaa 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppService.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
@@ -38,6 +38,7 @@
import android.media.tv.TvContentRating;
import android.media.tv.TvInputInfo;
import android.media.tv.TvInputManager;
+import android.media.tv.TvRecordingInfo;
import android.media.tv.TvTrackInfo;
import android.media.tv.TvView;
import android.media.tv.interactive.TvInteractiveAppView.TvInteractiveAppCallback;
@@ -456,6 +457,24 @@
}
/**
+ * Receives requested recording info.
+ *
+ * @param recordingInfo The requested recording info. Null if recording not found.
+ * @hide
+ */
+ public void onTvRecordingInfo(@Nullable TvRecordingInfo recordingInfo) {
+ }
+
+ /**
+ * Receives requested recording info.
+ *
+ * @param recordingInfoList The requested recording info list. Null if recording not found.
+ * @hide
+ */
+ public void onTvRecordingInfoList(@Nullable List<TvRecordingInfo> recordingInfoList) {
+ }
+
+ /**
* Receives started recording's ID.
*
* @param recordingId The ID of the recording started. The TV app should provide and
@@ -988,6 +1007,71 @@
}
/**
+ * Sets the recording info for the specified recording
+ *
+ * @hide
+ */
+ @CallSuper
+ public void setTvRecordingInfo(@NonNull String recordingId,
+ @NonNull TvRecordingInfo recordingInfo) {
+ executeOrPostRunnableOnMainThread(() -> {
+ try {
+ if (DEBUG) {
+ Log.d(TAG, "setTvRecordingInfo");
+ }
+ if (mSessionCallback != null) {
+ mSessionCallback.onSetTvRecordingInfo(recordingId, recordingInfo);
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "error in setTvRecordingInfo", e);
+ }
+ });
+ }
+
+ /**
+ * Gets the recording info for the specified recording
+ *
+ * @hide
+ */
+ @CallSuper
+ public void requestTvRecordingInfo(@NonNull String recordingId) {
+ executeOrPostRunnableOnMainThread(() -> {
+ try {
+ if (DEBUG) {
+ Log.d(TAG, "requestTvRecordingInfo");
+ }
+ if (mSessionCallback != null) {
+ mSessionCallback.onRequestTvRecordingInfo(recordingId);
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "error in requestTvRecordingInfo", e);
+ }
+ });
+ }
+
+ /**
+ * Gets the recording info list for the specified recording type
+ *
+ * @hide
+ */
+ @CallSuper
+ public void requestTvRecordingInfoList(@NonNull @TvRecordingInfo.TvRecordingListType
+ int type) {
+ executeOrPostRunnableOnMainThread(() -> {
+ try {
+ if (DEBUG) {
+ Log.d(TAG, "requestTvRecordingInfoList");
+ }
+ if (mSessionCallback != null) {
+ mSessionCallback.onRequestTvRecordingInfoList(type);
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "error in requestTvRecordingInfoList", e);
+ }
+ });
+ }
+
+ /**
* Requests signing of the given data.
*
* <p>This is used when the corresponding server of the broadcast-independent interactive
@@ -1097,6 +1181,14 @@
onCurrentTvInputId(inputId);
}
+ void sendTvRecordingInfo(@Nullable TvRecordingInfo recordingInfo) {
+ onTvRecordingInfo(recordingInfo);
+ }
+
+ void sendTvRecordingInfoList(@Nullable List<TvRecordingInfo> recordingInfoList) {
+ onTvRecordingInfoList(recordingInfoList);
+ }
+
void sendSigningResult(String signingId, byte[] result) {
onSigningResult(signingId, result);
}
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppView.java b/media/java/android/media/tv/interactive/TvInteractiveAppView.java
index 1177688..6777d1a 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppView.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppView.java
@@ -26,6 +26,7 @@
import android.graphics.Rect;
import android.graphics.RectF;
import android.media.tv.TvInputManager;
+import android.media.tv.TvRecordingInfo;
import android.media.tv.TvTrackInfo;
import android.media.tv.TvView;
import android.media.tv.interactive.TvInteractiveAppManager.Session;
@@ -581,6 +582,36 @@
}
/**
+ * Sends the requested {@link android.media.tv.TvRecordingInfo}.
+ *
+ * @param recordingInfo The recording info requested {@code null} if no recording found.
+ * @hide
+ */
+ public void sendTvRecordingInfo(@Nullable TvRecordingInfo recordingInfo) {
+ if (DEBUG) {
+ Log.d(TAG, "sendTvRecordingInfo");
+ }
+ if (mSession != null) {
+ mSession.sendTvRecordingInfo(recordingInfo);
+ }
+ }
+
+ /**
+ * Sends the requested {@link android.media.tv.TvRecordingInfo}.
+ *
+ * @param recordingInfoList The list of recording info requested.
+ * @hide
+ */
+ public void sendTvRecordingInfoList(@Nullable List<TvRecordingInfo> recordingInfoList) {
+ if (DEBUG) {
+ Log.d(TAG, "sendTvRecordingInfoList");
+ }
+ if (mSession != null) {
+ mSession.sendTvRecordingInfoList(recordingInfoList);
+ }
+ }
+
+ /**
* Alerts the TV interactive app that a recording has been started.
*
* @param recordingId The ID of the recording started. This ID is created and maintained by the
diff --git a/media/java/android/media/tv/tuner/filter/Filter.java b/media/java/android/media/tv/tuner/filter/Filter.java
index 8568c43..7e9443b 100644
--- a/media/java/android/media/tv/tuner/filter/Filter.java
+++ b/media/java/android/media/tv/tuner/filter/Filter.java
@@ -154,7 +154,8 @@
/** @hide */
@IntDef(prefix = "STATUS_",
- value = {STATUS_DATA_READY, STATUS_LOW_WATER, STATUS_HIGH_WATER, STATUS_OVERFLOW})
+ value = {STATUS_DATA_READY, STATUS_LOW_WATER, STATUS_HIGH_WATER, STATUS_OVERFLOW,
+ STATUS_NO_DATA})
@Retention(RetentionPolicy.SOURCE)
public @interface Status {}
@@ -183,6 +184,10 @@
* discarded.
*/
public static final int STATUS_OVERFLOW = DemuxFilterStatus.OVERFLOW;
+ /**
+ * The status of a filter that the filter buffer is empty and no filtered data is coming.
+ */
+ public static final int STATUS_NO_DATA = DemuxFilterStatus.NO_DATA;
/** @hide */
@IntDef(prefix = "SCRAMBLING_STATUS_",
diff --git a/media/tests/AudioPolicyTest/Android.bp b/media/tests/AudioPolicyTest/Android.bp
index 7963ff2..63292ce 100644
--- a/media/tests/AudioPolicyTest/Android.bp
+++ b/media/tests/AudioPolicyTest/Android.bp
@@ -20,4 +20,5 @@
platform_apis: true,
certificate: "platform",
resource_dirs: ["res"],
+ test_suites: ["device-tests"],
}
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/AudioManagerUnitTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/AudioManagerUnitTest.java
new file mode 100644
index 0000000..76543f4
--- /dev/null
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/AudioManagerUnitTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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.mediaframeworktest.unit;
+
+
+import static android.companion.virtual.VirtualDeviceManager.DEVICE_ID_DEFAULT;
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM;
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO;
+import static android.media.AudioManager.FX_KEY_CLICK;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+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.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import android.companion.virtual.VirtualDeviceManager;
+import android.content.Context;
+import android.media.AudioManager;
+import android.test.mock.MockContext;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class AudioManagerUnitTest {
+ private static final int TEST_VIRTUAL_DEVICE_ID = 42;
+
+ @Test
+ public void testAudioManager_playSoundWithDefaultDeviceContext() {
+ VirtualDeviceManager mockVdm = getMockVirtualDeviceManager(TEST_VIRTUAL_DEVICE_ID,
+ DEVICE_POLICY_CUSTOM);
+ Context defaultDeviceContext = getVirtualDeviceMockContext(DEVICE_ID_DEFAULT, /*vdm=*/
+ mockVdm);
+ AudioManager audioManager = new AudioManager(defaultDeviceContext);
+
+ audioManager.playSoundEffect(FX_KEY_CLICK);
+
+ // We expect no interactions with VDM when running on default device.
+ verifyZeroInteractions(mockVdm);
+ }
+
+ @Test
+ public void testAudioManager_playSoundWithVirtualDeviceContextDefaultPolicy() {
+ VirtualDeviceManager mockVdm = getMockVirtualDeviceManager(TEST_VIRTUAL_DEVICE_ID,
+ DEVICE_POLICY_DEFAULT);
+ Context defaultDeviceContext = getVirtualDeviceMockContext(TEST_VIRTUAL_DEVICE_ID, /*vdm=*/
+ mockVdm);
+ AudioManager audioManager = new AudioManager(defaultDeviceContext);
+
+ audioManager.playSoundEffect(FX_KEY_CLICK);
+
+ // We expect playback not to be delegated to VDM because of default device policy for audio.
+ verify(mockVdm, never()).playSoundEffect(anyInt(), anyInt());
+ }
+
+ @Test
+ public void testAudioManager_playSoundWithVirtualDeviceContextCustomPolicy() {
+ VirtualDeviceManager mockVdm = getMockVirtualDeviceManager(TEST_VIRTUAL_DEVICE_ID,
+ DEVICE_POLICY_CUSTOM);
+ Context defaultDeviceContext = getVirtualDeviceMockContext(TEST_VIRTUAL_DEVICE_ID, /*vdm=*/
+ mockVdm);
+ AudioManager audioManager = new AudioManager(defaultDeviceContext);
+
+ audioManager.playSoundEffect(FX_KEY_CLICK);
+
+ // We expect playback to be delegated to VDM because of custom device policy for audio.
+ verify(mockVdm, times(1)).playSoundEffect(TEST_VIRTUAL_DEVICE_ID, FX_KEY_CLICK);
+ }
+
+ private static Context getVirtualDeviceMockContext(int deviceId, VirtualDeviceManager vdm) {
+ MockContext mockContext = mock(MockContext.class);
+ when(mockContext.getDeviceId()).thenReturn(deviceId);
+ when(mockContext.getSystemService(VirtualDeviceManager.class)).thenReturn(vdm);
+ return mockContext;
+ }
+
+ private static VirtualDeviceManager getMockVirtualDeviceManager(
+ int deviceId, int audioDevicePolicy) {
+ VirtualDeviceManager vdmMock = mock(VirtualDeviceManager.class);
+ when(vdmMock.getDevicePolicy(anyInt(), anyInt())).thenReturn(DEVICE_POLICY_DEFAULT);
+ when(vdmMock.getDevicePolicy(deviceId, POLICY_TYPE_AUDIO)).thenReturn(audioDevicePolicy);
+ return vdmMock;
+ }
+}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
index 6a6a3c5..b7fb294 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
@@ -214,17 +214,21 @@
GetCredentialProviderData.Builder("io.enpass.app")
.setCredentialEntries(
listOf<Entry>(
+ newGetEntry(
+ "key1", "subkey-1", TYPE_PASSWORD_CREDENTIAL, "Password",
+ "elisa.family@outlook.com", null, 3L
+ ),
newGetEntry(
"key1", "subkey-1", TYPE_PUBLIC_KEY_CREDENTIAL, "Passkey",
- "elisa.bakery@gmail.com", "Elisa Beckett", 300L
+ "elisa.bakery@gmail.com", "Elisa Beckett", 0L
),
newGetEntry(
"key1", "subkey-2", TYPE_PASSWORD_CREDENTIAL, "Password",
- "elisa.bakery@gmail.com", null, 300L
+ "elisa.bakery@gmail.com", null, 10L
),
newGetEntry(
- "key1", "subkey-3", TYPE_PASSWORD_CREDENTIAL, "Password",
- "elisa.family@outlook.com", null, 100L
+ "key1", "subkey-3", TYPE_PUBLIC_KEY_CREDENTIAL, "Passkey",
+ "elisa.family@outlook.com", "Elisa Beckett", 1L
),
)
).setAuthenticationEntry(
@@ -247,12 +251,12 @@
.setCredentialEntries(
listOf<Entry>(
newGetEntry(
- "key1", "subkey-1", TYPE_PASSWORD_CREDENTIAL, "Password",
- "elisa.family@outlook.com", null, 600L
+ "key1", "subkey-2", TYPE_PASSWORD_CREDENTIAL, "Password",
+ "elisa.family@outlook.com", null, 4L
),
newGetEntry(
- "key1", "subkey-2", TYPE_PUBLIC_KEY_CREDENTIAL, "Passkey",
- "elisa.family@outlook.com", null, 100L
+ "key1", "subkey-3", TYPE_PASSWORD_CREDENTIAL, "Password",
+ "elisa.work@outlook.com", null, 11L
),
)
).setAuthenticationEntry(
@@ -467,7 +471,9 @@
private fun testGetRequestInfo(): RequestInfo {
return RequestInfo.newGetRequestInfo(
Binder(),
- GetCredentialRequest.Builder()
+ GetCredentialRequest.Builder(
+ Bundle()
+ )
.addGetCredentialOption(
GetCredentialOption(
TYPE_PUBLIC_KEY_CREDENTIAL, Bundle(), Bundle(), /*requireSystemProvider=*/ false)
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
index 5e7f1e0..ac0db5a 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
@@ -146,13 +146,19 @@
textAlign = TextAlign.Center,
style = MaterialTheme.typography.headlineSmall,
text = stringResource(
- if (sortedUserNameToCredentialEntryList.size == 1) {
- if (sortedUserNameToCredentialEntryList.first().sortedCredentialEntryList
- .first().credentialType
+ if (sortedUserNameToCredentialEntryList
+ .size == 1 && authenticationEntryList.isEmpty()
+ ) {
+ if (sortedUserNameToCredentialEntryList.first()
+ .sortedCredentialEntryList.first().credentialType
== PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL
- )
- R.string.get_dialog_title_use_passkey_for
+ ) R.string.get_dialog_title_use_passkey_for
else R.string.get_dialog_title_use_sign_in_for
+ } else if (
+ sortedUserNameToCredentialEntryList
+ .isEmpty() && authenticationEntryList.size == 1
+ ) {
+ R.string.get_dialog_title_use_sign_in_for
} else R.string.get_dialog_title_choose_sign_in_for,
requestDisplayInfo.appDomainName
),
@@ -164,20 +170,46 @@
.padding(horizontal = 24.dp)
.align(alignment = Alignment.CenterHorizontally)
) {
+ val usernameForCredentialSize = sortedUserNameToCredentialEntryList
+ .size
+ val authenticationEntrySize = authenticationEntryList.size
LazyColumn(
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
- items(sortedUserNameToCredentialEntryList) {
- CredentialEntryRow(
- credentialEntryInfo = it.sortedCredentialEntryList.first(),
- onEntrySelected = onEntrySelected,
- )
- }
- items(authenticationEntryList) {
- AuthenticationEntryRow(
- authenticationEntryInfo = it,
- onEntrySelected = onEntrySelected,
- )
+ // Show max 4 entries in this primary page
+ if (usernameForCredentialSize + authenticationEntrySize <= 4) {
+ items(sortedUserNameToCredentialEntryList) {
+ CredentialEntryRow(
+ credentialEntryInfo = it.sortedCredentialEntryList.first(),
+ onEntrySelected = onEntrySelected,
+ )
+ }
+ items(authenticationEntryList) {
+ AuthenticationEntryRow(
+ authenticationEntryInfo = it,
+ onEntrySelected = onEntrySelected,
+ )
+ }
+ } else if (usernameForCredentialSize < 4) {
+ items(sortedUserNameToCredentialEntryList) {
+ CredentialEntryRow(
+ credentialEntryInfo = it.sortedCredentialEntryList.first(),
+ onEntrySelected = onEntrySelected,
+ )
+ }
+ items(authenticationEntryList.take(4 - usernameForCredentialSize)) {
+ AuthenticationEntryRow(
+ authenticationEntryInfo = it,
+ onEntrySelected = onEntrySelected,
+ )
+ }
+ } else {
+ items(sortedUserNameToCredentialEntryList.take(4)) {
+ CredentialEntryRow(
+ credentialEntryInfo = it.sortedCredentialEntryList.first(),
+ onEntrySelected = onEntrySelected,
+ )
+ }
}
}
}
@@ -257,7 +289,7 @@
)
}
// Locked password manager
- if (!authenticationEntryList.isEmpty()) {
+ if (authenticationEntryList.isNotEmpty()) {
item {
LockedCredentials(
authenticationEntryList = authenticationEntryList,
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt
index c182397..294e468 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt
@@ -182,7 +182,7 @@
Preconditions.checkState(remoteEntryList.size <= 1)
// Compose sortedUserNameToCredentialEntryList
- val comparator = CredentialEntryInfoComparator()
+ val comparator = CredentialEntryInfoComparatorByTypeThenTimestamp()
// Sort per username
userNameToCredentialEntryMap.values.forEach {
it.sortWith(comparator)
@@ -191,7 +191,7 @@
val sortedUserNameToCredentialEntryList = userNameToCredentialEntryMap.map {
PerUserNameCredentialEntryList(it.key, it.value)
}.sortedWith(
- compareBy(comparator) { it.sortedCredentialEntryList.first() }
+ compareByDescending{ it.sortedCredentialEntryList.first().lastUsedTimeMillis }
)
return ProviderDisplayInfo(
@@ -219,7 +219,7 @@
GetScreenState.REMOTE_ONLY else GetScreenState.PRIMARY_SELECTION
}
-internal class CredentialEntryInfoComparator : Comparator<CredentialEntryInfo> {
+internal class CredentialEntryInfoComparatorByTypeThenTimestamp : Comparator<CredentialEntryInfo> {
override fun compare(p0: CredentialEntryInfo, p1: CredentialEntryInfo): Int {
// First prefer passkey type for its security benefits
if (p0.credentialType != p1.credentialType) {
diff --git a/packages/InputDevices/res/xml/keyboard_layouts.xml b/packages/InputDevices/res/xml/keyboard_layouts.xml
index 976a279..88bb30b 100644
--- a/packages/InputDevices/res/xml/keyboard_layouts.xml
+++ b/packages/InputDevices/res/xml/keyboard_layouts.xml
@@ -1,190 +1,328 @@
<?xml version="1.0" encoding="utf-8"?>
<keyboard-layouts xmlns:android="http://schemas.android.com/apk/res/android">
- <keyboard-layout android:name="keyboard_layout_english_uk"
- android:label="@string/keyboard_layout_english_uk_label"
- android:keyboardLayout="@raw/keyboard_layout_english_uk" />
+ <keyboard-layout
+ android:name="keyboard_layout_english_uk"
+ android:label="@string/keyboard_layout_english_uk_label"
+ android:keyboardLayout="@raw/keyboard_layout_english_uk"
+ android:keyboardLocale="en-Latn-GB"
+ android:keyboardLayoutType="qwerty" />
- <keyboard-layout android:name="keyboard_layout_english_us"
- android:label="@string/keyboard_layout_english_us_label"
- android:keyboardLayout="@raw/keyboard_layout_english_us" />
+ <keyboard-layout
+ android:name="keyboard_layout_english_us"
+ android:label="@string/keyboard_layout_english_us_label"
+ android:keyboardLayout="@raw/keyboard_layout_english_us"
+ android:keyboardLocale="en-Latn,en-Latn-US"
+ android:keyboardLayoutType="qwerty" />
- <keyboard-layout android:name="keyboard_layout_english_us_intl"
- android:label="@string/keyboard_layout_english_us_intl"
- android:keyboardLayout="@raw/keyboard_layout_english_us_intl" />
+ <keyboard-layout
+ android:name="keyboard_layout_english_us_intl"
+ android:label="@string/keyboard_layout_english_us_intl"
+ android:keyboardLayout="@raw/keyboard_layout_english_us_intl"
+ android:keyboardLocale="en-Latn-US"
+ android:keyboardLayoutType="extended" />
- <keyboard-layout android:name="keyboard_layout_english_us_colemak"
- android:label="@string/keyboard_layout_english_us_colemak_label"
- android:keyboardLayout="@raw/keyboard_layout_english_us_colemak" />
+ <keyboard-layout
+ android:name="keyboard_layout_english_us_colemak"
+ android:label="@string/keyboard_layout_english_us_colemak_label"
+ android:keyboardLayout="@raw/keyboard_layout_english_us_colemak"
+ android:keyboardLocale="en-Latn-US"
+ android:keyboardLayoutType="colemak" />
- <keyboard-layout android:name="keyboard_layout_english_us_dvorak"
- android:label="@string/keyboard_layout_english_us_dvorak_label"
- android:keyboardLayout="@raw/keyboard_layout_english_us_dvorak" />
+ <keyboard-layout
+ android:name="keyboard_layout_english_us_dvorak"
+ android:label="@string/keyboard_layout_english_us_dvorak_label"
+ android:keyboardLayout="@raw/keyboard_layout_english_us_dvorak"
+ android:keyboardLocale="en-Latn-US"
+ android:keyboardLayoutType="dvorak" />
- <keyboard-layout android:name="keyboard_layout_english_us_workman"
- android:label="@string/keyboard_layout_english_us_workman_label"
- android:keyboardLayout="@raw/keyboard_layout_english_us_workman" />
+ <keyboard-layout
+ android:name="keyboard_layout_english_us_workman"
+ android:label="@string/keyboard_layout_english_us_workman_label"
+ android:keyboardLayout="@raw/keyboard_layout_english_us_workman"
+ android:keyboardLocale="en-Latn-US"
+ android:keyboardLayoutType="workman" />
- <keyboard-layout android:name="keyboard_layout_german"
- android:label="@string/keyboard_layout_german_label"
- android:keyboardLayout="@raw/keyboard_layout_german" />
+ <keyboard-layout
+ android:name="keyboard_layout_german"
+ android:label="@string/keyboard_layout_german_label"
+ android:keyboardLayout="@raw/keyboard_layout_german"
+ android:keyboardLocale="de-Latn"
+ android:keyboardLayoutType="qwertz" />
- <keyboard-layout android:name="keyboard_layout_french"
- android:label="@string/keyboard_layout_french_label"
- android:keyboardLayout="@raw/keyboard_layout_french" />
+ <keyboard-layout
+ android:name="keyboard_layout_french"
+ android:label="@string/keyboard_layout_french_label"
+ android:keyboardLayout="@raw/keyboard_layout_french"
+ android:keyboardLocale="fr-Latn-FR"
+ android:keyboardLayoutType="azerty" />
- <keyboard-layout android:name="keyboard_layout_french_ca"
- android:label="@string/keyboard_layout_french_ca_label"
- android:keyboardLayout="@raw/keyboard_layout_french_ca" />
+ <keyboard-layout
+ android:name="keyboard_layout_french_ca"
+ android:label="@string/keyboard_layout_french_ca_label"
+ android:keyboardLayout="@raw/keyboard_layout_french_ca"
+ android:keyboardLocale="fr-Latn-CA"
+ android:keyboardLayoutType="qwerty" />
- <keyboard-layout android:name="keyboard_layout_russian"
- android:label="@string/keyboard_layout_russian_label"
- android:keyboardLayout="@raw/keyboard_layout_russian" />
+ <keyboard-layout
+ android:name="keyboard_layout_russian"
+ android:label="@string/keyboard_layout_russian_label"
+ android:keyboardLayout="@raw/keyboard_layout_russian"
+ android:keyboardLocale="ru-Cyrl" />
- <keyboard-layout android:name="keyboard_layout_russian_mac"
- android:label="@string/keyboard_layout_russian_mac_label"
- android:keyboardLayout="@raw/keyboard_layout_russian_mac" />
+ <keyboard-layout
+ android:name="keyboard_layout_russian_mac"
+ android:label="@string/keyboard_layout_russian_mac_label"
+ android:keyboardLayout="@raw/keyboard_layout_russian_mac"
+ android:keyboardLocale="ru-Cyrl"
+ android:keyboardLayoutType="extended" />
- <keyboard-layout android:name="keyboard_layout_spanish"
- android:label="@string/keyboard_layout_spanish_label"
- android:keyboardLayout="@raw/keyboard_layout_spanish" />
+ <keyboard-layout
+ android:name="keyboard_layout_spanish"
+ android:label="@string/keyboard_layout_spanish_label"
+ android:keyboardLayout="@raw/keyboard_layout_spanish"
+ android:keyboardLocale="es-Latn-ES"
+ android:keyboardLayoutType="qwerty" />
- <keyboard-layout android:name="keyboard_layout_swiss_french"
- android:label="@string/keyboard_layout_swiss_french_label"
- android:keyboardLayout="@raw/keyboard_layout_swiss_french" />
+ <keyboard-layout
+ android:name="keyboard_layout_swiss_french"
+ android:label="@string/keyboard_layout_swiss_french_label"
+ android:keyboardLayout="@raw/keyboard_layout_swiss_french"
+ android:keyboardLocale="fr-Latn-CH"
+ android:keyboardLayoutType="qwertz" />
- <keyboard-layout android:name="keyboard_layout_swiss_german"
- android:label="@string/keyboard_layout_swiss_german_label"
- android:keyboardLayout="@raw/keyboard_layout_swiss_german" />
+ <keyboard-layout
+ android:name="keyboard_layout_swiss_german"
+ android:label="@string/keyboard_layout_swiss_german_label"
+ android:keyboardLayout="@raw/keyboard_layout_swiss_german"
+ android:keyboardLocale="de-Latn-CH"
+ android:keyboardLayoutType="qwertz" />
- <keyboard-layout android:name="keyboard_layout_belgian"
- android:label="@string/keyboard_layout_belgian"
- android:keyboardLayout="@raw/keyboard_layout_belgian" />
+ <keyboard-layout
+ android:name="keyboard_layout_belgian"
+ android:label="@string/keyboard_layout_belgian"
+ android:keyboardLayout="@raw/keyboard_layout_belgian"
+ android:keyboardLocale="fr-Latn-BE"
+ android:keyboardLayoutType="azerty" />
- <keyboard-layout android:name="keyboard_layout_bulgarian"
- android:label="@string/keyboard_layout_bulgarian"
- android:keyboardLayout="@raw/keyboard_layout_bulgarian" />
+ <keyboard-layout
+ android:name="keyboard_layout_bulgarian"
+ android:label="@string/keyboard_layout_bulgarian"
+ android:keyboardLayout="@raw/keyboard_layout_bulgarian"
+ android:keyboardLocale="bg-Cyrl" />
- <keyboard-layout android:name="keyboard_layout_bulgarian_phonetic"
- android:label="@string/keyboard_layout_bulgarian_phonetic"
- android:keyboardLayout="@raw/keyboard_layout_bulgarian_phonetic" />
+ <keyboard-layout
+ android:name="keyboard_layout_bulgarian_phonetic"
+ android:label="@string/keyboard_layout_bulgarian_phonetic"
+ android:keyboardLayout="@raw/keyboard_layout_bulgarian_phonetic"
+ android:keyboardLocale="bg-Cyrl"
+ android:keyboardLayoutType="extended" />
- <keyboard-layout android:name="keyboard_layout_italian"
- android:label="@string/keyboard_layout_italian"
- android:keyboardLayout="@raw/keyboard_layout_italian" />
+ <keyboard-layout
+ android:name="keyboard_layout_italian"
+ android:label="@string/keyboard_layout_italian"
+ android:keyboardLayout="@raw/keyboard_layout_italian"
+ android:keyboardLocale="it-Latn"
+ android:keyboardLayoutType="qwerty" />
- <keyboard-layout android:name="keyboard_layout_danish"
- android:label="@string/keyboard_layout_danish"
- android:keyboardLayout="@raw/keyboard_layout_danish" />
+ <keyboard-layout
+ android:name="keyboard_layout_danish"
+ android:label="@string/keyboard_layout_danish"
+ android:keyboardLayout="@raw/keyboard_layout_danish"
+ android:keyboardLocale="da-Latn"
+ android:keyboardLayoutType="qwerty" />
- <keyboard-layout android:name="keyboard_layout_norwegian"
- android:label="@string/keyboard_layout_norwegian"
- android:keyboardLayout="@raw/keyboard_layout_norwegian" />
+ <keyboard-layout
+ android:name="keyboard_layout_norwegian"
+ android:label="@string/keyboard_layout_norwegian"
+ android:keyboardLayout="@raw/keyboard_layout_norwegian"
+ android:keyboardLocale="nb-Latn"
+ android:keyboardLayoutType="qwerty" />
- <keyboard-layout android:name="keyboard_layout_swedish"
- android:label="@string/keyboard_layout_swedish"
- android:keyboardLayout="@raw/keyboard_layout_swedish" />
+ <keyboard-layout
+ android:name="keyboard_layout_swedish"
+ android:label="@string/keyboard_layout_swedish"
+ android:keyboardLayout="@raw/keyboard_layout_swedish"
+ android:keyboardLocale="sv-Latn"
+ android:keyboardLayoutType="qwerty" />
- <keyboard-layout android:name="keyboard_layout_finnish"
- android:label="@string/keyboard_layout_finnish"
- android:keyboardLayout="@raw/keyboard_layout_finnish" />
+ <keyboard-layout
+ android:name="keyboard_layout_finnish"
+ android:label="@string/keyboard_layout_finnish"
+ android:keyboardLayout="@raw/keyboard_layout_finnish"
+ android:keyboardLocale="fi-Latn"
+ android:keyboardLayoutType="qwerty" />
- <keyboard-layout android:name="keyboard_layout_croatian"
- android:label="@string/keyboard_layout_croatian"
- android:keyboardLayout="@raw/keyboard_layout_croatian_and_slovenian" />
+ <keyboard-layout
+ android:name="keyboard_layout_croatian"
+ android:label="@string/keyboard_layout_croatian"
+ android:keyboardLayout="@raw/keyboard_layout_croatian_and_slovenian"
+ android:keyboardLocale="hr-Latn"
+ android:keyboardLayoutType="qwertz" />
- <keyboard-layout android:name="keyboard_layout_czech"
- android:label="@string/keyboard_layout_czech"
- android:keyboardLayout="@raw/keyboard_layout_czech" />
+ <keyboard-layout
+ android:name="keyboard_layout_czech"
+ android:label="@string/keyboard_layout_czech"
+ android:keyboardLayout="@raw/keyboard_layout_czech"
+ android:keyboardLocale="cs-Latn"
+ android:keyboardLayoutType="qwertz" />
- <keyboard-layout android:name="keyboard_layout_czech_qwerty"
- android:label="@string/keyboard_layout_czech_qwerty"
- android:keyboardLayout="@raw/keyboard_layout_czech_qwerty" />
+ <keyboard-layout
+ android:name="keyboard_layout_czech_qwerty"
+ android:label="@string/keyboard_layout_czech_qwerty"
+ android:keyboardLayout="@raw/keyboard_layout_czech_qwerty"
+ android:keyboardLocale="cs-Latn"
+ android:keyboardLayoutType="qwerty" />
- <keyboard-layout android:name="keyboard_layout_estonian"
- android:label="@string/keyboard_layout_estonian"
- android:keyboardLayout="@raw/keyboard_layout_estonian" />
+ <keyboard-layout
+ android:name="keyboard_layout_estonian"
+ android:label="@string/keyboard_layout_estonian"
+ android:keyboardLayout="@raw/keyboard_layout_estonian"
+ android:keyboardLocale="et-Latn"
+ android:keyboardLayoutType="qwerty" />
- <keyboard-layout android:name="keyboard_layout_hungarian"
- android:label="@string/keyboard_layout_hungarian"
- android:keyboardLayout="@raw/keyboard_layout_hungarian" />
+ <keyboard-layout
+ android:name="keyboard_layout_hungarian"
+ android:label="@string/keyboard_layout_hungarian"
+ android:keyboardLayout="@raw/keyboard_layout_hungarian"
+ android:keyboardLocale="hu-Latn"
+ android:keyboardLayoutType="qwertz" />
- <keyboard-layout android:name="keyboard_layout_icelandic"
- android:label="@string/keyboard_layout_icelandic"
- android:keyboardLayout="@raw/keyboard_layout_icelandic" />
+ <keyboard-layout
+ android:name="keyboard_layout_icelandic"
+ android:label="@string/keyboard_layout_icelandic"
+ android:keyboardLayout="@raw/keyboard_layout_icelandic"
+ android:keyboardLocale="is-Latn"
+ android:keyboardLayoutType="qwerty" />
- <keyboard-layout android:name="keyboard_layout_brazilian"
- android:label="@string/keyboard_layout_brazilian"
- android:keyboardLayout="@raw/keyboard_layout_brazilian" />
+ <keyboard-layout
+ android:name="keyboard_layout_brazilian"
+ android:label="@string/keyboard_layout_brazilian"
+ android:keyboardLayout="@raw/keyboard_layout_brazilian"
+ android:keyboardLocale="pt-Latn-BR"
+ android:keyboardLayoutType="qwerty" />
- <keyboard-layout android:name="keyboard_layout_portuguese"
- android:label="@string/keyboard_layout_portuguese"
- android:keyboardLayout="@raw/keyboard_layout_portuguese" />
+ <keyboard-layout
+ android:name="keyboard_layout_portuguese"
+ android:label="@string/keyboard_layout_portuguese"
+ android:keyboardLayout="@raw/keyboard_layout_portuguese"
+ android:keyboardLocale="pt-Latn-PT"
+ android:keyboardLayoutType="qwerty" />
- <keyboard-layout android:name="keyboard_layout_slovak"
- android:label="@string/keyboard_layout_slovak"
- android:keyboardLayout="@raw/keyboard_layout_slovak" />
+ <keyboard-layout
+ android:name="keyboard_layout_slovak"
+ android:label="@string/keyboard_layout_slovak"
+ android:keyboardLayout="@raw/keyboard_layout_slovak"
+ android:keyboardLocale="sk-Latn"
+ android:keyboardLayoutType="qwerty" />
- <keyboard-layout android:name="keyboard_layout_slovenian"
- android:label="@string/keyboard_layout_slovenian"
- android:keyboardLayout="@raw/keyboard_layout_croatian_and_slovenian" />
+ <keyboard-layout
+ android:name="keyboard_layout_slovenian"
+ android:label="@string/keyboard_layout_slovenian"
+ android:keyboardLayout="@raw/keyboard_layout_croatian_and_slovenian"
+ android:keyboardLocale="sl-Latn"
+ android:keyboardLayoutType="qwertz" />
- <keyboard-layout android:name="keyboard_layout_turkish"
- android:label="@string/keyboard_layout_turkish"
- android:keyboardLayout="@raw/keyboard_layout_turkish" />
+ <keyboard-layout
+ android:name="keyboard_layout_turkish"
+ android:label="@string/keyboard_layout_turkish"
+ android:keyboardLayout="@raw/keyboard_layout_turkish"
+ android:keyboardLocale="tr-Latn"
+ android:keyboardLayoutType="qwerty" />
- <keyboard-layout android:name="keyboard_layout_turkish_f"
- android:label="@string/keyboard_layout_turkish_f"
- android:keyboardLayout="@raw/keyboard_layout_turkish_f" />
+ <keyboard-layout
+ android:name="keyboard_layout_turkish"
+ android:label="@string/keyboard_layout_turkish"
+ android:keyboardLayout="@raw/keyboard_layout_turkish"
+ android:keyboardLocale="tr-Latn"
+ android:keyboardLayoutType="turkish_q" />
- <keyboard-layout android:name="keyboard_layout_ukrainian"
- android:label="@string/keyboard_layout_ukrainian"
- android:keyboardLayout="@raw/keyboard_layout_ukrainian" />
+ <keyboard-layout
+ android:name="keyboard_layout_turkish_f"
+ android:label="@string/keyboard_layout_turkish_f"
+ android:keyboardLayout="@raw/keyboard_layout_turkish_f"
+ android:keyboardLocale="tr-Latn"
+ android:keyboardLayoutType="turkish_f" />
- <keyboard-layout android:name="keyboard_layout_arabic"
- android:label="@string/keyboard_layout_arabic"
- android:keyboardLayout="@raw/keyboard_layout_arabic" />
+ <keyboard-layout
+ android:name="keyboard_layout_ukrainian"
+ android:label="@string/keyboard_layout_ukrainian"
+ android:keyboardLayout="@raw/keyboard_layout_ukrainian"
+ android:keyboardLocale="uk-Cyrl" />
- <keyboard-layout android:name="keyboard_layout_greek"
- android:label="@string/keyboard_layout_greek"
- android:keyboardLayout="@raw/keyboard_layout_greek" />
+ <keyboard-layout
+ android:name="keyboard_layout_arabic"
+ android:label="@string/keyboard_layout_arabic"
+ android:keyboardLayout="@raw/keyboard_layout_arabic"
+ android:keyboardLocale="ar-Arab" />
- <keyboard-layout android:name="keyboard_layout_hebrew"
- android:label="@string/keyboard_layout_hebrew"
- android:keyboardLayout="@raw/keyboard_layout_hebrew" />
+ <keyboard-layout
+ android:name="keyboard_layout_greek"
+ android:label="@string/keyboard_layout_greek"
+ android:keyboardLayout="@raw/keyboard_layout_greek"
+ android:keyboardLocale="el-Grek" />
- <keyboard-layout android:name="keyboard_layout_lithuanian"
- android:label="@string/keyboard_layout_lithuanian"
- android:keyboardLayout="@raw/keyboard_layout_lithuanian" />
+ <keyboard-layout
+ android:name="keyboard_layout_hebrew"
+ android:label="@string/keyboard_layout_hebrew"
+ android:keyboardLayout="@raw/keyboard_layout_hebrew"
+ android:keyboardLocale="iw-Hebr" />
- <keyboard-layout android:name="keyboard_layout_spanish_latin"
- android:label="@string/keyboard_layout_spanish_latin"
- android:keyboardLayout="@raw/keyboard_layout_spanish_latin" />
+ <keyboard-layout
+ android:name="keyboard_layout_lithuanian"
+ android:label="@string/keyboard_layout_lithuanian"
+ android:keyboardLayout="@raw/keyboard_layout_lithuanian"
+ android:keyboardLocale="lt-Latn"
+ android:keyboardLayoutType="qwerty" />
- <keyboard-layout android:name="keyboard_layout_latvian"
- android:label="@string/keyboard_layout_latvian"
- android:keyboardLayout="@raw/keyboard_layout_latvian_qwerty" />
+ <keyboard-layout
+ android:name="keyboard_layout_spanish_latin"
+ android:label="@string/keyboard_layout_spanish_latin"
+ android:keyboardLayout="@raw/keyboard_layout_spanish_latin"
+ android:keyboardLocale="es-Latn-419"
+ android:keyboardLayoutType="qwerty" />
- <keyboard-layout android:name="keyboard_layout_persian"
- android:label="@string/keyboard_layout_persian"
- android:keyboardLayout="@raw/keyboard_layout_persian" />
+ <keyboard-layout
+ android:name="keyboard_layout_latvian"
+ android:label="@string/keyboard_layout_latvian"
+ android:keyboardLayout="@raw/keyboard_layout_latvian_qwerty"
+ android:keyboardLocale="lv-Latn"
+ android:keyboardLayoutType="qwerty" />
- <keyboard-layout android:name="keyboard_layout_azerbaijani"
- android:label="@string/keyboard_layout_azerbaijani"
- android:keyboardLayout="@raw/keyboard_layout_azerbaijani" />
+ <keyboard-layout
+ android:name="keyboard_layout_persian"
+ android:label="@string/keyboard_layout_persian"
+ android:keyboardLayout="@raw/keyboard_layout_persian"
+ android:keyboardLocale="fa-Arab" />
- <keyboard-layout android:name="keyboard_layout_polish"
- android:label="@string/keyboard_layout_polish"
- android:keyboardLayout="@raw/keyboard_layout_polish" />
+ <keyboard-layout
+ android:name="keyboard_layout_azerbaijani"
+ android:label="@string/keyboard_layout_azerbaijani"
+ android:keyboardLayout="@raw/keyboard_layout_azerbaijani"
+ android:keyboardLocale="az-Latn-AZ"
+ android:keyboardLayoutType="qwerty" />
- <keyboard-layout android:name="keyboard_layout_belarusian"
- android:label="@string/keyboard_layout_belarusian"
- android:keyboardLayout="@raw/keyboard_layout_belarusian" />
+ <keyboard-layout
+ android:name="keyboard_layout_polish"
+ android:label="@string/keyboard_layout_polish"
+ android:keyboardLayout="@raw/keyboard_layout_polish"
+ android:keyboardLocale="pl-Latn"
+ android:keyboardLayoutType="qwerty" />
- <keyboard-layout android:name="keyboard_layout_mongolian"
- android:label="@string/keyboard_layout_mongolian"
- android:keyboardLayout="@raw/keyboard_layout_mongolian" />
+ <keyboard-layout
+ android:name="keyboard_layout_belarusian"
+ android:label="@string/keyboard_layout_belarusian"
+ android:keyboardLayout="@raw/keyboard_layout_belarusian"
+ android:keyboardLocale="be-Cyrl" />
- <keyboard-layout android:name="keyboard_layout_georgian"
- android:label="@string/keyboard_layout_georgian"
- android:keyboardLayout="@raw/keyboard_layout_georgian" />
+ <keyboard-layout
+ android:name="keyboard_layout_mongolian"
+ android:label="@string/keyboard_layout_mongolian"
+ android:keyboardLayout="@raw/keyboard_layout_mongolian"
+ android:keyboardLocale="mn-Cyrl" />
+
+ <keyboard-layout
+ android:name="keyboard_layout_georgian"
+ android:label="@string/keyboard_layout_georgian"
+ android:keyboardLayout="@raw/keyboard_layout_georgian"
+ android:keyboardLocale="ka-Geor" />
</keyboard-layouts>
diff --git a/packages/PackageInstaller/Android.bp b/packages/PackageInstaller/Android.bp
index fe640ad..fd982f5 100644
--- a/packages/PackageInstaller/Android.bp
+++ b/packages/PackageInstaller/Android.bp
@@ -39,12 +39,13 @@
certificate: "platform",
privileged: true,
- platform_apis: true,
+ platform_apis: false,
+ sdk_version: "system_current",
rename_resources_package: false,
-
static_libs: [
"xz-java",
"androidx.leanback_leanback",
+ "androidx.annotation_annotation",
],
}
@@ -56,7 +57,8 @@
certificate: "platform",
privileged: true,
- platform_apis: true,
+ platform_apis: false,
+ sdk_version: "system_current",
rename_resources_package: false,
overrides: ["PackageInstaller"],
@@ -75,13 +77,15 @@
certificate: "platform",
privileged: true,
- platform_apis: true,
+ platform_apis: false,
+ sdk_version: "system_current",
rename_resources_package: false,
overrides: ["PackageInstaller"],
static_libs: [
"xz-java",
"androidx.leanback_leanback",
+ "androidx.annotation_annotation",
],
aaptflags: ["--product tv"],
}
diff --git a/packages/PackageInstaller/res/drawable-hdpi/popup_bottom_bright.9.png b/packages/PackageInstaller/res/drawable-hdpi/popup_bottom_bright.9.png
new file mode 100644
index 0000000..6e5fbb5
--- /dev/null
+++ b/packages/PackageInstaller/res/drawable-hdpi/popup_bottom_bright.9.png
Binary files differ
diff --git a/packages/PackageInstaller/res/drawable-hdpi/popup_bottom_dark.9.png b/packages/PackageInstaller/res/drawable-hdpi/popup_bottom_dark.9.png
new file mode 100644
index 0000000..3434b2d
--- /dev/null
+++ b/packages/PackageInstaller/res/drawable-hdpi/popup_bottom_dark.9.png
Binary files differ
diff --git a/packages/PackageInstaller/res/drawable-hdpi/popup_bottom_medium.9.png b/packages/PackageInstaller/res/drawable-hdpi/popup_bottom_medium.9.png
new file mode 100644
index 0000000..673a509
--- /dev/null
+++ b/packages/PackageInstaller/res/drawable-hdpi/popup_bottom_medium.9.png
Binary files differ
diff --git a/packages/PackageInstaller/res/drawable-hdpi/popup_center_bright.9.png b/packages/PackageInstaller/res/drawable-hdpi/popup_center_bright.9.png
new file mode 100644
index 0000000..c2a739c
--- /dev/null
+++ b/packages/PackageInstaller/res/drawable-hdpi/popup_center_bright.9.png
Binary files differ
diff --git a/packages/PackageInstaller/res/drawable-hdpi/popup_center_dark.9.png b/packages/PackageInstaller/res/drawable-hdpi/popup_center_dark.9.png
new file mode 100644
index 0000000..9d2bfb1
--- /dev/null
+++ b/packages/PackageInstaller/res/drawable-hdpi/popup_center_dark.9.png
Binary files differ
diff --git a/packages/PackageInstaller/res/drawable-hdpi/popup_center_medium.9.png b/packages/PackageInstaller/res/drawable-hdpi/popup_center_medium.9.png
new file mode 100644
index 0000000..4375bf2d
--- /dev/null
+++ b/packages/PackageInstaller/res/drawable-hdpi/popup_center_medium.9.png
Binary files differ
diff --git a/packages/PackageInstaller/res/drawable-hdpi/popup_full_bright.9.png b/packages/PackageInstaller/res/drawable-hdpi/popup_full_bright.9.png
new file mode 100644
index 0000000..6b8aa9d
--- /dev/null
+++ b/packages/PackageInstaller/res/drawable-hdpi/popup_full_bright.9.png
Binary files differ
diff --git a/packages/PackageInstaller/res/drawable-hdpi/popup_full_dark.9.png b/packages/PackageInstaller/res/drawable-hdpi/popup_full_dark.9.png
new file mode 100644
index 0000000..2884abe
--- /dev/null
+++ b/packages/PackageInstaller/res/drawable-hdpi/popup_full_dark.9.png
Binary files differ
diff --git a/packages/PackageInstaller/res/drawable-hdpi/popup_top_bright.9.png b/packages/PackageInstaller/res/drawable-hdpi/popup_top_bright.9.png
new file mode 100644
index 0000000..76c35ec
--- /dev/null
+++ b/packages/PackageInstaller/res/drawable-hdpi/popup_top_bright.9.png
Binary files differ
diff --git a/packages/PackageInstaller/res/drawable-hdpi/popup_top_dark.9.png b/packages/PackageInstaller/res/drawable-hdpi/popup_top_dark.9.png
new file mode 100644
index 0000000..f317330
--- /dev/null
+++ b/packages/PackageInstaller/res/drawable-hdpi/popup_top_dark.9.png
Binary files differ
diff --git a/packages/PackageInstaller/res/drawable/ic_dialog_info.png b/packages/PackageInstaller/res/drawable/ic_dialog_info.png
new file mode 100644
index 0000000..efee1ef
--- /dev/null
+++ b/packages/PackageInstaller/res/drawable/ic_dialog_info.png
Binary files differ
diff --git a/packages/PackageInstaller/res/layout-television/alert_dialog_button_bar_leanback.xml b/packages/PackageInstaller/res/layout-television/alert_dialog_button_bar_leanback.xml
new file mode 100644
index 0000000..3ced1db
--- /dev/null
+++ b/packages/PackageInstaller/res/layout-television/alert_dialog_button_bar_leanback.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/buttonPanel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:scrollbarAlwaysDrawVerticalTrack="true"
+ android:scrollIndicators="top|bottom"
+ android:fillViewport="true"
+ style="?android:attr/buttonBarStyle">
+ <com.android.packageinstaller.ButtonBarLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layoutDirection="locale"
+ android:orientation="horizontal"
+ android:gravity="start">
+
+ <Button
+ android:id="@+id/button1"
+ style="?android:attr/buttonBarPositiveButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+
+ <Button
+ android:id="@+id/button2"
+ style="?android:attr/buttonBarNegativeButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+
+ <Space
+ android:id="@+id/spacer"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:visibility="invisible" />
+
+ <Button
+ android:id="@+id/button3"
+ style="?attr/buttonBarNeutralButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+
+ </com.android.packageinstaller.ButtonBarLayout>
+</ScrollView>
diff --git a/packages/PackageInstaller/res/layout-television/alert_dialog_leanback.xml b/packages/PackageInstaller/res/layout-television/alert_dialog_leanback.xml
new file mode 100644
index 0000000..0290624
--- /dev/null
+++ b/packages/PackageInstaller/res/layout-television/alert_dialog_leanback.xml
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<com.android.packageinstaller.AlertDialogLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/parentPanel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="start|top"
+ android:orientation="vertical">
+
+ <include layout="@layout/alert_dialog_title_material" />
+
+ <FrameLayout
+ android:id="@+id/contentPanel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="48dp">
+
+ <ScrollView
+ android:id="@+id/scrollView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:clipToPadding="false">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <Space
+ android:id="@+id/textSpacerNoTitle"
+ android:visibility="gone"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/dialog_padding_top_material" />
+
+ <TextView
+ android:id="@+id/message"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingEnd="?android:attr/dialogPreferredPadding"
+ android:paddingStart="?android:attr/dialogPreferredPadding"
+ style="@android:style/TextAppearance.Material.Subhead" />
+
+ <Space
+ android:id="@+id/textSpacerNoButtons"
+ android:visibility="gone"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/dialog_padding_top_material" />
+ </LinearLayout>
+ </ScrollView>
+ </FrameLayout>
+
+ <FrameLayout
+ android:id="@+id/customPanel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="48dp">
+
+ <FrameLayout
+ android:id="@+id/custom"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+ </FrameLayout>
+
+ <include
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ layout="@layout/alert_dialog_button_bar_leanback" />
+</com.android.packageinstaller.AlertDialogLayout>
diff --git a/packages/PackageInstaller/res/layout-television/alert_dialog_leanback_button_panel_side.xml b/packages/PackageInstaller/res/layout-television/alert_dialog_leanback_button_panel_side.xml
new file mode 100644
index 0000000..f9668dd
--- /dev/null
+++ b/packages/PackageInstaller/res/layout-television/alert_dialog_leanback_button_panel_side.xml
@@ -0,0 +1,126 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 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.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/parentPanel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <LinearLayout
+ android:id="@+id/leftPanel"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <LinearLayout android:id="@+id/topPanel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+ <LinearLayout android:id="@+id/title_template"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_vertical|start"
+ android:paddingStart="16dip"
+ android:paddingEnd="16dip"
+ android:paddingTop="16dip"
+ android:paddingBottom="8dip">
+ <ImageView android:id="@+id/icon"
+ android:layout_width="32dip"
+ android:layout_height="32dip"
+ android:layout_marginEnd="8dip"
+ android:scaleType="fitCenter"
+ android:src="@null" />
+ <com.android.packageinstaller.DialogTitle android:id="@+id/alertTitle"
+ style="?android:attr/windowTitleStyle"
+ android:singleLine="true"
+ android:ellipsize="end"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAlignment="viewStart" />
+ </LinearLayout>
+ <!-- If the client uses a customTitle, it will be added here. -->
+ </LinearLayout>
+
+ <LinearLayout android:id="@+id/contentPanel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:minHeight="64dp">
+ <ScrollView android:id="@+id/scrollView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:clipToPadding="false">
+ <TextView android:id="@+id/message"
+ style="?android:attr/textAppearanceMedium"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingStart="16dip"
+ android:paddingEnd="16dip"
+ android:paddingTop="16dip"
+ android:paddingBottom="16dip" />
+ </ScrollView>
+ </LinearLayout>
+
+ <FrameLayout android:id="@+id/customPanel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:minHeight="64dp">
+ <FrameLayout android:id="@+id/custom"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+ </FrameLayout>
+ </LinearLayout>
+
+ <LinearLayout android:id="@+id/buttonPanel"
+ style="?attr/buttonBarStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:orientation="vertical"
+ android:gravity="end">
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layoutDirection="locale"
+ android:orientation="vertical">
+ <Button android:id="@+id/button3"
+ style="?attr/buttonBarNeutralButtonStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:maxLines="2"
+ android:minHeight="@dimen/alert_dialog_button_bar_height" />
+ <Button android:id="@+id/button2"
+ style="?attr/buttonBarNegativeButtonStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:maxLines="2"
+ android:minHeight="@dimen/alert_dialog_button_bar_height" />
+ <Button android:id="@+id/button1"
+ style="?attr/buttonBarPositiveButtonStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:maxLines="2"
+ android:minHeight="@dimen/alert_dialog_button_bar_height" />
+ </LinearLayout>
+ </LinearLayout>
+</LinearLayout>
diff --git a/packages/PackageInstaller/res/layout/alert_dialog_button_bar_material.xml b/packages/PackageInstaller/res/layout/alert_dialog_button_bar_material.xml
new file mode 100644
index 0000000..e4977e7
--- /dev/null
+++ b/packages/PackageInstaller/res/layout/alert_dialog_button_bar_material.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 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.
+-->
+
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/buttonPanel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:scrollbarAlwaysDrawVerticalTrack="true"
+ android:scrollIndicators="top|bottom"
+ android:fillViewport="true"
+ style="?android:attr/buttonBarStyle">
+ <com.android.packageinstaller.ButtonBarLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layoutDirection="locale"
+ android:orientation="horizontal"
+ android:paddingStart="12dp"
+ android:paddingEnd="12dp"
+ android:paddingTop="4dp"
+ android:paddingBottom="4dp"
+ android:gravity="bottom">
+
+ <Button
+ android:id="@+id/button3"
+ style="?android:attr/buttonBarNeutralButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+
+ <Space
+ android:id="@+id/spacer"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:visibility="invisible" />
+
+ <Button
+ android:id="@+id/button2"
+ style="?android:attr/buttonBarNegativeButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+
+ <Button
+ android:id="@+id/button1"
+ style="?android:attr/buttonBarPositiveButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+ </com.android.packageinstaller.ButtonBarLayout>
+</ScrollView>
diff --git a/packages/PackageInstaller/res/layout/alert_dialog_material.xml b/packages/PackageInstaller/res/layout/alert_dialog_material.xml
new file mode 100644
index 0000000..10e9149
--- /dev/null
+++ b/packages/PackageInstaller/res/layout/alert_dialog_material.xml
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 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.
+-->
+
+<com.android.packageinstaller.AlertDialogLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/parentPanel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="start|top"
+ android:orientation="vertical">
+
+ <include layout="@layout/alert_dialog_title_material" />
+
+ <FrameLayout
+ android:id="@+id/contentPanel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="48dp">
+
+ <ScrollView
+ android:id="@+id/scrollView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:clipToPadding="false">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <Space
+ android:id="@+id/textSpacerNoTitle"
+ android:visibility="gone"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/dialog_padding_top_material" />
+
+ <TextView
+ android:id="@+id/message"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingEnd="?android:attr/dialogPreferredPadding"
+ android:paddingStart="?android:attr/dialogPreferredPadding"
+ style="@android:style/TextAppearance.Material.Subhead" />
+
+ <Space
+ android:id="@+id/textSpacerNoButtons"
+ android:visibility="gone"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/dialog_padding_top_material" />
+ </LinearLayout>
+ </ScrollView>
+ </FrameLayout>
+
+ <FrameLayout
+ android:id="@+id/customPanel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="48dp">
+
+ <FrameLayout
+ android:id="@+id/custom"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+ </FrameLayout>
+
+ <include
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ layout="@layout/alert_dialog_button_bar_material" />
+</com.android.packageinstaller.AlertDialogLayout>
diff --git a/packages/PackageInstaller/res/layout/alert_dialog_progress.xml b/packages/PackageInstaller/res/layout/alert_dialog_progress.xml
new file mode 100644
index 0000000..fe06b65
--- /dev/null
+++ b/packages/PackageInstaller/res/layout/alert_dialog_progress.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 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.
+-->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content" android:layout_height="match_parent">
+ <ProgressBar android:id="@+id/progress"
+ style="?android:attr/progressBarStyleHorizontal"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="12dip"
+ android:layout_marginBottom="1dip"
+ android:layout_marginStart="10dip"
+ android:layout_marginEnd="10dip"
+ android:layout_centerHorizontal="true" />
+ <TextView
+ android:id="@+id/progress_percent"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingBottom="12dip"
+ android:layout_marginStart="10dip"
+ android:layout_marginEnd="10dip"
+ android:layout_alignParentStart="true"
+ android:layout_below="@id/progress"
+ />
+ <TextView
+ android:id="@+id/progress_number"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingBottom="12dip"
+ android:layout_marginStart="10dip"
+ android:layout_marginEnd="10dip"
+ android:layout_alignParentEnd="true"
+ android:layout_below="@id/progress"
+ />
+</RelativeLayout>
diff --git a/packages/PackageInstaller/res/layout/alert_dialog_progress_material.xml b/packages/PackageInstaller/res/layout/alert_dialog_progress_material.xml
new file mode 100644
index 0000000..fb98259
--- /dev/null
+++ b/packages/PackageInstaller/res/layout/alert_dialog_progress_material.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 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.
+-->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:paddingStart="?attr/dialogPreferredPadding"
+ android:paddingTop="@dimen/dialog_padding_top_material"
+ android:paddingEnd="?attr/dialogPreferredPadding"
+ android:paddingBottom="@dimen/dialog_padding_top_material">
+ <ProgressBar
+ android:id="@+id/progress"
+ style="?android:attr/progressBarStyleHorizontal"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true" />
+ <TextView
+ android:id="@+id/progress_percent"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true"
+ android:layout_below="@id/progress" />
+ <TextView
+ android:id="@+id/progress_number"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentEnd="true"
+ android:layout_below="@id/progress" />
+</RelativeLayout>
diff --git a/packages/PackageInstaller/res/layout/alert_dialog_title_material.xml b/packages/PackageInstaller/res/layout/alert_dialog_title_material.xml
new file mode 100644
index 0000000..45d9bb6
--- /dev/null
+++ b/packages/PackageInstaller/res/layout/alert_dialog_title_material.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2015 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/topPanel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <!-- If the client uses a customTitle, it will be added here. -->
+
+ <LinearLayout
+ android:id="@+id/title_template"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_vertical|start"
+ android:paddingStart="?android:attr/dialogPreferredPadding"
+ android:paddingEnd="?android:attr/dialogPreferredPadding"
+ android:paddingTop="@dimen/dialog_padding_top_material">
+
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="32dip"
+ android:layout_height="32dip"
+ android:layout_marginEnd="8dip"
+ android:scaleType="fitCenter"
+ android:src="@null" />
+
+ <com.android.packageinstaller.DialogTitle
+ android:id="@+id/alertTitle"
+ android:singleLine="true"
+ android:ellipsize="end"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAlignment="viewStart"
+ style="?android:attr/windowTitleStyle" />
+ </LinearLayout>
+
+ <Space
+ android:id="@+id/titleDividerNoCustom"
+ android:visibility="gone"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/dialog_title_divider_material" />
+</LinearLayout>
diff --git a/packages/PackageInstaller/res/layout/progress_dialog.xml b/packages/PackageInstaller/res/layout/progress_dialog.xml
new file mode 100644
index 0000000..0d3cd4a
--- /dev/null
+++ b/packages/PackageInstaller/res/layout/progress_dialog.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/* //device/apps/common/res/layout/alert_dialog.xml
+**
+** Copyright 2006, 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.
+*/
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <LinearLayout android:id="@+id/body"
+ android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:baselineAligned="false"
+ android:paddingStart="8dip"
+ android:paddingTop="10dip"
+ android:paddingEnd="8dip"
+ android:paddingBottom="10dip">
+
+ <ProgressBar android:id="@android:id/progress"
+ style="?android:attr/progressBarStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:max="10000"
+ android:layout_marginEnd="12dip" />
+
+ <TextView android:id="@+id/message"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical" />
+ </LinearLayout>
+</FrameLayout>
diff --git a/packages/PackageInstaller/res/layout/progress_dialog_material.xml b/packages/PackageInstaller/res/layout/progress_dialog_material.xml
new file mode 100644
index 0000000..2417965
--- /dev/null
+++ b/packages/PackageInstaller/res/layout/progress_dialog_material.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 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.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:id="@+id/body"
+ android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:baselineAligned="false"
+ android:paddingStart="?attr/dialogPreferredPadding"
+ android:paddingTop="@dimen/dialog_padding_top_material"
+ android:paddingEnd="?attr/dialogPreferredPadding"
+ android:paddingBottom="@dimen/dialog_padding_top_material">
+
+ <ProgressBar
+ android:id="@id/progress"
+ style="?android:attr/progressBarStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:max="10000"
+ android:layout_marginEnd="?attr/dialogPreferredPadding" />
+
+ <TextView
+ android:id="@+id/message"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical" />
+ </LinearLayout>
+</FrameLayout>
diff --git a/packages/PackageInstaller/res/layout/select_dialog_item_material.xml b/packages/PackageInstaller/res/layout/select_dialog_item_material.xml
new file mode 100644
index 0000000..b45edc6
--- /dev/null
+++ b/packages/PackageInstaller/res/layout/select_dialog_item_material.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 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.
+-->
+
+<!--
+ This layout file is used by the AlertDialog when displaying a list of items.
+ This layout file is inflated and used as the TextView to display individual
+ items.
+-->
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@android:id/text1"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeightSmall"
+ android:textAppearance="?android:attr/textAppearanceListItemSmall"
+ android:textColor="?android:attr/textColorAlertDialogListItem"
+ android:gravity="center_vertical"
+ android:paddingStart="?attr/listPreferredItemPaddingStart"
+ android:paddingEnd="?attr/listPreferredItemPaddingEnd"
+ android:ellipsize="marquee" />
diff --git a/packages/PackageInstaller/res/layout/select_dialog_material.xml b/packages/PackageInstaller/res/layout/select_dialog_material.xml
new file mode 100644
index 0000000..125b9b8
--- /dev/null
+++ b/packages/PackageInstaller/res/layout/select_dialog_material.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 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.
+-->
+
+<!--
+ This layout file is used by the AlertDialog when displaying a list of items.
+ This layout file is inflated and used as the ListView to display the items.
+ Assign an ID so its state will be saved/restored.
+-->
+<view class="com.android.packageinstaller.AlertController$RecycleListView"
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@id/select_dialog_listview"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:cacheColorHint="@null"
+ android:divider="?attr/listDividerAlertDialog"
+ android:scrollbars="vertical"
+ android:overScrollMode="ifContentScrolls"
+ android:textAlignment="viewStart"
+ android:clipToPadding="false"/>
+ <!--android:paddingBottomNoButtons="@dimen/dialog_list_padding_bottom_no_buttons"
+ android:paddingTopNoTitle="@dimen/dialog_list_padding_top_no_title"/>-->
diff --git a/packages/PackageInstaller/res/layout/select_dialog_multichoice_material.xml b/packages/PackageInstaller/res/layout/select_dialog_multichoice_material.xml
new file mode 100644
index 0000000..52f709e
--- /dev/null
+++ b/packages/PackageInstaller/res/layout/select_dialog_multichoice_material.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 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.
+-->
+
+<CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@android:id/text1"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?attr/listPreferredItemHeightSmall"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textColor="?attr/textColorAlertDialogListItem"
+ android:gravity="center_vertical"
+ android:paddingStart="@dimen/select_dialog_padding_start_material"
+ android:paddingEnd="?attr/dialogPreferredPadding"
+ android:drawableStart="?android:attr/listChoiceIndicatorMultiple"
+ android:drawablePadding="@dimen/select_dialog_drawable_padding_start_material"
+ android:ellipsize="marquee" />
diff --git a/packages/PackageInstaller/res/layout/select_dialog_singlechoice_material.xml b/packages/PackageInstaller/res/layout/select_dialog_singlechoice_material.xml
new file mode 100644
index 0000000..8345b18
--- /dev/null
+++ b/packages/PackageInstaller/res/layout/select_dialog_singlechoice_material.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 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.
+-->
+
+<CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@android:id/text1"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?attr/listPreferredItemHeightSmall"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textColor="?attr/textColorAlertDialogListItem"
+ android:gravity="center_vertical"
+ android:paddingStart="@dimen/select_dialog_padding_start_material"
+ android:paddingEnd="?attr/dialogPreferredPadding"
+ android:drawableStart="?android:attr/listChoiceIndicatorSingle"
+ android:drawablePadding="@dimen/select_dialog_drawable_padding_start_material"
+ android:ellipsize="marquee" />
diff --git a/packages/PackageInstaller/res/values-night/themes.xml b/packages/PackageInstaller/res/values-night/themes.xml
index 483b0cf..18320f7 100644
--- a/packages/PackageInstaller/res/values-night/themes.xml
+++ b/packages/PackageInstaller/res/values-night/themes.xml
@@ -18,6 +18,8 @@
<resources>
<style name="Theme.AlertDialogActivity"
- parent="@android:style/Theme.DeviceDefault.Dialog.Alert" />
+ parent="@android:style/Theme.DeviceDefault.Dialog.Alert">
+ <item name="alertDialogStyle">@style/AlertDialog</item>
+ </style>
</resources>
diff --git a/packages/PackageInstaller/res/values-television/styles.xml b/packages/PackageInstaller/res/values-television/styles.xml
new file mode 100644
index 0000000..936fff0
--- /dev/null
+++ b/packages/PackageInstaller/res/values-television/styles.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources>
+ <style name="AlertDialog.Leanback" parent="@style/AlertDialog">
+ <item name="buttonPanelSideLayout">@layout/alert_dialog_leanback_button_panel_side</item>
+ <item name="layout">@layout/alert_dialog_leanback</item>
+ </style>
+</resources>
\ No newline at end of file
diff --git a/packages/PackageInstaller/res/values-television/themes.xml b/packages/PackageInstaller/res/values-television/themes.xml
index 5ae4957..1cc6933 100644
--- a/packages/PackageInstaller/res/values-television/themes.xml
+++ b/packages/PackageInstaller/res/values-television/themes.xml
@@ -17,15 +17,20 @@
<resources>
- <style name="Theme.AlertDialogActivity.NoAnimation">
+ <style name="Theme.AlertDialogActivity.NoAnimation"
+ parent="@style/Theme.AlertDialogActivity.NoActionBar">
<item name="android:windowAnimationStyle">@null</item>
</style>
<style name="Theme.AlertDialogActivity"
- parent="@android:style/Theme.DeviceDefault.Light.Dialog.Alert" />
+ parent="@android:style/Theme.DeviceDefault.Light.Dialog.Alert">
+ <item name="alertDialogStyle">@style/AlertDialog.Leanback</item>
+ </style>
<style name="Theme.AlertDialogActivity.NoActionBar"
- parent="@android:style/Theme.DeviceDefault.Light.NoActionBar">
+ parent="@android:style/Theme.Material.Light.NoActionBar">
+ <item name="android:windowActionBar">false</item>
+ <item name="android:windowNoTitle">true</item>
</style>
</resources>
diff --git a/packages/PackageInstaller/res/values/attrs.xml b/packages/PackageInstaller/res/values/attrs.xml
index e220f4c..e3070e2 100644
--- a/packages/PackageInstaller/res/values/attrs.xml
+++ b/packages/PackageInstaller/res/values/attrs.xml
@@ -32,4 +32,49 @@
<attr name="circle_radius_pressed_percent" format="fraction" />
</declare-styleable>
<!-- END: Ported from WearableSupport -->
+ <declare-styleable name="Theme">
+ <attr name="alertDialogCenterButtons" format="boolean" />
+ </declare-styleable>
+ <declare-styleable name="AlertDialog">
+ <attr name="fullDark" format="reference|color" />
+ <attr name="topDark" format="reference|color" />
+ <attr name="centerDark" format="reference|color" />
+ <attr name="bottomDark" format="reference|color" />
+ <attr name="fullBright" format="reference|color" />
+ <attr name="topBright" format="reference|color" />
+ <attr name="centerBright" format="reference|color" />
+ <attr name="bottomBright" format="reference|color" />
+ <attr name="bottomMedium" format="reference|color" />
+ <attr name="centerMedium" format="reference|color" />
+ <attr name="layout" format="reference" />
+ <attr name="buttonPanelSideLayout" format="reference" />
+ <attr name="listLayout" format="reference" />
+ <attr name="multiChoiceItemLayout" format="reference" />
+ <attr name="singleChoiceItemLayout" format="reference" />
+ <attr name="listItemLayout" format="reference" />
+ <attr name="progressLayout" format="reference" />
+ <attr name="horizontalProgressLayout" format="reference" />
+ <!-- @hide Not ready for public use. -->
+ <attr name="showTitle" format="boolean" />
+ <!-- Whether fullDark, etc. should use default values if null. -->
+ <attr name="needsDefaultBackgrounds" format="boolean" />
+ <!-- Workaround until we replace AlertController with custom layout. -->
+ <attr name="controllerType">
+ <!-- The default controller. -->
+ <enum name="normal" value="0" />
+ <!-- Controller for micro specific layout. -->
+ <enum name="micro" value="1" />
+ </attr>
+ <!-- Offset when scrolling to a selection. -->
+ <attr name="selectionScrollOffset" format="dimension" />
+ </declare-styleable>
+ <declare-styleable name="ButtonBarLayout">
+ <!-- Whether to automatically stack the buttons when there is not
+ enough space to lay them out side-by-side. -->
+ <attr name="allowStacking" format="boolean" />
+ </declare-styleable>
+ <declare-styleable name="TextAppearance">
+ <!-- Size of the text. Recommended dimension type for text is "sp" for scaled-pixels (example: 15sp). -->
+ <attr name="textSize" format="dimension" />
+ </declare-styleable>
</resources>
diff --git a/packages/PackageInstaller/res/values/dimens.xml b/packages/PackageInstaller/res/values/dimens.xml
index 112723f..bfea05e 100644
--- a/packages/PackageInstaller/res/values/dimens.xml
+++ b/packages/PackageInstaller/res/values/dimens.xml
@@ -41,4 +41,16 @@
<dimen name="wear_permission_review_pref_padding">8dp</dimen>
<dimen name="wear_permission_review_icon_size">24dp</dimen>
+
+ <!-- Dialog title height -->
+ <dimen name="alert_dialog_title_height">64dip</dimen>
+ <!-- Dialog button bar height -->
+ <dimen name="alert_dialog_button_bar_height">48dip</dimen>
+ <!-- The amount to offset when scrolling to a selection in an AlertDialog -->
+ <dimen name="config_alertDialogSelectionScrollOffset">0dp</dimen>
+ <dimen name="dialog_padding_top_material">18dp</dimen>
+ <dimen name="dialog_title_divider_material">8dp</dimen>
+ <!-- Dialog padding minus control padding, used to fix alignment. -->
+ <dimen name="select_dialog_padding_start_material">20dp</dimen>
+ <dimen name="select_dialog_drawable_padding_start_material">20dp</dimen>
</resources>
diff --git a/packages/PackageInstaller/res/values/integers.xml b/packages/PackageInstaller/res/values/integers.xml
new file mode 100644
index 0000000..22ad3a3
--- /dev/null
+++ b/packages/PackageInstaller/res/values/integers.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<resources>
+ <!-- The alert controller to use for alert dialogs. -->
+ <integer name="config_alertDialogController">0</integer>
+</resources>
\ No newline at end of file
diff --git a/packages/PackageInstaller/res/values/styles.xml b/packages/PackageInstaller/res/values/styles.xml
new file mode 100644
index 0000000..ca797e1
--- /dev/null
+++ b/packages/PackageInstaller/res/values/styles.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<resources>
+ <style name="AlertDialog">
+ <item name="fullDark">@empty</item>
+ <item name="topDark">@empty</item>
+ <item name="centerDark">@empty</item>
+ <item name="bottomDark">@empty</item>
+ <item name="fullBright">@empty</item>
+ <item name="topBright">@empty</item>
+ <item name="centerBright">@empty</item>
+ <item name="bottomBright">@empty</item>
+ <item name="bottomMedium">@empty</item>
+ <item name="centerMedium">@empty</item>
+ <item name="layout">@layout/alert_dialog_material</item>
+ <item name="listLayout">@layout/select_dialog_material</item>
+ <item name="progressLayout">@layout/progress_dialog_material</item>
+ <item name="horizontalProgressLayout">@layout/alert_dialog_progress_material</item>
+ <item name="listItemLayout">@layout/select_dialog_item_material</item>
+ <item name="multiChoiceItemLayout">@layout/select_dialog_multichoice_material</item>
+ <item name="singleChoiceItemLayout">@layout/select_dialog_singlechoice_material</item>
+ <item name="controllerType">@integer/config_alertDialogController</item>
+ <item name="selectionScrollOffset">@dimen/config_alertDialogSelectionScrollOffset</item>
+ <item name="needsDefaultBackgrounds">false</item>
+ </style>
+ <style name="TextAppearance">
+ <item name="textSize">16sp</item>
+ </style>
+</resources>
\ No newline at end of file
diff --git a/packages/PackageInstaller/res/values/themes.xml b/packages/PackageInstaller/res/values/themes.xml
index eecf9a1..9a06229 100644
--- a/packages/PackageInstaller/res/values/themes.xml
+++ b/packages/PackageInstaller/res/values/themes.xml
@@ -23,7 +23,9 @@
</style>
<style name="Theme.AlertDialogActivity"
- parent="@android:style/Theme.DeviceDefault.Light.Dialog.Alert" />
+ parent="@android:style/Theme.DeviceDefault.Light.Dialog.Alert">
+ <item name="alertDialogStyle">@style/AlertDialog</item>
+ </style>
<style name="Theme.AlertDialogActivity.NoActionBar">
<item name="android:windowActionBar">false</item>
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/AlertActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/AlertActivity.java
new file mode 100644
index 0000000..7947400
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/AlertActivity.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2007 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.packageinstaller;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.view.KeyEvent;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+
+/**
+ * An activity that follows the visual style of an AlertDialog.
+ *
+ * @see #mAlert
+ * @see #setupAlert()
+ */
+public abstract class AlertActivity extends Activity implements DialogInterface {
+
+ public AlertActivity() {
+ }
+
+ /**
+ * The model for the alert.
+ *
+ */
+ protected AlertController mAlert;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mAlert = new AlertController(this, this, getWindow());
+ }
+
+ public void cancel() {
+ finish();
+ }
+
+ public void dismiss() {
+ // This is called after the click, since we finish when handling the
+ // click, don't do that again here.
+ if (!isFinishing()) {
+ finish();
+ }
+ }
+
+ @Override
+ public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+ return dispatchPopulateAccessibilityEvent(this, event);
+ }
+
+ public static boolean dispatchPopulateAccessibilityEvent(Activity act,
+ AccessibilityEvent event) {
+ event.setClassName(Dialog.class.getName());
+ event.setPackageName(act.getPackageName());
+
+ ViewGroup.LayoutParams params = act.getWindow().getAttributes();
+ boolean isFullScreen = (params.width == ViewGroup.LayoutParams.MATCH_PARENT) &&
+ (params.height == ViewGroup.LayoutParams.MATCH_PARENT);
+ event.setFullScreen(isFullScreen);
+
+ return false;
+ }
+
+ /**
+ * Sets up the alert, including applying the parameters to the alert model,
+ * and installing the alert's content.
+ *
+ * @see #mAlert
+ */
+ protected void setupAlert() {
+ mAlert.installContent();
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (mAlert.onKeyDown(keyCode, event)) return true;
+ return super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (mAlert.onKeyUp(keyCode, event)) return true;
+ return super.onKeyUp(keyCode, event);
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/AlertController.java b/packages/PackageInstaller/src/com/android/packageinstaller/AlertController.java
new file mode 100644
index 0000000..33f38a6
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/AlertController.java
@@ -0,0 +1,685 @@
+/*
+ * Copyright (C) 2008 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.packageinstaller;
+
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.Message;
+import android.text.TextUtils;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.ViewParent;
+import android.view.ViewStub;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.ScrollView;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+
+import com.android.packageinstaller.R;
+
+import java.lang.ref.WeakReference;
+
+public class AlertController {
+ private final Context mContext;
+ private final DialogInterface mDialogInterface;
+ protected final Window mWindow;
+
+ private CharSequence mTitle;
+ protected CharSequence mMessage;
+ protected ListView mListView;
+ private View mView;
+
+ private int mViewLayoutResId;
+
+ private int mViewSpacingLeft;
+ private int mViewSpacingTop;
+ private int mViewSpacingRight;
+ private int mViewSpacingBottom;
+ private boolean mViewSpacingSpecified = false;
+
+ private Button mButtonPositive;
+ private CharSequence mButtonPositiveText;
+ private Message mButtonPositiveMessage;
+
+ private Button mButtonNegative;
+ private CharSequence mButtonNegativeText;
+ private Message mButtonNegativeMessage;
+
+ private Button mButtonNeutral;
+ private CharSequence mButtonNeutralText;
+ private Message mButtonNeutralMessage;
+
+ protected ScrollView mScrollView;
+
+ private int mIconId = 0;
+ private Drawable mIcon;
+
+ private ImageView mIconView;
+ private TextView mTitleView;
+ protected TextView mMessageView;
+
+ private ListAdapter mAdapter;
+
+ private int mAlertDialogLayout;
+ private int mButtonPanelSideLayout;
+ private int mListLayout;
+ private int mMultiChoiceItemLayout;
+ private int mSingleChoiceItemLayout;
+ private int mListItemLayout;
+
+ private boolean mShowTitle;
+
+ private Handler mHandler;
+
+ private final View.OnClickListener mButtonHandler = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ final Message m;
+ if (v == mButtonPositive && mButtonPositiveMessage != null) {
+ m = Message.obtain(mButtonPositiveMessage);
+ } else if (v == mButtonNegative && mButtonNegativeMessage != null) {
+ m = Message.obtain(mButtonNegativeMessage);
+ } else if (v == mButtonNeutral && mButtonNeutralMessage != null) {
+ m = Message.obtain(mButtonNeutralMessage);
+ } else {
+ m = null;
+ }
+
+ if (m != null) {
+ m.sendToTarget();
+ }
+
+ // Post a message so we dismiss after the above handlers are executed
+ mHandler.obtainMessage(ButtonHandler.MSG_DISMISS_DIALOG, mDialogInterface)
+ .sendToTarget();
+ }
+ };
+
+ private static final class ButtonHandler extends Handler {
+ // Button clicks have Message.what as the BUTTON{1,2,3} constant
+ private static final int MSG_DISMISS_DIALOG = 1;
+
+ private WeakReference<DialogInterface> mDialog;
+
+ public ButtonHandler(DialogInterface dialog) {
+ mDialog = new WeakReference<>(dialog);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+
+ case DialogInterface.BUTTON_POSITIVE:
+ case DialogInterface.BUTTON_NEGATIVE:
+ case DialogInterface.BUTTON_NEUTRAL:
+ ((DialogInterface.OnClickListener) msg.obj).onClick(mDialog.get(), msg.what);
+ break;
+
+ case MSG_DISMISS_DIALOG:
+ ((DialogInterface) msg.obj).dismiss();
+ }
+ }
+ }
+
+ public AlertController(Context context, DialogInterface di, Window window) {
+ mContext = context;
+ mDialogInterface = di;
+ mWindow = window;
+ mHandler = new ButtonHandler(di);
+
+ final TypedArray a = context.obtainStyledAttributes(null,
+ R.styleable.AlertDialog, R.attr.alertDialogStyle, 0);
+
+ mAlertDialogLayout = a.getResourceId(
+ R.styleable.AlertDialog_layout, R.layout.alert_dialog_material);
+ mButtonPanelSideLayout = a.getResourceId(
+ R.styleable.AlertDialog_buttonPanelSideLayout, 0);
+ mListLayout = a.getResourceId(
+ R.styleable.AlertDialog_listLayout, R.layout.select_dialog_material);
+
+ mMultiChoiceItemLayout = a.getResourceId(
+ R.styleable.AlertDialog_multiChoiceItemLayout,
+ R.layout.select_dialog_multichoice_material);
+ mSingleChoiceItemLayout = a.getResourceId(
+ R.styleable.AlertDialog_singleChoiceItemLayout,
+ R.layout.select_dialog_singlechoice_material);
+ mListItemLayout = a.getResourceId(
+ R.styleable.AlertDialog_listItemLayout,
+ R.layout.select_dialog_item_material);
+ mShowTitle = a.getBoolean(R.styleable.AlertDialog_showTitle, true);
+
+ a.recycle();
+
+ /* We use a custom title so never request a window title */
+ window.requestFeature(Window.FEATURE_NO_TITLE);
+ }
+
+ static boolean canTextInput(View v) {
+ if (v.onCheckIsTextEditor()) {
+ return true;
+ }
+
+ if (!(v instanceof ViewGroup)) {
+ return false;
+ }
+
+ ViewGroup vg = (ViewGroup)v;
+ int i = vg.getChildCount();
+ while (i > 0) {
+ i--;
+ v = vg.getChildAt(i);
+ if (canTextInput(v)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public void installContent() {
+ mWindow.setContentView(mAlertDialogLayout);
+ setupView();
+ }
+
+ public void setTitle(CharSequence title) {
+ mTitle = title;
+ if (mTitleView != null) {
+ mTitleView.setText(title);
+ }
+ mWindow.setTitle(title);
+ }
+
+ /**
+ * Set the view resource to display in the dialog.
+ */
+ public void setView(int layoutResId) {
+ mView = null;
+ mViewLayoutResId = layoutResId;
+ mViewSpacingSpecified = false;
+ }
+
+ /**
+ * Sets a click listener or a message to be sent when the button is clicked.
+ * You only need to pass one of {@code listener} or {@code msg}.
+ *
+ * @param whichButton Which button, can be one of
+ * {@link DialogInterface#BUTTON_POSITIVE},
+ * {@link DialogInterface#BUTTON_NEGATIVE}, or
+ * {@link DialogInterface#BUTTON_NEUTRAL}
+ * @param text The text to display in positive button.
+ * @param listener The {@link DialogInterface.OnClickListener} to use.
+ * @param msg The {@link Message} to be sent when clicked.
+ */
+ public void setButton(int whichButton, CharSequence text,
+ DialogInterface.OnClickListener listener, Message msg) {
+
+ if (msg == null && listener != null) {
+ msg = mHandler.obtainMessage(whichButton, listener);
+ }
+
+ switch (whichButton) {
+
+ case DialogInterface.BUTTON_POSITIVE:
+ mButtonPositiveText = text;
+ mButtonPositiveMessage = msg;
+ break;
+
+ case DialogInterface.BUTTON_NEGATIVE:
+ mButtonNegativeText = text;
+ mButtonNegativeMessage = msg;
+ break;
+
+ case DialogInterface.BUTTON_NEUTRAL:
+ mButtonNeutralText = text;
+ mButtonNeutralMessage = msg;
+ break;
+
+ default:
+ throw new IllegalArgumentException("Button does not exist");
+ }
+ }
+
+ /**
+ * Specifies the icon to display next to the alert title.
+ *
+ * @param resId the resource identifier of the drawable to use as the icon,
+ * or 0 for no icon
+ */
+ public void setIcon(int resId) {
+ mIcon = null;
+ mIconId = resId;
+
+ if (mIconView != null) {
+ if (resId != 0) {
+ mIconView.setVisibility(View.VISIBLE);
+ mIconView.setImageResource(mIconId);
+ } else {
+ mIconView.setVisibility(View.GONE);
+ }
+ }
+ }
+
+ /**
+ * Specifies the icon to display next to the alert title.
+ *
+ * @param icon the drawable to use as the icon or null for no icon
+ */
+ public void setIcon(Drawable icon) {
+ mIcon = icon;
+ mIconId = 0;
+
+ if (mIconView != null) {
+ if (icon != null) {
+ mIconView.setVisibility(View.VISIBLE);
+ mIconView.setImageDrawable(icon);
+ } else {
+ mIconView.setVisibility(View.GONE);
+ }
+ }
+ }
+
+ public Button getButton(int whichButton) {
+ switch (whichButton) {
+ case DialogInterface.BUTTON_POSITIVE:
+ return mButtonPositive;
+ case DialogInterface.BUTTON_NEGATIVE:
+ return mButtonNegative;
+ case DialogInterface.BUTTON_NEUTRAL:
+ return mButtonNeutral;
+ default:
+ return null;
+ }
+ }
+
+ @SuppressWarnings({"UnusedDeclaration"})
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ return mScrollView != null && mScrollView.executeKeyEvent(event);
+ }
+
+ @SuppressWarnings({"UnusedDeclaration"})
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ return mScrollView != null && mScrollView.executeKeyEvent(event);
+ }
+
+ /**
+ * Resolves whether a custom or default panel should be used. Removes the
+ * default panel if a custom panel should be used. If the resolved panel is
+ * a view stub, inflates before returning.
+ *
+ * @param customPanel the custom panel
+ * @param defaultPanel the default panel
+ * @return the panel to use
+ */
+ @Nullable
+ private ViewGroup resolvePanel(@Nullable View customPanel, @Nullable View defaultPanel) {
+ if (customPanel == null) {
+ // Inflate the default panel, if needed.
+ if (defaultPanel instanceof ViewStub) {
+ defaultPanel = ((ViewStub) defaultPanel).inflate();
+ }
+
+ return (ViewGroup) defaultPanel;
+ }
+
+ // Remove the default panel entirely.
+ if (defaultPanel != null) {
+ final ViewParent parent = defaultPanel.getParent();
+ if (parent instanceof ViewGroup) {
+ ((ViewGroup) parent).removeView(defaultPanel);
+ }
+ }
+
+ // Inflate the custom panel, if needed.
+ if (customPanel instanceof ViewStub) {
+ customPanel = ((ViewStub) customPanel).inflate();
+ }
+
+ return (ViewGroup) customPanel;
+ }
+
+ private void setupView() {
+ final View parentPanel = mWindow.findViewById(R.id.parentPanel);
+ final View defaultTopPanel = parentPanel.findViewById(R.id.topPanel);
+ final View defaultContentPanel = parentPanel.findViewById(R.id.contentPanel);
+ final View defaultButtonPanel = parentPanel.findViewById(R.id.buttonPanel);
+
+ // Install custom content before setting up the title or buttons so
+ // that we can handle panel overrides.
+ final ViewGroup customPanel = parentPanel.findViewById(R.id.customPanel);
+ setupCustomContent(customPanel);
+
+ final View customTopPanel = customPanel.findViewById(R.id.topPanel);
+ final View customContentPanel = customPanel.findViewById(R.id.contentPanel);
+ final View customButtonPanel = customPanel.findViewById(R.id.buttonPanel);
+
+ // Resolve the correct panels and remove the defaults, if needed.
+ final ViewGroup topPanel = resolvePanel(customTopPanel, defaultTopPanel);
+ final ViewGroup contentPanel = resolvePanel(customContentPanel, defaultContentPanel);
+ final ViewGroup buttonPanel = resolvePanel(customButtonPanel, defaultButtonPanel);
+
+ setupContent(contentPanel);
+ setupButtons(buttonPanel);
+ setupTitle(topPanel);
+
+ final boolean hasCustomPanel = customPanel != null
+ && customPanel.getVisibility() != View.GONE;
+ final boolean hasTopPanel = topPanel != null
+ && topPanel.getVisibility() != View.GONE;
+ final boolean hasButtonPanel = buttonPanel != null
+ && buttonPanel.getVisibility() != View.GONE;
+
+ if (!parentPanel.isInTouchMode()) {
+ final View content = hasCustomPanel ? customPanel : contentPanel;
+ if (!requestFocusForContent(content)) {
+ requestFocusForDefaultButton();
+ }
+ }
+
+ if (hasTopPanel) {
+ // Only clip scrolling content to padding if we have a title.
+ if (mScrollView != null) {
+ mScrollView.setClipToPadding(true);
+ }
+
+ // Only show the divider if we have a title.
+ View divider = null;
+ if (mMessage != null || hasCustomPanel) {
+ divider = topPanel.findViewById(R.id.titleDividerNoCustom);
+ }
+
+ if (divider != null) {
+ divider.setVisibility(View.VISIBLE);
+ }
+ } else {
+ if (contentPanel != null) {
+ final View spacer = contentPanel.findViewById(R.id.textSpacerNoTitle);
+ if (spacer != null) {
+ spacer.setVisibility(View.VISIBLE);
+ }
+ }
+ }
+
+ // Update scroll indicators as needed.
+ if (!hasCustomPanel) {
+ final View content = mScrollView;
+ if (content != null) {
+ final int indicators = (hasTopPanel ? View.SCROLL_INDICATOR_TOP : 0)
+ | (hasButtonPanel ? View.SCROLL_INDICATOR_BOTTOM : 0);
+ content.setScrollIndicators(indicators,
+ View.SCROLL_INDICATOR_TOP | View.SCROLL_INDICATOR_BOTTOM);
+ }
+ }
+
+ final TypedArray a = mContext.obtainStyledAttributes(
+ null, R.styleable.AlertDialog, R.attr.alertDialogStyle, 0);
+ setBackground(a, topPanel, contentPanel, customPanel, buttonPanel,
+ hasTopPanel, hasCustomPanel, hasButtonPanel);
+ a.recycle();
+ }
+
+ private boolean requestFocusForContent(View content) {
+ return content != null && content.requestFocus();
+ }
+
+ private void requestFocusForDefaultButton() {
+ if (mButtonPositive.getVisibility() == View.VISIBLE) {
+ mButtonPositive.requestFocus();
+ } else if (mButtonNegative.getVisibility() == View.VISIBLE) {
+ mButtonNegative.requestFocus();
+ } else if (mButtonNeutral.getVisibility() == View.VISIBLE) {
+ mButtonNeutral.requestFocus();
+ }
+ }
+
+ private void setupCustomContent(ViewGroup customPanel) {
+ final View customView;
+ if (mView != null) {
+ customView = mView;
+ } else if (mViewLayoutResId != 0) {
+ final LayoutInflater inflater = LayoutInflater.from(mContext);
+ customView = inflater.inflate(mViewLayoutResId, customPanel, false);
+ } else {
+ customView = null;
+ }
+
+ final boolean hasCustomView = customView != null;
+ if (!hasCustomView || !canTextInput(customView)) {
+ mWindow.setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM,
+ WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
+ }
+
+ if (hasCustomView) {
+ final FrameLayout custom = mWindow.findViewById(R.id.custom);
+ custom.addView(customView, new LayoutParams(MATCH_PARENT, MATCH_PARENT));
+
+ if (mViewSpacingSpecified) {
+ custom.setPadding(
+ mViewSpacingLeft, mViewSpacingTop, mViewSpacingRight, mViewSpacingBottom);
+ }
+ } else {
+ customPanel.setVisibility(View.GONE);
+ }
+ }
+
+ private void setupTitle(ViewGroup topPanel) {
+ mIconView = mWindow.findViewById(R.id.icon);
+
+ final boolean hasTextTitle = !TextUtils.isEmpty(mTitle);
+ if (hasTextTitle && mShowTitle) {
+ // Display the title if a title is supplied, else hide it.
+ mTitleView = mWindow.findViewById(R.id.alertTitle);
+ mTitleView.setText(mTitle);
+
+ // Do this last so that if the user has supplied any icons we
+ // use them instead of the default ones. If the user has
+ // specified 0 then make it disappear.
+ if (mIconId != 0) {
+ mIconView.setImageResource(mIconId);
+ } else if (mIcon != null) {
+ mIconView.setImageDrawable(mIcon);
+ } else {
+ // Apply the padding from the icon to ensure the title is
+ // aligned correctly.
+ mTitleView.setPadding(mIconView.getPaddingLeft(),
+ mIconView.getPaddingTop(),
+ mIconView.getPaddingRight(),
+ mIconView.getPaddingBottom());
+ mIconView.setVisibility(View.GONE);
+ }
+ } else {
+ // Hide the title template
+ final View titleTemplate = mWindow.findViewById(R.id.title_template);
+ titleTemplate.setVisibility(View.GONE);
+ mIconView.setVisibility(View.GONE);
+ topPanel.setVisibility(View.GONE);
+ }
+ }
+
+ private void setupContent(ViewGroup contentPanel) {
+ mScrollView = contentPanel.findViewById(R.id.scrollView);
+ mScrollView.setFocusable(false);
+
+ // Special case for users that only want to display a String
+ mMessageView = contentPanel.findViewById(R.id.message);
+ if (mMessageView == null) {
+ return;
+ }
+
+ mMessageView.setVisibility(View.GONE);
+ mScrollView.removeView(mMessageView);
+
+ contentPanel.setVisibility(View.GONE);
+ }
+
+ private void setupButtons(ViewGroup buttonPanel) {
+ int BIT_BUTTON_POSITIVE = 1;
+ int BIT_BUTTON_NEGATIVE = 2;
+ int BIT_BUTTON_NEUTRAL = 4;
+ int whichButtons = 0;
+ mButtonPositive = buttonPanel.findViewById(R.id.button1);
+ mButtonPositive.setOnClickListener(mButtonHandler);
+
+ if (TextUtils.isEmpty(mButtonPositiveText)) {
+ mButtonPositive.setVisibility(View.GONE);
+ } else {
+ mButtonPositive.setText(mButtonPositiveText);
+ mButtonPositive.setVisibility(View.VISIBLE);
+ whichButtons = whichButtons | BIT_BUTTON_POSITIVE;
+ }
+
+ mButtonNegative = buttonPanel.findViewById(R.id.button2);
+ mButtonNegative.setOnClickListener(mButtonHandler);
+
+ if (TextUtils.isEmpty(mButtonNegativeText)) {
+ mButtonNegative.setVisibility(View.GONE);
+ } else {
+ mButtonNegative.setText(mButtonNegativeText);
+ mButtonNegative.setVisibility(View.VISIBLE);
+
+ whichButtons = whichButtons | BIT_BUTTON_NEGATIVE;
+ }
+
+ mButtonNeutral = buttonPanel.findViewById(R.id.button3);
+ mButtonNeutral.setOnClickListener(mButtonHandler);
+
+ if (TextUtils.isEmpty(mButtonNeutralText)) {
+ mButtonNeutral.setVisibility(View.GONE);
+ } else {
+ mButtonNeutral.setText(mButtonNeutralText);
+ mButtonNeutral.setVisibility(View.VISIBLE);
+
+ whichButtons = whichButtons | BIT_BUTTON_NEUTRAL;
+ }
+
+ final boolean hasButtons = whichButtons != 0;
+ if (!hasButtons) {
+ buttonPanel.setVisibility(View.GONE);
+ }
+ }
+
+ private void setBackground(TypedArray a, View topPanel, View contentPanel, View customPanel,
+ View buttonPanel, boolean hasTitle, boolean hasCustomView, boolean hasButtons) {
+ int fullDark = 0;
+ int topDark = 0;
+ int centerDark = 0;
+ int bottomDark = 0;
+ int fullBright = 0;
+ int topBright = 0;
+ int centerBright = 0;
+ int bottomBright = 0;
+ int bottomMedium = 0;
+
+ topBright = a.getResourceId(R.styleable.AlertDialog_topBright, topBright);
+ topDark = a.getResourceId(R.styleable.AlertDialog_topDark, topDark);
+ centerBright = a.getResourceId(R.styleable.AlertDialog_centerBright, centerBright);
+ centerDark = a.getResourceId(R.styleable.AlertDialog_centerDark, centerDark);
+
+ /* We now set the background of all of the sections of the alert.
+ * First collect together each section that is being displayed along
+ * with whether it is on a light or dark background, then run through
+ * them setting their backgrounds. This is complicated because we need
+ * to correctly use the full, top, middle, and bottom graphics depending
+ * on how many views they are and where they appear.
+ */
+
+ final View[] views = new View[4];
+ final boolean[] light = new boolean[4];
+ View lastView = null;
+ boolean lastLight = false;
+
+ int pos = 0;
+ if (hasTitle) {
+ views[pos] = topPanel;
+ light[pos] = false;
+ pos++;
+ }
+
+ /* The contentPanel displays either a custom text message or
+ * a ListView. If it's text we should use the dark background
+ * for ListView we should use the light background. PIA does not use
+ * a list view. Hence, we set it to use dark background. If neither
+ * are there the contentPanel will be hidden so set it as null.
+ */
+ views[pos] = contentPanel.getVisibility() == View.GONE ? null : contentPanel;
+ light[pos] = false;
+ pos++;
+
+ if (hasCustomView) {
+ views[pos] = customPanel;
+ light[pos] = false;
+ pos++;
+ }
+
+ if (hasButtons) {
+ views[pos] = buttonPanel;
+ light[pos] = true;
+ }
+
+ boolean setView = false;
+ for (pos = 0; pos < views.length; pos++) {
+ final View v = views[pos];
+ if (v == null) {
+ continue;
+ }
+
+ if (lastView != null) {
+ if (!setView) {
+ lastView.setBackgroundResource(lastLight ? topBright : topDark);
+ } else {
+ lastView.setBackgroundResource(lastLight ? centerBright : centerDark);
+ }
+ setView = true;
+ }
+
+ lastView = v;
+ lastLight = light[pos];
+ }
+
+ if (lastView != null) {
+ if (setView) {
+ bottomBright = a.getResourceId(R.styleable.AlertDialog_bottomBright, bottomBright);
+ bottomMedium = a.getResourceId(R.styleable.AlertDialog_bottomMedium, bottomMedium);
+ bottomDark = a.getResourceId(R.styleable.AlertDialog_bottomDark, bottomDark);
+
+ // ListViews will use the Bright background, but buttons use the
+ // Medium background.
+ lastView.setBackgroundResource(
+ lastLight ? (hasButtons ? bottomMedium : bottomBright) : bottomDark);
+ } else {
+ fullBright = a.getResourceId(R.styleable.AlertDialog_fullBright, fullBright);
+ fullDark = a.getResourceId(R.styleable.AlertDialog_fullDark, fullDark);
+
+ lastView.setBackgroundResource(lastLight ? fullBright : fullDark);
+ }
+ }
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/AlertDialogLayout.java b/packages/PackageInstaller/src/com/android/packageinstaller/AlertDialogLayout.java
new file mode 100644
index 0000000..e22171e
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/AlertDialogLayout.java
@@ -0,0 +1,357 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.packageinstaller;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+
+import androidx.annotation.AttrRes;
+import androidx.annotation.Nullable;
+import androidx.annotation.StyleRes;
+
+import com.android.packageinstaller.R;
+
+/**
+ * Special implementation of linear layout that's capable of laying out alert
+ * dialog components.
+ * <p>
+ * A dialog consists of up to three panels. All panels are optional, and a
+ * dialog may contain only a single panel. The panels are laid out according
+ * to the following guidelines:
+ * <ul>
+ * <li>topPanel: exactly wrap_content</li>
+ * <li>contentPanel OR customPanel: at most fill_parent, first priority for
+ * extra space</li>
+ * <li>buttonPanel: at least minHeight, at most wrap_content, second
+ * priority for extra space</li>
+ * </ul>
+ */
+public class AlertDialogLayout extends LinearLayout {
+
+ public AlertDialogLayout(@Nullable Context context) {
+ super(context);
+ }
+
+ public AlertDialogLayout(@Nullable Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public AlertDialogLayout(@Nullable Context context, @Nullable AttributeSet attrs,
+ @AttrRes int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public AlertDialogLayout(@Nullable Context context, @Nullable AttributeSet attrs,
+ @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ if (!tryOnMeasure(widthMeasureSpec, heightMeasureSpec)) {
+ // Failed to perform custom measurement, let superclass handle it.
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+ }
+
+ private boolean tryOnMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ View topPanel = null;
+ View buttonPanel = null;
+ View middlePanel = null;
+
+ final int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() == View.GONE) {
+ continue;
+ }
+
+ final int id = child.getId();
+ switch (id) {
+ case R.id.topPanel:
+ topPanel = child;
+ break;
+ case R.id.buttonPanel:
+ buttonPanel = child;
+ break;
+ case R.id.contentPanel:
+ case R.id.customPanel:
+ if (middlePanel != null) {
+ // Both the content and custom are visible. Abort!
+ return false;
+ }
+ middlePanel = child;
+ break;
+ default:
+ // Unknown top-level child. Abort!
+ return false;
+ }
+ }
+
+ final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+ final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+
+ int childState = 0;
+ int usedHeight = getPaddingTop() + getPaddingBottom();
+
+ if (topPanel != null) {
+ topPanel.measure(widthMeasureSpec, MeasureSpec.UNSPECIFIED);
+
+ usedHeight += topPanel.getMeasuredHeight();
+ childState = combineMeasuredStates(childState, topPanel.getMeasuredState());
+ }
+
+ int buttonHeight = 0;
+ int buttonWantsHeight = 0;
+ if (buttonPanel != null) {
+ buttonPanel.measure(widthMeasureSpec, MeasureSpec.UNSPECIFIED);
+ buttonHeight = resolveMinimumHeight(buttonPanel);
+ buttonWantsHeight = buttonPanel.getMeasuredHeight() - buttonHeight;
+
+ usedHeight += buttonHeight;
+ childState = combineMeasuredStates(childState, buttonPanel.getMeasuredState());
+ }
+
+ int middleHeight = 0;
+ if (middlePanel != null) {
+ final int childHeightSpec;
+ if (heightMode == MeasureSpec.UNSPECIFIED) {
+ childHeightSpec = MeasureSpec.UNSPECIFIED;
+ } else {
+ childHeightSpec = MeasureSpec.makeMeasureSpec(
+ Math.max(0, heightSize - usedHeight), heightMode);
+ }
+
+ middlePanel.measure(widthMeasureSpec, childHeightSpec);
+ middleHeight = middlePanel.getMeasuredHeight();
+
+ usedHeight += middleHeight;
+ childState = combineMeasuredStates(childState, middlePanel.getMeasuredState());
+ }
+
+ int remainingHeight = heightSize - usedHeight;
+
+ // Time for the "real" button measure pass. If we have remaining space,
+ // make the button pane bigger up to its target height. Otherwise,
+ // just remeasure the button at whatever height it needs.
+ if (buttonPanel != null) {
+ usedHeight -= buttonHeight;
+
+ final int heightToGive = Math.min(remainingHeight, buttonWantsHeight);
+ if (heightToGive > 0) {
+ remainingHeight -= heightToGive;
+ buttonHeight += heightToGive;
+ }
+
+ final int childHeightSpec = MeasureSpec.makeMeasureSpec(
+ buttonHeight, MeasureSpec.EXACTLY);
+ buttonPanel.measure(widthMeasureSpec, childHeightSpec);
+
+ usedHeight += buttonPanel.getMeasuredHeight();
+ childState = combineMeasuredStates(childState, buttonPanel.getMeasuredState());
+ }
+
+ // If we still have remaining space, make the middle pane bigger up
+ // to the maximum height.
+ if (middlePanel != null && remainingHeight > 0) {
+ usedHeight -= middleHeight;
+
+ final int heightToGive = remainingHeight;
+ remainingHeight -= heightToGive;
+ middleHeight += heightToGive;
+
+ // Pass the same height mode as we're using for the dialog itself.
+ // If it's EXACTLY, then the middle pane MUST use the entire
+ // height.
+ final int childHeightSpec = MeasureSpec.makeMeasureSpec(
+ middleHeight, heightMode);
+ middlePanel.measure(widthMeasureSpec, childHeightSpec);
+
+ usedHeight += middlePanel.getMeasuredHeight();
+ childState = combineMeasuredStates(childState, middlePanel.getMeasuredState());
+ }
+
+ // Compute desired width as maximum child width.
+ int maxWidth = 0;
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() != View.GONE) {
+ maxWidth = Math.max(maxWidth, child.getMeasuredWidth());
+ }
+ }
+
+ maxWidth += getPaddingLeft() + getPaddingRight();
+
+ final int widthSizeAndState = resolveSizeAndState(maxWidth, widthMeasureSpec, childState);
+ final int heightSizeAndState = resolveSizeAndState(usedHeight, heightMeasureSpec, 0);
+ setMeasuredDimension(widthSizeAndState, heightSizeAndState);
+
+ // If the children weren't already measured EXACTLY, we need to run
+ // another measure pass to for MATCH_PARENT widths.
+ if (widthMode != MeasureSpec.EXACTLY) {
+ forceUniformWidth(count, heightMeasureSpec);
+ }
+
+ return true;
+ }
+
+ /**
+ * Remeasures child views to exactly match the layout's measured width.
+ *
+ * @param count the number of child views
+ * @param heightMeasureSpec the original height measure spec
+ */
+ private void forceUniformWidth(int count, int heightMeasureSpec) {
+ // Pretend that the linear layout has an exact size.
+ final int uniformMeasureSpec = MeasureSpec.makeMeasureSpec(
+ getMeasuredWidth(), MeasureSpec.EXACTLY);
+
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() != GONE) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (lp.width == LayoutParams.MATCH_PARENT) {
+ // Temporarily force children to reuse their old measured
+ // height.
+ final int oldHeight = lp.height;
+ lp.height = child.getMeasuredHeight();
+
+ // Remeasure with new dimensions.
+ measureChildWithMargins(child, uniformMeasureSpec, 0, heightMeasureSpec, 0);
+ lp.height = oldHeight;
+ }
+ }
+ }
+ }
+
+ /**
+ * Attempts to resolve the minimum height of a view.
+ * <p>
+ * If the view doesn't have a minimum height set and only contains a single
+ * child, attempts to resolve the minimum height of the child view.
+ *
+ * @param v the view whose minimum height to resolve
+ * @return the minimum height
+ */
+ private int resolveMinimumHeight(View v) {
+ final int minHeight = v.getMinimumHeight();
+ if (minHeight > 0) {
+ return minHeight;
+ }
+
+ if (v instanceof ViewGroup) {
+ final ViewGroup vg = (ViewGroup) v;
+ if (vg.getChildCount() == 1) {
+ return resolveMinimumHeight(vg.getChildAt(0));
+ }
+ }
+
+ return 0;
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ final int paddingLeft = getPaddingLeft();
+ final int paddingRight = getPaddingRight();
+ final int paddingTop = getPaddingTop();
+
+ // Where right end of child should go
+ final int width = right - left;
+ final int childRight = width - paddingRight;
+
+ // Space available for child
+ final int childSpace = width - paddingLeft - paddingRight;
+
+ final int totalLength = getMeasuredHeight();
+ final int count = getChildCount();
+ final int gravity = getGravity();
+ final int majorGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
+ final int minorGravity = gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
+
+ int childTop;
+ switch (majorGravity) {
+ case Gravity.BOTTOM:
+ // totalLength contains the padding already
+ childTop = paddingTop + bottom - top - totalLength;
+ break;
+
+ // totalLength contains the padding already
+ case Gravity.CENTER_VERTICAL:
+ childTop = paddingTop + (bottom - top - totalLength) / 2;
+ break;
+
+ case Gravity.TOP:
+ default:
+ childTop = paddingTop;
+ break;
+ }
+
+ final Drawable dividerDrawable = getDividerDrawable();
+ final int dividerHeight = dividerDrawable == null ?
+ 0 : dividerDrawable.getIntrinsicHeight();
+
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child != null && child.getVisibility() != GONE) {
+ final int childWidth = child.getMeasuredWidth();
+ final int childHeight = child.getMeasuredHeight();
+
+ final LayoutParams lp =
+ (LayoutParams) child.getLayoutParams();
+
+ int layoutGravity = lp.gravity;
+ if (layoutGravity < 0) {
+ layoutGravity = minorGravity;
+ }
+ final int layoutDirection = getLayoutDirection();
+ final int absoluteGravity = Gravity.getAbsoluteGravity(
+ layoutGravity, layoutDirection);
+
+ final int childLeft;
+ switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
+ case Gravity.CENTER_HORIZONTAL:
+ childLeft = paddingLeft + ((childSpace - childWidth) / 2)
+ + lp.leftMargin - lp.rightMargin;
+ break;
+
+ case Gravity.RIGHT:
+ childLeft = childRight - childWidth - lp.rightMargin;
+ break;
+
+ case Gravity.LEFT:
+ default:
+ childLeft = paddingLeft + lp.leftMargin;
+ break;
+ }
+
+ childTop += lp.topMargin;
+ setChildFrame(child, childLeft, childTop, childWidth, childHeight);
+ childTop += childHeight + lp.bottomMargin;
+ }
+ }
+ }
+
+ private void setChildFrame(View child, int left, int top, int width, int height) {
+ child.layout(left, top, left + width, top + height);
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/ButtonBarLayout.java b/packages/PackageInstaller/src/com/android/packageinstaller/ButtonBarLayout.java
new file mode 100644
index 0000000..8d478c4
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/ButtonBarLayout.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2015 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.packageinstaller;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.widget.LinearLayout;
+
+import com.android.packageinstaller.R;
+
+/**
+ * An extension of LinearLayout that automatically switches to vertical
+ * orientation when it can't fit its child views horizontally.
+ */
+public class ButtonBarLayout extends LinearLayout {
+ /** Amount of the second button to "peek" above the fold when stacked. */
+ private static final int PEEK_BUTTON_DP = 16;
+
+ /** Whether the current configuration allows stacking. */
+ private boolean mAllowStacking;
+
+ private int mLastWidthSize = -1;
+
+ private int mMinimumHeight = 0;
+
+ public ButtonBarLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ final TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ButtonBarLayout);
+ mAllowStacking = ta.getBoolean(R.styleable.ButtonBarLayout_allowStacking, true);
+ ta.recycle();
+ }
+
+ public void setAllowStacking(boolean allowStacking) {
+ if (mAllowStacking != allowStacking) {
+ mAllowStacking = allowStacking;
+ if (!mAllowStacking && getOrientation() == LinearLayout.VERTICAL) {
+ setStacked(false);
+ }
+ requestLayout();
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+
+ if (mAllowStacking) {
+ if (widthSize > mLastWidthSize && isStacked()) {
+ // We're being measured wider this time, try un-stacking.
+ setStacked(false);
+ }
+
+ mLastWidthSize = widthSize;
+ }
+
+ boolean needsRemeasure = false;
+
+ // If we're not stacked, make sure the measure spec is AT_MOST rather
+ // than EXACTLY. This ensures that we'll still get TOO_SMALL so that we
+ // know to stack the buttons.
+ final int initialWidthMeasureSpec;
+ if (!isStacked() && MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) {
+ initialWidthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.AT_MOST);
+
+ // We'll need to remeasure again to fill excess space.
+ needsRemeasure = true;
+ } else {
+ initialWidthMeasureSpec = widthMeasureSpec;
+ }
+
+ super.onMeasure(initialWidthMeasureSpec, heightMeasureSpec);
+
+ if (mAllowStacking && !isStacked()) {
+ final int measuredWidth = getMeasuredWidthAndState();
+ final int measuredWidthState = measuredWidth & MEASURED_STATE_MASK;
+ if (measuredWidthState == MEASURED_STATE_TOO_SMALL) {
+ setStacked(true);
+
+ // Measure again in the new orientation.
+ needsRemeasure = true;
+ }
+ }
+
+ if (needsRemeasure) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ // Compute minimum height such that, when stacked, some portion of the
+ // second button is visible.
+ int minHeight = 0;
+ final int firstVisible = getNextVisibleChildIndex(0);
+ if (firstVisible >= 0) {
+ final View firstButton = getChildAt(firstVisible);
+ final LayoutParams firstParams = (LayoutParams) firstButton.getLayoutParams();
+ minHeight += getPaddingTop() + firstButton.getMeasuredHeight()
+ + firstParams.topMargin + firstParams.bottomMargin;
+ if (isStacked()) {
+ final int secondVisible = getNextVisibleChildIndex(firstVisible + 1);
+ if (secondVisible >= 0) {
+ minHeight += getChildAt(secondVisible).getPaddingTop()
+ + PEEK_BUTTON_DP * getResources().getDisplayMetrics().density;
+ }
+ } else {
+ minHeight += getPaddingBottom();
+ }
+ }
+
+ if (getMinimumHeight() != minHeight) {
+ setMinimumHeight(minHeight);
+ }
+ }
+
+ private int getNextVisibleChildIndex(int index) {
+ for (int i = index, count = getChildCount(); i < count; i++) {
+ if (getChildAt(i).getVisibility() == View.VISIBLE) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ public int getMinimumHeight() {
+ return Math.max(mMinimumHeight, super.getMinimumHeight());
+ }
+
+ private void setStacked(boolean stacked) {
+ setOrientation(stacked ? LinearLayout.VERTICAL : LinearLayout.HORIZONTAL);
+ setGravity(stacked ? Gravity.END : Gravity.BOTTOM);
+
+ final View spacer = findViewById(R.id.spacer);
+ if (spacer != null) {
+ spacer.setVisibility(stacked ? View.GONE : View.INVISIBLE);
+ }
+
+ // Reverse the child order. This is specific to the Material button
+ // bar's layout XML and will probably not generalize.
+ final int childCount = getChildCount();
+ for (int i = childCount - 2; i >= 0; i--) {
+ bringChildToFront(getChildAt(i));
+ }
+ }
+
+ private boolean isStacked() {
+ return getOrientation() == LinearLayout.VERTICAL;
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/DeleteStagedFileOnResult.java b/packages/PackageInstaller/src/com/android/packageinstaller/DeleteStagedFileOnResult.java
index 33e5231..4c5875b 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/DeleteStagedFileOnResult.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/DeleteStagedFileOnResult.java
@@ -16,11 +16,12 @@
package com.android.packageinstaller;
-import android.annotation.Nullable;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
+import androidx.annotation.Nullable;
+
import java.io.File;
/**
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/DialogTitle.java b/packages/PackageInstaller/src/com/android/packageinstaller/DialogTitle.java
new file mode 100644
index 0000000..068834c
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/DialogTitle.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2008 Google Inc.
+ *
+ * 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.packageinstaller;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.text.Layout;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.widget.TextView;
+
+import com.android.packageinstaller.R;
+/**
+ * Used by dialogs to change the font size and number of lines to try to fit
+ * the text to the available space.
+ */
+public class DialogTitle extends TextView {
+
+ public DialogTitle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public DialogTitle(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public DialogTitle(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public DialogTitle(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ final Layout layout = getLayout();
+ if (layout != null) {
+ final int lineCount = layout.getLineCount();
+ if (lineCount > 0) {
+ final int ellipsisCount = layout.getEllipsisCount(lineCount - 1);
+ if (ellipsisCount > 0) {
+ setSingleLine(false);
+ setMaxLines(2);
+
+ final TypedArray a = getContext().obtainStyledAttributes(null,
+ R.styleable.TextAppearance, android.R.attr.textAppearanceMedium,
+ android.R.style.TextAppearance_Medium);
+ final int textSize = a.getDimensionPixelSize(
+ R.styleable.TextAppearance_textSize, 0);
+ if (textSize != 0) {
+ // textSize is already expressed in pixels
+ setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
+ }
+ a.recycle();
+
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+ }
+ }
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/EventResultPersister.java b/packages/PackageInstaller/src/com/android/packageinstaller/EventResultPersister.java
index 3a94fdc..8639f47 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/EventResultPersister.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/EventResultPersister.java
@@ -16,8 +16,6 @@
package com.android.packageinstaller;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInstaller;
@@ -27,6 +25,9 @@
import android.util.SparseArray;
import android.util.Xml;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;
@@ -40,17 +41,18 @@
/**
* Persists results of events and calls back observers when a matching result arrives.
*/
-class EventResultPersister {
+public class EventResultPersister {
private static final String LOG_TAG = EventResultPersister.class.getSimpleName();
/** Id passed to {@link #addObserver(int, EventResultObserver)} to generate new id */
- static final int GENERATE_NEW_ID = Integer.MIN_VALUE;
+ public static final int GENERATE_NEW_ID = Integer.MIN_VALUE;
/**
* The extra with the id to set in the intent delivered to
* {@link #onEventReceived(Context, Intent)}
*/
- static final String EXTRA_ID = "EventResultPersister.EXTRA_ID";
+ public static final String EXTRA_ID = "EventResultPersister.EXTRA_ID";
+ public static final String EXTRA_SERVICE_ID = "EventResultPersister.EXTRA_SERVICE_ID";
/** Persisted state of this object */
private final AtomicFile mResultsFile;
@@ -89,8 +91,8 @@
}
/** Call back when a result is received. Observer is removed when onResult it called. */
- interface EventResultObserver {
- void onResult(int status, int legacyStatus, @Nullable String message);
+ public interface EventResultObserver {
+ void onResult(int status, int legacyStatus, @Nullable String message, int serviceId);
}
/**
@@ -153,12 +155,14 @@
int status = readIntAttribute(parser, "status");
int legacyStatus = readIntAttribute(parser, "legacyStatus");
String statusMessage = readStringAttribute(parser, "statusMessage");
+ int serviceId = readIntAttribute(parser, "serviceId");
if (mResults.get(id) != null) {
throw new Exception("id " + id + " has two results");
}
- mResults.put(id, new EventResult(status, legacyStatus, statusMessage));
+ mResults.put(id, new EventResult(status, legacyStatus, statusMessage,
+ serviceId));
} else {
throw new Exception("unexpected tag");
}
@@ -190,6 +194,7 @@
int id = intent.getIntExtra(EXTRA_ID, 0);
String statusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
int legacyStatus = intent.getIntExtra(PackageInstaller.EXTRA_LEGACY_STATUS, 0);
+ int serviceId = intent.getIntExtra(EXTRA_SERVICE_ID, 0);
EventResultObserver observerToCall = null;
synchronized (mLock) {
@@ -204,9 +209,9 @@
}
if (observerToCall != null) {
- observerToCall.onResult(status, legacyStatus, statusMessage);
+ observerToCall.onResult(status, legacyStatus, statusMessage, serviceId);
} else {
- mResults.put(id, new EventResult(status, legacyStatus, statusMessage));
+ mResults.put(id, new EventResult(status, legacyStatus, statusMessage, serviceId));
writeState();
}
}
@@ -258,6 +263,8 @@
serializer.attribute(null, "statusMessage",
results.valueAt(i).message);
}
+ serializer.attribute(null, "serviceId",
+ Integer.toString(results.valueAt(i).serviceId));
serializer.endTag(null, "result");
}
@@ -311,7 +318,8 @@
if (resultIndex >= 0) {
EventResult result = mResults.valueAt(resultIndex);
- observer.onResult(result.status, result.legacyStatus, result.message);
+ observer.onResult(result.status, result.legacyStatus, result.message,
+ result.serviceId);
mResults.removeAt(resultIndex);
writeState();
} else {
@@ -341,13 +349,15 @@
public final int status;
public final int legacyStatus;
@Nullable public final String message;
+ public final int serviceId;
- private EventResult(int status, int legacyStatus, @Nullable String message) {
+ private EventResult(int status, int legacyStatus, @Nullable String message, int serviceId) {
this.status = status;
this.legacyStatus = legacyStatus;
this.message = message;
+ this.serviceId = serviceId;
}
}
- class OutOfIdsException extends Exception {}
+ public class OutOfIdsException extends Exception {}
}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallEventReceiver.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallEventReceiver.java
index c70d7db..be8eabb 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallEventReceiver.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallEventReceiver.java
@@ -16,11 +16,12 @@
package com.android.packageinstaller;
-import android.annotation.NonNull;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
+import androidx.annotation.NonNull;
+
/**
* Receives install events and perists them using a {@link EventResultPersister}.
*/
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallFailed.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallFailed.java
index 54105bb..3505cfb 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallFailed.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallFailed.java
@@ -16,7 +16,6 @@
package com.android.packageinstaller;
-import android.annotation.Nullable;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
@@ -32,7 +31,7 @@
import android.util.Log;
import android.view.View;
-import com.android.internal.app.AlertActivity;
+import androidx.annotation.Nullable;
import java.io.File;
@@ -79,6 +78,8 @@
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ setFinishOnTouchOutside(true);
+
int statusCode = getIntent().getIntExtra(PackageInstaller.EXTRA_STATUS,
PackageInstaller.STATUS_FAILURE);
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallInstalling.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallInstalling.java
index 3aa8dbf..93387e2 100755
--- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallInstalling.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallInstalling.java
@@ -16,28 +16,22 @@
package com.android.packageinstaller;
-import static android.content.pm.PackageInstaller.SessionParams.UID_UNKNOWN;
-
-import android.annotation.Nullable;
import android.app.PendingIntent;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInstaller;
+import android.content.pm.PackageInstaller.InstallInfo;
import android.content.pm.PackageManager;
-import android.content.pm.parsing.ApkLiteParseUtils;
-import android.content.pm.parsing.PackageLite;
-import android.content.pm.parsing.result.ParseResult;
-import android.content.pm.parsing.result.ParseTypeImpl;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
+import android.os.Process;
import android.util.Log;
import android.view.View;
import android.widget.Button;
-import com.android.internal.app.AlertActivity;
-import com.android.internal.content.InstallLocationUtils;
+import androidx.annotation.Nullable;
import java.io.File;
import java.io.FileInputStream;
@@ -139,34 +133,30 @@
params.setOriginatingUri(getIntent()
.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI));
params.setOriginatingUid(getIntent().getIntExtra(Intent.EXTRA_ORIGINATING_UID,
- UID_UNKNOWN));
+ Process.INVALID_UID));
params.setInstallerPackageName(getIntent().getStringExtra(
Intent.EXTRA_INSTALLER_PACKAGE_NAME));
params.setInstallReason(PackageManager.INSTALL_REASON_USER);
File file = new File(mPackageURI.getPath());
try {
- final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing();
- final ParseResult<PackageLite> result = ApkLiteParseUtils.parsePackageLite(
- input.reset(), file, /* flags */ 0);
- if (result.isError()) {
- Log.e(LOG_TAG, "Cannot parse package " + file + ". Assuming defaults.");
- Log.e(LOG_TAG,
- "Cannot calculate installed size " + file + ". Try only apk size.");
+ final InstallInfo result = getPackageManager().getPackageInstaller()
+ .getInstallInfo(file, 0);
+ params.setAppPackageName(result.getPackageName());
+ params.setInstallLocation(result.getInstallLocation());
+ try {
+ params.setSize(result.calculateInstalledSize(params));
+ } catch (IOException e) {
+ e.printStackTrace();
params.setSize(file.length());
- } else {
- final PackageLite pkg = result.getResult();
- params.setAppPackageName(pkg.getPackageName());
- params.setInstallLocation(pkg.getInstallLocation());
- params.setSize(InstallLocationUtils.calculateInstalledSize(pkg,
- params.abiOverride));
}
- } catch (IOException e) {
+ } catch (PackageInstaller.PackageParsingException e) {
+
+ Log.e(LOG_TAG, "Cannot parse package " + file + ". Assuming defaults.", e);
Log.e(LOG_TAG,
"Cannot calculate installed size " + file + ". Try only apk size.");
params.setSize(file.length());
}
-
try {
mInstallId = InstallEventReceiver
.addObserver(this, EventResultPersister.GENERATE_NEW_ID,
@@ -281,8 +271,11 @@
* @param statusCode The installation result.
* @param legacyStatus The installation as used internally in the package manager.
* @param statusMessage The detailed installation result.
+ * @param serviceId Id for PowerManager.WakeLock service. Used only by Wear devices
+ * during an uninstall.
*/
- private void launchFinishBasedOnResult(int statusCode, int legacyStatus, String statusMessage) {
+ private void launchFinishBasedOnResult(int statusCode, int legacyStatus, String statusMessage,
+ int serviceId /* ignore */) {
if (statusCode == PackageInstaller.STATUS_SUCCESS) {
launchSuccess();
} else {
@@ -292,7 +285,7 @@
/**
* Send the package to the package installer and then register a event result observer that
- * will call {@link #launchFinishBasedOnResult(int, int, String)}
+ * will call {@link #launchFinishBasedOnResult(int, int, String, int)}
*/
private final class InstallingAsyncTask extends AsyncTask<Void, Void,
PackageInstaller.Session> {
@@ -318,6 +311,7 @@
try (InputStream in = new FileInputStream(file)) {
long sizeBytes = file.length();
+ long totalRead = 0;
try (OutputStream out = session
.openWrite("PackageInstaller", 0, sizeBytes)) {
byte[] buffer = new byte[1024 * 1024];
@@ -336,8 +330,9 @@
out.write(buffer, 0, numRead);
if (sizeBytes > 0) {
- float fraction = ((float) numRead / (float) sizeBytes);
- session.addProgress(fraction);
+ totalRead += numRead;
+ float fraction = ((float) totalRead / (float) sizeBytes);
+ session.setStagingProgress(fraction);
}
}
}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java
index b6b8837..68de2f6 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java
@@ -16,7 +16,6 @@
package com.android.packageinstaller;
-import android.annotation.Nullable;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
@@ -31,7 +30,7 @@
import android.util.Log;
import android.view.View;
-import com.android.internal.app.AlertActivity;
+import androidx.annotation.Nullable;
import java.io.File;
import java.io.FileOutputStream;
@@ -58,6 +57,7 @@
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ setFinishOnTouchOutside(true);
mAlert.setIcon(R.drawable.ic_file_download);
mAlert.setTitle(getString(R.string.app_name_unknown));
mAlert.setView(R.layout.install_content_view);
@@ -123,7 +123,8 @@
* Show an error message and set result as error.
*/
private void showError() {
- (new ErrorDialog()).showAllowingStateLoss(getFragmentManager(), "error");
+ getFragmentManager().beginTransaction()
+ .add(new ErrorDialog(), "error").commitAllowingStateLoss();
Intent result = new Intent();
result.putExtra(Intent.EXTRA_INSTALL_RESULT,
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java
index 88c1036..7338e64 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java
@@ -19,26 +19,25 @@
import static com.android.packageinstaller.PackageUtil.getMaxTargetSdkVersionForUid;
import android.Manifest;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.app.Activity;
-import android.app.ActivityManager;
import android.content.ContentResolver;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ProviderInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
-import android.os.RemoteException;
+import android.os.Process;
import android.text.TextUtils;
import android.util.EventLog;
import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
import java.util.Arrays;
/**
@@ -80,12 +79,11 @@
final ApplicationInfo sourceInfo = getSourceInfo(callingPackage);
final int originatingUid = getOriginatingUid(sourceInfo);
boolean isTrustedSource = false;
- if (sourceInfo != null
- && (sourceInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0) {
+ if (sourceInfo != null && sourceInfo.isPrivilegedApp()) {
isTrustedSource = intent.getBooleanExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, false);
}
- if (!isTrustedSource && originatingUid != PackageInstaller.SessionParams.UID_UNKNOWN) {
+ if (!isTrustedSource && originatingUid != Process.INVALID_UID) {
final int targetSdkVersion = getMaxTargetSdkVersionForUid(this, originatingUid);
if (targetSdkVersion < 0) {
Log.w(LOG_TAG, "Cannot get target sdk version for uid " + originatingUid);
@@ -99,6 +97,10 @@
}
}
+ if (sessionId != -1 && !isCallerSessionOwner(originatingUid, sessionId)) {
+ mAbortInstall = true;
+ }
+
final String installerPackageNameFromIntent = getIntent().getStringExtra(
Intent.EXTRA_INSTALLER_PACKAGE_NAME);
if (installerPackageNameFromIntent != null) {
@@ -208,30 +210,27 @@
}
/**
- * Get the originating uid if possible, or
- * {@link android.content.pm.PackageInstaller.SessionParams#UID_UNKNOWN} if not available
+ * Get the originating uid if possible, or {@link Process#INVALID_UID} if not available
*
* @param sourceInfo The source of this installation
- * @return The UID of the installation source or UID_UNKNOWN
+ * @return The UID of the installation source or INVALID_UID
*/
private int getOriginatingUid(@Nullable ApplicationInfo sourceInfo) {
// The originating uid from the intent. We only trust/use this if it comes from either
// the document manager app or the downloads provider
final int uidFromIntent = getIntent().getIntExtra(Intent.EXTRA_ORIGINATING_UID,
- PackageInstaller.SessionParams.UID_UNKNOWN);
+ Process.INVALID_UID);
final int callingUid;
if (sourceInfo != null) {
callingUid = sourceInfo.uid;
} else {
- try {
- callingUid = ActivityManager.getService()
- .getLaunchedFromUid(getActivityToken());
- } catch (RemoteException ex) {
+ callingUid = getLaunchedFromUid();
+ if (callingUid == Process.INVALID_UID) {
// Cannot reach ActivityManager. Aborting install.
Log.e(LOG_TAG, "Could not determine the launching uid.");
mAbortInstall = true;
- return PackageInstaller.SessionParams.UID_UNKNOWN;
+ return Process.INVALID_UID;
}
}
if (checkPermission(Manifest.permission.MANAGE_DOCUMENTS, -1, callingUid)
@@ -253,7 +252,8 @@
return false;
}
final ApplicationInfo appInfo = downloadProviderPackage.applicationInfo;
- return (appInfo.isSystemApp() && uid == appInfo.uid);
+ return ((appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0
+ && uid == appInfo.uid);
}
@NonNull
@@ -268,8 +268,14 @@
try {
return mPackageManager.canPackageQuery(callingPackage, targetPackage);
- } catch (NameNotFoundException e) {
+ } catch (PackageManager.NameNotFoundException e) {
return false;
}
}
+
+ private boolean isCallerSessionOwner(int originatingUid, int sessionId) {
+ PackageInstaller packageInstaller = getPackageManager().getPackageInstaller();
+ int installerUid = packageInstaller.getSessionInfo(sessionId).getInstallerUid();
+ return (originatingUid == Process.ROOT_UID) || (originatingUid == installerUid);
+ }
}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallSuccess.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallSuccess.java
index 38c06dd..73c03a5 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallSuccess.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallSuccess.java
@@ -16,7 +16,6 @@
package com.android.packageinstaller;
-import android.annotation.Nullable;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.DialogInterface;
@@ -30,7 +29,7 @@
import android.view.View;
import android.widget.Button;
-import com.android.internal.app.AlertActivity;
+import androidx.annotation.Nullable;
import java.io.File;
import java.util.List;
@@ -54,6 +53,8 @@
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ setFinishOnTouchOutside(true);
+
if (getIntent().getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false)) {
// Return result if requested
Intent result = new Intent();
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstalledNotificationUtils.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstalledNotificationUtils.java
index eea12ec..228a9f5 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstalledNotificationUtils.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstalledNotificationUtils.java
@@ -16,7 +16,6 @@
package com.android.packageinstaller;
-import android.annotation.NonNull;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
@@ -24,7 +23,6 @@
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageItemInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
@@ -32,8 +30,11 @@
import android.net.Uri;
import android.os.Bundle;
import android.provider.Settings;
+import android.text.TextUtils;
import android.util.Log;
+import androidx.annotation.NonNull;
+
/**
* A util class that handle and post new app installed notifications.
*/
@@ -107,8 +108,8 @@
@NonNull String packageName) {
CharSequence label = appInfo.loadSafeLabel(context.getPackageManager(),
DEFAULT_MAX_LABEL_SIZE_PX,
- PackageItemInfo.SAFE_LABEL_FLAG_TRIM
- | PackageItemInfo.SAFE_LABEL_FLAG_FIRST_LINE).toString();
+ TextUtils.SAFE_STRING_FLAG_TRIM
+ | TextUtils.SAFE_STRING_FLAG_FIRST_LINE).toString();
if (label != null) {
return label.toString();
}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstalledReceiver.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstalledReceiver.java
index 74c7b58..2278f7c 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstalledReceiver.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstalledReceiver.java
@@ -20,7 +20,6 @@
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
-import android.provider.Settings;
import android.util.Log;
/**
@@ -33,8 +32,7 @@
@Override
public void onReceive(Context context, Intent intent) {
- if (Settings.Global.getInt(context.getContentResolver(),
- Settings.Global.SHOW_NEW_APP_INSTALLED_NOTIFICATION_ENABLED, 0) == 0) {
+ if (!context.getPackageManager().shouldShowNewAppInstalledNotification()) {
return;
}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
index fa93670..3138158 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
@@ -21,11 +21,8 @@
import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
import android.Manifest;
-import android.annotation.NonNull;
-import android.annotation.StringRes;
import android.app.Activity;
import android.app.AlertDialog;
-import android.app.AppGlobals;
import android.app.AppOpsManager;
import android.app.Dialog;
import android.app.DialogFragment;
@@ -36,24 +33,26 @@
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
-import android.content.pm.IPackageManager;
import android.content.pm.PackageInfo;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageInstaller.SessionInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.pm.UserInfo;
import android.graphics.drawable.BitmapDrawable;
import android.net.Uri;
import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
import android.os.Process;
+import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
import android.util.Log;
import android.view.View;
import android.widget.Button;
-import com.android.internal.app.AlertActivity;
+import androidx.annotation.NonNull;
+import androidx.annotation.StringRes;
import java.io.File;
import java.util.ArrayList;
@@ -86,13 +85,12 @@
private Uri mPackageURI;
private Uri mOriginatingURI;
private Uri mReferrerURI;
- private int mOriginatingUid = PackageInstaller.SessionParams.UID_UNKNOWN;
+ private int mOriginatingUid = Process.INVALID_UID;
private String mOriginatingPackage; // The package name corresponding to #mOriginatingUid
private int mActivityResultCode = Activity.RESULT_CANCELED;
private final boolean mLocalLOGV = false;
PackageManager mPm;
- IPackageManager mIpm;
AppOpsManager mAppOpsManager;
UserManager mUserManager;
PackageInstaller mInstaller;
@@ -166,7 +164,8 @@
DialogFragment newDialog = createDialog(id);
if (newDialog != null) {
- newDialog.showAllowingStateLoss(getFragmentManager(), "dialog");
+ getFragmentManager().beginTransaction()
+ .add(newDialog, "dialog").commitAllowingStateLoss();
}
}
@@ -211,9 +210,9 @@
// Log the fact that the app is requesting an install, and is now allowed to do it
// (before this point we could only log that it's requesting an install, but isn't
// allowed to do it yet).
- int appOpCode =
- AppOpsManager.permissionToOpCode(Manifest.permission.REQUEST_INSTALL_PACKAGES);
- mAppOpsManager.noteOpNoThrow(appOpCode, mOriginatingUid, mOriginatingPackage,
+ String appOpStr =
+ AppOpsManager.permissionToOp(Manifest.permission.REQUEST_INSTALL_PACKAGES);
+ mAppOpsManager.noteOpNoThrow(appOpStr, mOriginatingUid, mOriginatingPackage,
mCallingAttributionTag,
"Successfully started package installation activity");
@@ -250,12 +249,9 @@
private boolean isInstallRequestFromUnknownSource(Intent intent) {
if (mCallingPackage != null && intent.getBooleanExtra(
Intent.EXTRA_NOT_UNKNOWN_SOURCE, false)) {
- if (mSourceInfo != null) {
- if ((mSourceInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED)
- != 0) {
- // Privileged apps can bypass unknown sources check if they want.
- return false;
- }
+ if (mSourceInfo != null && mSourceInfo.isPrivilegedApp()) {
+ // Privileged apps can bypass unknown sources check if they want.
+ return false;
}
}
return true;
@@ -305,7 +301,7 @@
@Override
protected void onCreate(Bundle icicle) {
- if (mLocalLOGV) Log.i(TAG, "creating for user " + getUserId());
+ if (mLocalLOGV) Log.i(TAG, "creating for user " + UserHandle.myUserId());
getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
super.onCreate(null);
@@ -316,7 +312,6 @@
setFinishOnTouchOutside(true);
mPm = getPackageManager();
- mIpm = AppGlobals.getPackageManager();
mAppOpsManager = (AppOpsManager) getSystemService(Context.APP_OPS_SERVICE);
mInstaller = mPm.getPackageInstaller();
mUserManager = (UserManager) getSystemService(Context.USER_SERVICE);
@@ -328,8 +323,8 @@
mCallingAttributionTag = intent.getStringExtra(EXTRA_CALLING_ATTRIBUTION_TAG);
mSourceInfo = intent.getParcelableExtra(EXTRA_ORIGINAL_SOURCE_INFO);
mOriginatingUid = intent.getIntExtra(Intent.EXTRA_ORIGINATING_UID,
- PackageInstaller.SessionParams.UID_UNKNOWN);
- mOriginatingPackage = (mOriginatingUid != PackageInstaller.SessionParams.UID_UNKNOWN)
+ Process.INVALID_UID);
+ mOriginatingPackage = (mOriginatingUid != Process.INVALID_UID)
? getPackageNameForUid(mOriginatingUid) : null;
final Object packageSource;
@@ -337,21 +332,23 @@
final int sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID,
-1 /* defaultValue */);
final SessionInfo info = mInstaller.getSessionInfo(sessionId);
- if (info == null || !info.sealed || info.resolvedBaseCodePath == null) {
+ final String resolvedBaseCodePath = intent.getStringExtra(
+ PackageInstaller.EXTRA_RESOLVED_BASE_PATH);
+ if (info == null || !info.isSealed() || resolvedBaseCodePath == null) {
Log.w(TAG, "Session " + mSessionId + " in funky state; ignoring");
finish();
return;
}
mSessionId = sessionId;
- packageSource = Uri.fromFile(new File(info.resolvedBaseCodePath));
+ packageSource = Uri.fromFile(new File(resolvedBaseCodePath));
mOriginatingURI = null;
mReferrerURI = null;
} else if (PackageInstaller.ACTION_CONFIRM_PRE_APPROVAL.equals(action)) {
final int sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID,
-1 /* defaultValue */);
final SessionInfo info = mInstaller.getSessionInfo(sessionId);
- if (info == null || !info.isPreapprovalRequested) {
+ if (info == null || !info.getIsPreApprovalRequested()) {
Log.w(TAG, "Session " + mSessionId + " in funky state; ignoring");
finish();
return;
@@ -547,15 +544,15 @@
return;
}
// Shouldn't use static constant directly, see b/65534401.
- final int appOpCode =
- AppOpsManager.permissionToOpCode(Manifest.permission.REQUEST_INSTALL_PACKAGES);
- final int appOpMode = mAppOpsManager.noteOpNoThrow(appOpCode, mOriginatingUid,
+ final String appOpStr =
+ AppOpsManager.permissionToOp(Manifest.permission.REQUEST_INSTALL_PACKAGES);
+ final int appOpMode = mAppOpsManager.noteOpNoThrow(appOpStr, mOriginatingUid,
mOriginatingPackage, mCallingAttributionTag,
"Started package installation activity");
if (mLocalLOGV) Log.i(TAG, "handleUnknownSources(): appMode=" + appOpMode);
switch (appOpMode) {
case AppOpsManager.MODE_DEFAULT:
- mAppOpsManager.setMode(appOpCode, mOriginatingUid,
+ mAppOpsManager.setMode(appOpStr, mOriginatingUid,
mOriginatingPackage, AppOpsManager.MODE_ERRORED);
// fall through
case AppOpsManager.MODE_ERRORED:
@@ -588,8 +585,8 @@
switch (scheme) {
case SCHEME_PACKAGE: {
- for (UserInfo info : mUserManager.getUsers()) {
- PackageManager pmForUser = createContextAsUser(info.getUserHandle(), 0)
+ for (UserHandle handle : mUserManager.getUserHandles(true)) {
+ PackageManager pmForUser = createContextAsUser(handle, 0)
.getPackageManager();
try {
if (pmForUser.canPackageQuery(mCallingPackage, packageName)) {
@@ -645,9 +642,9 @@
* @return {@code true} iff the installer could be set up
*/
private boolean processSessionInfo(@NonNull SessionInfo info) {
- mPkgInfo = generateStubPackageInfo(info.appPackageName);
- mAppSnippet = new PackageUtil.AppSnippet(info.appLabel,
- info.appIcon != null ? new BitmapDrawable(getResources(), info.appIcon)
+ mPkgInfo = generateStubPackageInfo(info.getAppPackageName());
+ mAppSnippet = new PackageUtil.AppSnippet(info.getAppLabel(),
+ info.getAppIcon() != null ? new BitmapDrawable(getResources(), info.getAppIcon())
: getPackageManager().getDefaultActivityIcon());
return true;
}
@@ -693,7 +690,7 @@
if (mReferrerURI != null) {
newIntent.putExtra(Intent.EXTRA_REFERRER, mReferrerURI);
}
- if (mOriginatingUid != PackageInstaller.SessionParams.UID_UNKNOWN) {
+ if (mOriginatingUid != Process.INVALID_UID) {
newIntent.putExtra(Intent.EXTRA_ORIGINATING_UID, mOriginatingUid);
}
if (installerPackageName != null) {
@@ -829,7 +826,7 @@
if (isDestroyed()) {
return;
}
- getMainThreadHandler().postDelayed(() -> {
+ new Handler(Looper.getMainLooper()).postDelayed(() -> {
if (!isDestroyed()) {
startActivity(getIntent().addFlags(FLAG_ACTIVITY_REORDER_TO_FRONT));
}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageUtil.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageUtil.java
index f5570df..698159f 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/PackageUtil.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageUtil.java
@@ -17,14 +17,11 @@
package com.android.packageinstaller;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.app.Activity;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
-import android.content.res.AssetManager;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.os.UserHandle;
@@ -33,6 +30,9 @@
import android.widget.ImageView;
import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
import java.io.File;
/**
@@ -131,16 +131,15 @@
public static AppSnippet getAppSnippet(
Activity pContext, ApplicationInfo appInfo, File sourceFile) {
final String archiveFilePath = sourceFile.getAbsolutePath();
- Resources pRes = pContext.getResources();
- AssetManager assmgr = new AssetManager();
- assmgr.addAssetPath(archiveFilePath);
- Resources res = new Resources(assmgr, pRes.getDisplayMetrics(), pRes.getConfiguration());
+ PackageManager pm = pContext.getPackageManager();
+ appInfo.publicSourceDir = archiveFilePath;
+
CharSequence label = null;
// Try to load the label from the package's resources. If an app has not explicitly
// specified any label, just use the package name.
if (appInfo.labelRes != 0) {
try {
- label = res.getText(appInfo.labelRes);
+ label = appInfo.loadLabel(pm);
} catch (Resources.NotFoundException e) {
}
}
@@ -154,7 +153,7 @@
try {
if (appInfo.icon != 0) {
try {
- icon = res.getDrawable(appInfo.icon);
+ icon = appInfo.loadIcon(pm);
} catch (Resources.NotFoundException e) {
}
}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/TemporaryFileManager.java b/packages/PackageInstaller/src/com/android/packageinstaller/TemporaryFileManager.java
index f77318c..afb2ea4 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/TemporaryFileManager.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/TemporaryFileManager.java
@@ -16,13 +16,14 @@
package com.android.packageinstaller;
-import android.annotation.NonNull;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.SystemClock;
import android.util.Log;
+import androidx.annotation.NonNull;
+
import java.io.File;
import java.io.IOException;
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/UninstallEventReceiver.java b/packages/PackageInstaller/src/com/android/packageinstaller/UninstallEventReceiver.java
index c3e9c23..86b0321 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/UninstallEventReceiver.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/UninstallEventReceiver.java
@@ -16,11 +16,12 @@
package com.android.packageinstaller;
-import android.annotation.NonNull;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
+import androidx.annotation.NonNull;
+
/**
* Receives uninstall events and persists them using a {@link EventResultPersister}.
*/
@@ -58,7 +59,7 @@
*
* @return The id for this event
*/
- static int addObserver(@NonNull Context context, int id,
+ public static int addObserver(@NonNull Context context, int id,
@NonNull EventResultPersister.EventResultObserver observer)
throws EventResultPersister.OutOfIdsException {
return getReceiver(context).addObserver(id, observer);
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/UninstallFinish.java b/packages/PackageInstaller/src/com/android/packageinstaller/UninstallFinish.java
index 973ab89..b9552fc 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/UninstallFinish.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/UninstallFinish.java
@@ -16,29 +16,27 @@
package com.android.packageinstaller;
-import android.annotation.NonNull;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
-import android.app.admin.IDevicePolicyManager;
+import android.app.admin.DevicePolicyManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
-import android.content.pm.IPackageManager;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
-import android.content.pm.UserInfo;
import android.graphics.drawable.Icon;
-import android.os.RemoteException;
-import android.os.ServiceManager;
+import android.os.Process;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
import android.util.Log;
import android.widget.Toast;
+import androidx.annotation.NonNull;
+
import java.util.List;
/**
@@ -94,28 +92,24 @@
switch (legacyStatus) {
case PackageManager.DELETE_FAILED_DEVICE_POLICY_MANAGER: {
- IDevicePolicyManager dpm = IDevicePolicyManager.Stub.asInterface(
- ServiceManager.getService(Context.DEVICE_POLICY_SERVICE));
// Find out if the package is an active admin for some non-current user.
- int myUserId = UserHandle.myUserId();
- UserInfo otherBlockingUser = null;
- for (UserInfo user : userManager.getUsers()) {
+ UserHandle myUserHandle = Process.myUserHandle();
+ UserHandle otherBlockingUserHandle = null;
+ for (UserHandle otherUserHandle : userManager.getUserHandles(true)) {
// We only catch the case when the user in question is neither the
// current user nor its profile.
- if (isProfileOfOrSame(userManager, myUserId, user.id)) {
+ if (isProfileOfOrSame(userManager, myUserHandle, otherUserHandle)) {
continue;
}
-
- try {
- if (dpm.packageHasActiveAdmins(appInfo.packageName, user.id)) {
- otherBlockingUser = user;
- break;
- }
- } catch (RemoteException e) {
- Log.e(LOG_TAG, "Failed to talk to package manager", e);
+ DevicePolicyManager dpm =
+ context.createContextAsUser(otherUserHandle, 0)
+ .getSystemService(DevicePolicyManager.class);
+ if (dpm.packageHasActiveAdmins(appInfo.packageName)) {
+ otherBlockingUserHandle = otherUserHandle;
+ break;
}
}
- if (otherBlockingUser == null) {
+ if (otherBlockingUserHandle == null) {
Log.d(LOG_TAG, "Uninstall failed because " + appInfo.packageName
+ " is a device admin");
@@ -124,46 +118,41 @@
R.string.uninstall_failed_device_policy_manager));
} else {
Log.d(LOG_TAG, "Uninstall failed because " + appInfo.packageName
- + " is a device admin of user " + otherBlockingUser);
+ + " is a device admin of user " + otherBlockingUserHandle);
+ String userName =
+ context.createContextAsUser(otherBlockingUserHandle, 0)
+ .getSystemService(UserManager.class).getUserName();
setBigText(uninstallFailedNotification, String.format(context.getString(
R.string.uninstall_failed_device_policy_manager_of_user),
- otherBlockingUser.name));
+ userName));
}
break;
}
case PackageManager.DELETE_FAILED_OWNER_BLOCKED: {
- IPackageManager packageManager = IPackageManager.Stub.asInterface(
- ServiceManager.getService("package"));
-
- List<UserInfo> users = userManager.getUsers();
- int blockingUserId = UserHandle.USER_NULL;
- for (int i = 0; i < users.size(); ++i) {
- final UserInfo user = users.get(i);
- try {
- if (packageManager.getBlockUninstallForUser(appInfo.packageName,
- user.id)) {
- blockingUserId = user.id;
- break;
- }
- } catch (RemoteException e) {
- // Shouldn't happen.
- Log.e(LOG_TAG, "Failed to talk to package manager", e);
+ PackageManager packageManager = context.getPackageManager();
+ List<UserHandle> userHandles = userManager.getUserHandles(true);
+ UserHandle otherBlockingUserHandle = null;
+ for (int i = 0; i < userHandles.size(); ++i) {
+ final UserHandle handle = userHandles.get(i);
+ if (packageManager.canUserUninstall(appInfo.packageName, handle)) {
+ otherBlockingUserHandle = handle;
+ break;
}
}
- int myUserId = UserHandle.myUserId();
- if (isProfileOfOrSame(userManager, myUserId, blockingUserId)) {
+ UserHandle myUserHandle = Process.myUserHandle();
+ if (isProfileOfOrSame(userManager, myUserHandle, otherBlockingUserHandle)) {
addDeviceManagerButton(context, uninstallFailedNotification);
} else {
addManageUsersButton(context, uninstallFailedNotification);
}
- if (blockingUserId == UserHandle.USER_NULL) {
+ if (otherBlockingUserHandle == null) {
Log.d(LOG_TAG,
"Uninstall failed for " + appInfo.packageName + " with code "
+ returnCode + " no blocking user");
- } else if (blockingUserId == UserHandle.USER_SYSTEM) {
+ } else if (otherBlockingUserHandle == UserHandle.SYSTEM) {
setBigText(uninstallFailedNotification,
context.getString(R.string.uninstall_blocked_device_owner));
} else {
@@ -200,18 +189,18 @@
* Is a profile part of a user?
*
* @param userManager The user manager
- * @param userId The id of the user
- * @param profileId The id of the profile
+ * @param userHandle The handle of the user
+ * @param profileHandle The handle of the profile
*
* @return If the profile is part of the user or the profile parent of the user
*/
- private boolean isProfileOfOrSame(@NonNull UserManager userManager, int userId, int profileId) {
- if (userId == profileId) {
+ private boolean isProfileOfOrSame(UserManager userManager, UserHandle userHandle,
+ UserHandle profileHandle) {
+ if (userHandle.equals(profileHandle)) {
return true;
}
-
- UserInfo parentUser = userManager.getProfileParent(profileId);
- return parentUser != null && parentUser.id == userId;
+ return userManager.getProfileParent(profileHandle) != null
+ && userManager.getProfileParent(profileHandle).equals(userHandle);
}
/**
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/UninstallUninstalling.java b/packages/PackageInstaller/src/com/android/packageinstaller/UninstallUninstalling.java
index 1485352..b60aba8 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/UninstallUninstalling.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/UninstallUninstalling.java
@@ -16,9 +16,7 @@
package com.android.packageinstaller;
-import android.annotation.Nullable;
import android.app.Activity;
-import android.app.ActivityThread;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
@@ -27,19 +25,18 @@
import android.app.PendingIntent;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
-import android.content.pm.IPackageDeleteObserver2;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
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;
+import androidx.annotation.Nullable;
+
/**
* Start an uninstallation, show a dialog while uninstalling and return result to the caller.
*/
@@ -57,7 +54,7 @@
private int mUninstallId;
private ApplicationInfo mAppInfo;
- private IBinder mCallback;
+ private PackageManager.UninstallCompleteCallback mCallback;
private boolean mReturnResult;
private String mLabel;
@@ -68,7 +65,8 @@
setFinishOnTouchOutside(false);
mAppInfo = getIntent().getParcelableExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO);
- mCallback = getIntent().getIBinderExtra(PackageInstaller.EXTRA_CALLBACK);
+ mCallback = getIntent().getParcelableExtra(PackageInstaller.EXTRA_CALLBACK,
+ PackageManager.UninstallCompleteCallback.class);
mReturnResult = getIntent().getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false);
mLabel = getIntent().getStringExtra(EXTRA_APP_LABEL);
@@ -119,15 +117,10 @@
int flags = allUsers ? PackageManager.DELETE_ALL_USERS : 0;
flags |= keepData ? PackageManager.DELETE_KEEP_DATA : 0;
- try {
- ActivityThread.getPackageManager().getPackageInstaller().uninstall(
- new VersionedPackage(mAppInfo.packageName,
- PackageManager.VERSION_CODE_HIGHEST),
- getPackageName(), flags, pendingIntent.getIntentSender(),
- user.getIdentifier());
- } catch (RemoteException e) {
- e.rethrowFromSystemServer();
- }
+ getPackageManager().getPackageInstaller().uninstall(
+ new VersionedPackage(mAppInfo.packageName,
+ PackageManager.VERSION_CODE_HIGHEST),
+ flags, pendingIntent.getIntentSender());
} else {
mUninstallId = savedInstanceState.getInt(UNINSTALL_ID);
UninstallEventReceiver.addObserver(this, mUninstallId, this);
@@ -135,7 +128,7 @@
} catch (EventResultPersister.OutOfIdsException | IllegalArgumentException e) {
Log.e(LOG_TAG, "Fails to start uninstall", e);
onResult(PackageInstaller.STATUS_FAILURE, PackageManager.DELETE_FAILED_INTERNAL_ERROR,
- null);
+ null, 0);
}
}
@@ -152,15 +145,10 @@
}
@Override
- public void onResult(int status, int legacyStatus, @Nullable String message) {
+ public void onResult(int status, int legacyStatus, @Nullable String message, int serviceId) {
if (mCallback != null) {
// The caller will be informed about the result via a callback
- final IPackageDeleteObserver2 observer = IPackageDeleteObserver2.Stub
- .asInterface(mCallback);
- try {
- observer.onPackageDeleted(mAppInfo.packageName, legacyStatus, message);
- } catch (RemoteException ignored) {
- }
+ mCallback.onUninstallComplete(mAppInfo.packageName, legacyStatus, message);
} else if (mReturnResult) {
// The caller will be informed about the result and might decide to display it
Intent result = new Intent();
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/UninstallerActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/UninstallerActivity.java
index 0198168..04496b9 100755
--- a/packages/PackageInstaller/src/com/android/packageinstaller/UninstallerActivity.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/UninstallerActivity.java
@@ -22,12 +22,7 @@
import static com.android.packageinstaller.PackageUtil.getMaxTargetSdkVersionForUid;
import android.Manifest;
-import android.annotation.NonNull;
-import android.annotation.StringRes;
import android.app.Activity;
-import android.app.ActivityManager;
-import android.app.ActivityThread;
-import android.app.AppGlobals;
import android.app.AppOpsManager;
import android.app.DialogFragment;
import android.app.Fragment;
@@ -37,12 +32,9 @@
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ComponentName;
-import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
-import android.content.pm.IPackageDeleteObserver2;
-import android.content.pm.IPackageManager;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
import android.content.pm.VersionedPackage;
@@ -50,13 +42,14 @@
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.os.ServiceManager;
+import android.os.Process;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.annotation.StringRes;
+
import com.android.packageinstaller.handheld.ErrorDialogFragment;
import com.android.packageinstaller.handheld.UninstallAlertDialogFragment;
import com.android.packageinstaller.television.ErrorFragment;
@@ -80,7 +73,7 @@
public ActivityInfo activityInfo;
public boolean allUsers;
public UserHandle user;
- public IBinder callback;
+ public PackageManager.UninstallCompleteCallback callback;
}
private String mPackageName;
@@ -94,44 +87,8 @@
// be stale, if e.g. the app was uninstalled while the activity was destroyed.
super.onCreate(null);
- try {
- int callingUid = ActivityManager.getService().getLaunchedFromUid(getActivityToken());
-
- String callingPackage = getPackageNameForUid(callingUid);
- if (callingPackage == null) {
- Log.e(TAG, "Package not found for originating uid " + callingUid);
- setResult(Activity.RESULT_FIRST_USER);
- finish();
- return;
- } else {
- AppOpsManager appOpsManager = (AppOpsManager) getSystemService(
- Context.APP_OPS_SERVICE);
- if (appOpsManager.noteOpNoThrow(
- AppOpsManager.OPSTR_REQUEST_DELETE_PACKAGES, callingUid, callingPackage)
- != MODE_ALLOWED) {
- Log.e(TAG, "Install from uid " + callingUid + " disallowed by AppOps");
- setResult(Activity.RESULT_FIRST_USER);
- finish();
- return;
- }
- }
-
- if (getMaxTargetSdkVersionForUid(this, callingUid)
- >= Build.VERSION_CODES.P && AppGlobals.getPackageManager().checkUidPermission(
- Manifest.permission.REQUEST_DELETE_PACKAGES, callingUid)
- != PackageManager.PERMISSION_GRANTED
- && AppGlobals.getPackageManager().checkUidPermission(
- Manifest.permission.DELETE_PACKAGES, callingUid)
- != PackageManager.PERMISSION_GRANTED) {
- Log.e(TAG, "Uid " + callingUid + " does not have "
- + Manifest.permission.REQUEST_DELETE_PACKAGES + " or "
- + Manifest.permission.DELETE_PACKAGES);
-
- setResult(Activity.RESULT_FIRST_USER);
- finish();
- return;
- }
- } catch (RemoteException ex) {
+ int callingUid = getLaunchedFromUid();
+ if (callingUid == Process.INVALID_UID) {
// Cannot reach Package/ActivityManager. Aborting uninstall.
Log.e(TAG, "Could not determine the launching uid.");
@@ -140,6 +97,38 @@
return;
}
+ String callingPackage = getPackageNameForUid(callingUid);
+ if (callingPackage == null) {
+ Log.e(TAG, "Package not found for originating uid " + callingUid);
+ setResult(Activity.RESULT_FIRST_USER);
+ finish();
+ return;
+ } else {
+ AppOpsManager appOpsManager = getSystemService(AppOpsManager.class);
+ if (appOpsManager.noteOpNoThrow(
+ AppOpsManager.OPSTR_REQUEST_DELETE_PACKAGES, callingUid, callingPackage)
+ != MODE_ALLOWED) {
+ Log.e(TAG, "Install from uid " + callingUid + " disallowed by AppOps");
+ setResult(Activity.RESULT_FIRST_USER);
+ finish();
+ return;
+ }
+ }
+
+ if (getMaxTargetSdkVersionForUid(this, callingUid) >= Build.VERSION_CODES.P
+ && getBaseContext().checkPermission(Manifest.permission.REQUEST_DELETE_PACKAGES,
+ 0 /* random value for pid */, callingUid) != PackageManager.PERMISSION_GRANTED
+ && getBaseContext().checkPermission(Manifest.permission.DELETE_PACKAGES,
+ 0 /* random value for pid */, callingUid) != PackageManager.PERMISSION_GRANTED) {
+ Log.e(TAG, "Uid " + callingUid + " does not have "
+ + Manifest.permission.REQUEST_DELETE_PACKAGES + " or "
+ + Manifest.permission.DELETE_PACKAGES);
+
+ setResult(Activity.RESULT_FIRST_USER);
+ finish();
+ return;
+ }
+
// Get intent information.
// We expect an intent with URI of the form package://<packageName>#<className>
// className is optional; if specified, it is the activity the user chose to uninstall
@@ -157,37 +146,37 @@
return;
}
- final IPackageManager pm = IPackageManager.Stub.asInterface(
- ServiceManager.getService("package"));
+ PackageManager pm = getPackageManager();
+ UserManager userManager = getBaseContext().getSystemService(UserManager.class);
mDialogInfo = new DialogInfo();
mDialogInfo.allUsers = intent.getBooleanExtra(Intent.EXTRA_UNINSTALL_ALL_USERS, false);
- if (mDialogInfo.allUsers && !UserManager.get(this).isAdminUser()) {
+ if (mDialogInfo.allUsers && !userManager.isAdminUser()) {
Log.e(TAG, "Only admin user can request uninstall for all users");
showUserIsNotAllowed();
return;
}
mDialogInfo.user = intent.getParcelableExtra(Intent.EXTRA_USER);
if (mDialogInfo.user == null) {
- mDialogInfo.user = android.os.Process.myUserHandle();
+ mDialogInfo.user = Process.myUserHandle();
} else {
- UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE);
List<UserHandle> profiles = userManager.getUserProfiles();
if (!profiles.contains(mDialogInfo.user)) {
- Log.e(TAG, "User " + android.os.Process.myUserHandle() + " can't request uninstall "
+ Log.e(TAG, "User " + Process.myUserHandle() + " can't request uninstall "
+ "for user " + mDialogInfo.user);
showUserIsNotAllowed();
return;
}
}
- mDialogInfo.callback = intent.getIBinderExtra(PackageInstaller.EXTRA_CALLBACK);
+ mDialogInfo.callback = intent.getParcelableExtra(PackageInstaller.EXTRA_CALLBACK,
+ PackageManager.UninstallCompleteCallback.class);
try {
mDialogInfo.appInfo = pm.getApplicationInfo(mPackageName,
- PackageManager.MATCH_ANY_USER, mDialogInfo.user.getIdentifier());
- } catch (RemoteException e) {
+ PackageManager.ApplicationInfoFlags.of(PackageManager.MATCH_ANY_USER));
+ } catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "Unable to get packageName. Package manager is dead?");
}
@@ -202,9 +191,9 @@
if (className != null) {
try {
mDialogInfo.activityInfo = pm.getActivityInfo(
- new ComponentName(mPackageName, className), 0,
- mDialogInfo.user.getIdentifier());
- } catch (RemoteException e) {
+ new ComponentName(mPackageName, className),
+ PackageManager.ComponentInfoFlags.of(0));
+ } catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "Unable to get className. Package manager is dead?");
// Continue as the ActivityInfo isn't critical.
}
@@ -315,7 +304,6 @@
newIntent.putExtra(UninstallUninstalling.EXTRA_APP_LABEL, label);
newIntent.putExtra(UninstallUninstalling.EXTRA_KEEP_DATA, keepData);
newIntent.putExtra(PackageInstaller.EXTRA_CALLBACK, mDialogInfo.callback);
-
if (returnResult) {
newIntent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
}
@@ -366,11 +354,10 @@
int flags = mDialogInfo.allUsers ? PackageManager.DELETE_ALL_USERS : 0;
flags |= keepData ? PackageManager.DELETE_KEEP_DATA : 0;
- ActivityThread.getPackageManager().getPackageInstaller().uninstall(
+ getPackageManager().getPackageInstaller().uninstall(
new VersionedPackage(mDialogInfo.appInfo.packageName,
PackageManager.VERSION_CODE_HIGHEST),
- getPackageName(), flags, pendingIntent.getIntentSender(),
- mDialogInfo.user.getIdentifier());
+ flags, pendingIntent.getIntentSender());
} catch (Exception e) {
notificationManager.cancel(uninstallId);
@@ -382,13 +369,8 @@
public void dispatchAborted() {
if (mDialogInfo != null && mDialogInfo.callback != null) {
- final IPackageDeleteObserver2 observer = IPackageDeleteObserver2.Stub.asInterface(
- mDialogInfo.callback);
- try {
- observer.onPackageDeleted(mPackageName,
- PackageManager.DELETE_FAILED_ABORTED, "Cancelled by user");
- } catch (RemoteException ignored) {
- }
+ mDialogInfo.callback.onUninstallComplete(mPackageName,
+ PackageManager.DELETE_FAILED_ABORTED, "Cancelled by user");
}
}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/handheld/UninstallAlertDialogFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/handheld/UninstallAlertDialogFragment.java
index a1bc992..21f4be0 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/handheld/UninstallAlertDialogFragment.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/handheld/UninstallAlertDialogFragment.java
@@ -16,10 +16,10 @@
package com.android.packageinstaller.handheld;
+import static android.os.UserManager.USER_TYPE_PROFILE_CLONE;
+import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED;
import static android.text.format.Formatter.formatFileSize;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
@@ -29,7 +29,6 @@
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
-import android.content.pm.UserInfo;
import android.os.Bundle;
import android.os.Process;
import android.os.UserHandle;
@@ -41,6 +40,9 @@
import android.widget.CheckBox;
import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
import com.android.packageinstaller.R;
import com.android.packageinstaller.UninstallerActivity;
@@ -91,11 +93,11 @@
long appDataSize = 0;
if (user == null) {
- List<UserInfo> users = userManager.getUsers();
+ List<UserHandle> userHandles = userManager.getUserHandles(true);
- int numUsers = users.size();
+ int numUsers = userHandles.size();
for (int i = 0; i < numUsers; i++) {
- appDataSize += getAppDataSizeForUser(pkg, UserHandle.of(users.get(i).id));
+ appDataSize += getAppDataSizeForUser(pkg, userHandles.get(i));
}
} else {
appDataSize = getAppDataSizeForUser(pkg, user);
@@ -128,7 +130,7 @@
final boolean isUpdate =
((dialogInfo.appInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0);
final UserHandle myUserHandle = Process.myUserHandle();
- UserManager userManager = UserManager.get(getActivity());
+ UserManager userManager = getContext().getSystemService(UserManager.class);
if (isUpdate) {
if (isSingleUser(userManager)) {
messageBuilder.append(getString(R.string.uninstall_update_text));
@@ -139,20 +141,25 @@
if (dialogInfo.allUsers && !isSingleUser(userManager)) {
messageBuilder.append(getString(R.string.uninstall_application_text_all_users));
} else if (!dialogInfo.user.equals(myUserHandle)) {
- UserInfo userInfo = userManager.getUserInfo(dialogInfo.user.getIdentifier());
- if (userInfo.isManagedProfile()
- && userInfo.profileGroupId == myUserHandle.getIdentifier()) {
+ int userId = dialogInfo.user.getIdentifier();
+ UserManager customUserManager = getContext()
+ .createContextAsUser(UserHandle.of(userId), 0)
+ .getSystemService(UserManager.class);
+ String userName = customUserManager.getUserName();
+
+ if (customUserManager.isUserOfType(USER_TYPE_PROFILE_MANAGED)
+ && customUserManager.isSameProfileGroup(dialogInfo.user, myUserHandle)) {
messageBuilder.append(
getString(R.string.uninstall_application_text_current_user_work_profile,
- userInfo.name));
- } else if (userInfo.isCloneProfile()
- && userInfo.profileGroupId == myUserHandle.getIdentifier()) {
+ userName));
+ } else if (customUserManager.isUserOfType(USER_TYPE_PROFILE_CLONE)
+ && customUserManager.isSameProfileGroup(dialogInfo.user, myUserHandle)) {
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));
+ getString(R.string.uninstall_application_text_user, userName));
}
} else if (isCloneProfile(myUserHandle)) {
isClonedApp = true;
@@ -238,8 +245,8 @@
// Check if another instance of given package exists in clone user profile.
if (cloneUser != null) {
try {
- if (getContext().getPackageManager()
- .getPackageUidAsUser(packageName, cloneUser.getIdentifier()) > 0) {
+ if (getContext().getPackageManager().getPackageUidAsUser(packageName,
+ PackageManager.PackageInfoFlags.of(0), cloneUser.getIdentifier()) > 0) {
return true;
}
} catch (PackageManager.NameNotFoundException e) {
@@ -273,7 +280,6 @@
*/
private boolean isSingleUser(UserManager userManager) {
final int userCount = userManager.getUserCount();
- return userCount == 1
- || (UserManager.isSplitSystemUser() && userCount == 2);
+ return userCount == 1 || (UserManager.isHeadlessSystemUserMode() && userCount == 2);
}
}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/television/UninstallAlertFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/television/UninstallAlertFragment.java
index 2d241ca..5c5720a 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/television/UninstallAlertFragment.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/television/UninstallAlertFragment.java
@@ -16,10 +16,11 @@
package com.android.packageinstaller.television;
+import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED;
+
import android.app.Activity;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
-import android.content.pm.UserInfo;
import android.os.Bundle;
import android.os.Process;
import android.os.UserHandle;
@@ -63,7 +64,7 @@
final boolean isUpdate =
((dialogInfo.appInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0);
final UserHandle myUserHandle = Process.myUserHandle();
- UserManager userManager = UserManager.get(getActivity());
+ UserManager userManager = getContext().getSystemService(UserManager.class);
if (isUpdate) {
if (isSingleUser(userManager)) {
messageBuilder.append(getString(R.string.uninstall_update_text));
@@ -74,15 +75,21 @@
if (dialogInfo.allUsers && !isSingleUser(userManager)) {
messageBuilder.append(getString(R.string.uninstall_application_text_all_users));
} else if (!dialogInfo.user.equals(myUserHandle)) {
- UserInfo userInfo = userManager.getUserInfo(dialogInfo.user.getIdentifier());
- if (userInfo.isManagedProfile()
- && userInfo.profileGroupId == myUserHandle.getIdentifier()) {
+ int userId = dialogInfo.user.getIdentifier();
+ UserManager customUserManager = getContext()
+ .createContextAsUser(UserHandle.of(userId), 0)
+ .getSystemService(UserManager.class);
+ String userName = customUserManager.getUserName();
+
+ if (customUserManager.isUserOfType(USER_TYPE_PROFILE_MANAGED)
+ && customUserManager.isSameProfileGroup(dialogInfo.user, myUserHandle)) {
+
messageBuilder.append(
getString(R.string.uninstall_application_text_current_user_work_profile,
- userInfo.name));
+ userName));
} else {
messageBuilder.append(
- getString(R.string.uninstall_application_text_user, userInfo.name));
+ getString(R.string.uninstall_application_text_user, userName));
}
} else {
messageBuilder.append(getString(R.string.uninstall_application_text));
@@ -126,7 +133,6 @@
*/
private boolean isSingleUser(UserManager userManager) {
final int userCount = userManager.getUserCount();
- return userCount == 1
- || (UserManager.isSplitSystemUser() && userCount == 2);
+ return userCount == 1 || (UserManager.isHeadlessSystemUserMode() && userCount == 2);
}
}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/television/UninstallAppProgress.java b/packages/PackageInstaller/src/com/android/packageinstaller/television/UninstallAppProgress.java
index a4f217c..0c59d44 100755
--- a/packages/PackageInstaller/src/com/android/packageinstaller/television/UninstallAppProgress.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/television/UninstallAppProgress.java
@@ -17,24 +17,20 @@
package com.android.packageinstaller.television;
import android.app.Activity;
-import android.app.admin.IDevicePolicyManager;
+import android.app.PendingIntent;
+import android.app.admin.DevicePolicyManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
-import android.content.pm.IPackageDeleteObserver;
-import android.content.pm.IPackageDeleteObserver2;
-import android.content.pm.IPackageManager;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
-import android.content.pm.UserInfo;
+import android.content.pm.VersionedPackage;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.os.Handler;
-import android.os.IBinder;
import android.os.Message;
-import android.os.RemoteException;
-import android.os.ServiceManager;
+import android.os.Process;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.Log;
@@ -42,8 +38,12 @@
import android.view.KeyEvent;
import android.widget.Toast;
+import androidx.annotation.Nullable;
+
+import com.android.packageinstaller.EventResultPersister;
import com.android.packageinstaller.PackageUtil;
import com.android.packageinstaller.R;
+import com.android.packageinstaller.UninstallEventReceiver;
import java.lang.ref.WeakReference;
import java.util.List;
@@ -55,14 +55,17 @@
* by an intent with the intent's class name explicitly set to UninstallAppProgress and expects
* the application object of the application to uninstall.
*/
-public class UninstallAppProgress extends Activity {
+public class UninstallAppProgress extends Activity implements
+ EventResultPersister.EventResultObserver {
private static final String TAG = "UninstallAppProgress";
private static final String FRAGMENT_TAG = "progress_fragment";
+ private static final String BROADCAST_ACTION =
+ "com.android.packageinstaller.ACTION_UNINSTALL_COMMIT";
private ApplicationInfo mAppInfo;
private boolean mAllUsers;
- private IBinder mCallback;
+ private PackageManager.UninstallCompleteCallback mCallback;
private volatile int mResultCode = -1;
@@ -116,13 +119,7 @@
final String packageName = (String) msg.obj;
if (mCallback != null) {
- final IPackageDeleteObserver2 observer = IPackageDeleteObserver2.Stub
- .asInterface(mCallback);
- try {
- observer.onPackageDeleted(mAppInfo.packageName, mResultCode,
- packageName);
- } catch (RemoteException ignored) {
- }
+ mCallback.onUninstallComplete(mAppInfo.packageName, mResultCode, packageName);
finish();
return;
}
@@ -139,37 +136,34 @@
// Update the status text
final String statusText;
+ Context ctx = getBaseContext();
switch (msg.arg1) {
case PackageManager.DELETE_SUCCEEDED:
statusText = getString(R.string.uninstall_done);
// Show a Toast and finish the activity
- Context ctx = getBaseContext();
Toast.makeText(ctx, statusText, Toast.LENGTH_LONG).show();
setResultAndFinish();
return;
case PackageManager.DELETE_FAILED_DEVICE_POLICY_MANAGER: {
UserManager userManager =
(UserManager) getSystemService(Context.USER_SERVICE);
- IDevicePolicyManager dpm = IDevicePolicyManager.Stub.asInterface(
- ServiceManager.getService(Context.DEVICE_POLICY_SERVICE));
// Find out if the package is an active admin for some non-current user.
- int myUserId = UserHandle.myUserId();
- UserInfo otherBlockingUser = null;
- for (UserInfo user : userManager.getUsers()) {
+ UserHandle myUserHandle = Process.myUserHandle();
+ UserHandle otherBlockingUserHandle = null;
+ for (UserHandle otherUserHandle : userManager.getUserHandles(true)) {
// We only catch the case when the user in question is neither the
// current user nor its profile.
- if (isProfileOfOrSame(userManager, myUserId, user.id)) continue;
-
- try {
- if (dpm.packageHasActiveAdmins(packageName, user.id)) {
- otherBlockingUser = user;
- break;
- }
- } catch (RemoteException e) {
- Log.e(TAG, "Failed to talk to package manager", e);
+ if (isProfileOfOrSame(userManager, myUserHandle, otherUserHandle)) {
+ continue;
+ }
+ DevicePolicyManager dpm = ctx.createContextAsUser(otherUserHandle, 0)
+ .getSystemService(DevicePolicyManager.class);
+ if (dpm.packageHasActiveAdmins(packageName)) {
+ otherBlockingUserHandle = otherUserHandle;
+ break;
}
}
- if (otherBlockingUser == null) {
+ if (otherBlockingUserHandle == null) {
Log.d(TAG, "Uninstall failed because " + packageName
+ " is a device admin");
getProgressFragment().setDeviceManagerButtonVisible(true);
@@ -177,45 +171,40 @@
R.string.uninstall_failed_device_policy_manager);
} else {
Log.d(TAG, "Uninstall failed because " + packageName
- + " is a device admin of user " + otherBlockingUser);
+ + " is a device admin of user " + otherBlockingUserHandle);
getProgressFragment().setDeviceManagerButtonVisible(false);
+ String userName = ctx.createContextAsUser(otherBlockingUserHandle, 0)
+ .getSystemService(UserManager.class).getUserName();
statusText = String.format(
getString(R.string.uninstall_failed_device_policy_manager_of_user),
- otherBlockingUser.name);
+ userName);
}
break;
}
case PackageManager.DELETE_FAILED_OWNER_BLOCKED: {
UserManager userManager =
(UserManager) getSystemService(Context.USER_SERVICE);
- IPackageManager packageManager = IPackageManager.Stub.asInterface(
- ServiceManager.getService("package"));
- List<UserInfo> users = userManager.getUsers();
- int blockingUserId = UserHandle.USER_NULL;
- for (int i = 0; i < users.size(); ++i) {
- final UserInfo user = users.get(i);
- try {
- if (packageManager.getBlockUninstallForUser(packageName,
- user.id)) {
- blockingUserId = user.id;
- break;
- }
- } catch (RemoteException e) {
- // Shouldn't happen.
- Log.e(TAG, "Failed to talk to package manager", e);
+ PackageManager packageManager = ctx.getPackageManager();
+ List<UserHandle> userHandles = userManager.getUserHandles(true);
+ UserHandle otherBlockingUserHandle = null;
+ for (int i = 0; i < userHandles.size(); ++i) {
+ final UserHandle handle = userHandles.get(i);
+ if (packageManager.canUserUninstall(packageName, handle)) {
+ otherBlockingUserHandle = handle;
+ break;
}
}
- int myUserId = UserHandle.myUserId();
- if (isProfileOfOrSame(userManager, myUserId, blockingUserId)) {
+ UserHandle myUserHandle = Process.myUserHandle();
+ if (isProfileOfOrSame(userManager, myUserHandle, otherBlockingUserHandle)) {
getProgressFragment().setDeviceManagerButtonVisible(true);
} else {
getProgressFragment().setDeviceManagerButtonVisible(false);
getProgressFragment().setUsersButtonVisible(true);
}
// TODO: b/25442806
- if (blockingUserId == UserHandle.USER_SYSTEM) {
+ if (otherBlockingUserHandle == UserHandle.SYSTEM) {
statusText = getString(R.string.uninstall_blocked_device_owner);
- } else if (blockingUserId == UserHandle.USER_NULL) {
+ } else if (otherBlockingUserHandle == null) {
Log.d(TAG, "Uninstall failed for " + packageName + " with code "
+ msg.arg1 + " no blocking user");
statusText = getString(R.string.uninstall_failed);
@@ -239,12 +228,13 @@
}
}
- private boolean isProfileOfOrSame(UserManager userManager, int userId, int profileId) {
- if (userId == profileId) {
+ private boolean isProfileOfOrSame(UserManager userManager, UserHandle userHandle,
+ UserHandle profileHandle) {
+ if (userHandle.equals(profileHandle)) {
return true;
}
- UserInfo parentUser = userManager.getProfileParent(profileId);
- return parentUser != null && parentUser.id == userId;
+ return userManager.getProfileParent(profileHandle) != null
+ && userManager.getProfileParent(profileHandle).equals(userHandle);
}
@Override
@@ -253,7 +243,8 @@
Intent intent = getIntent();
mAppInfo = intent.getParcelableExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO);
- mCallback = intent.getIBinderExtra(PackageInstaller.EXTRA_CALLBACK);
+ mCallback = intent.getParcelableExtra(PackageInstaller.EXTRA_CALLBACK,
+ PackageManager.UninstallCompleteCallback.class);
// This currently does not support going through a onDestroy->onCreate cycle. Hence if that
// happened, just fail the operation for mysterious reasons.
@@ -261,12 +252,7 @@
mResultCode = PackageManager.DELETE_FAILED_INTERNAL_ERROR;
if (mCallback != null) {
- final IPackageDeleteObserver2 observer = IPackageDeleteObserver2.Stub
- .asInterface(mCallback);
- try {
- observer.onPackageDeleted(mAppInfo.packageName, mResultCode, null);
- } catch (RemoteException ignored) {
- }
+ mCallback.onUninstallComplete(mAppInfo.packageName, mResultCode, null);
finish();
} else {
setResultAndFinish();
@@ -278,10 +264,9 @@
mAllUsers = intent.getBooleanExtra(Intent.EXTRA_UNINSTALL_ALL_USERS, false);
UserHandle user = intent.getParcelableExtra(Intent.EXTRA_USER);
if (user == null) {
- user = android.os.Process.myUserHandle();
+ user = Process.myUserHandle();
}
- PackageDeleteObserver observer = new PackageDeleteObserver();
// Make window transparent until initView is called. In many cases we can avoid showing the
// UI at all as the app is uninstalled very quickly. If we show the UI and instantly remove
@@ -291,11 +276,29 @@
getWindow().setNavigationBarColor(Color.TRANSPARENT);
try {
- getPackageManager().deletePackageAsUser(mAppInfo.packageName, observer,
- mAllUsers ? PackageManager.DELETE_ALL_USERS : 0, user.getIdentifier());
+ int uninstallId = UninstallEventReceiver.addObserver(this,
+ EventResultPersister.GENERATE_NEW_ID, this);
+
+ Intent broadcastIntent = new Intent(BROADCAST_ACTION);
+ broadcastIntent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+ broadcastIntent.putExtra(EventResultPersister.EXTRA_ID, uninstallId);
+ broadcastIntent.setPackage(getPackageName());
+
+ PendingIntent pendingIntent = PendingIntent.getBroadcast(this, uninstallId,
+ broadcastIntent, PendingIntent.FLAG_UPDATE_CURRENT
+ | PendingIntent.FLAG_MUTABLE);
+
+ createContextAsUser(user, 0).getPackageManager().getPackageInstaller().uninstall(
+ new VersionedPackage(mAppInfo.packageName, PackageManager.VERSION_CODE_HIGHEST),
+ mAllUsers ? PackageManager.DELETE_ALL_USERS : 0,
+ pendingIntent.getIntentSender());
} catch (IllegalArgumentException e) {
// Couldn't find the package, no need to call uninstall.
Log.w(TAG, "Could not find package, not deleting " + mAppInfo.packageName, e);
+ } catch (EventResultPersister.OutOfIdsException e) {
+ Log.e(TAG, "Fails to start uninstall", e);
+ onResult(PackageInstaller.STATUS_FAILURE, PackageManager.DELETE_FAILED_INTERNAL_ERROR,
+ null, 0);
}
mHandler.sendMessageDelayed(mHandler.obtainMessage(UNINSTALL_IS_SLOW),
@@ -306,13 +309,12 @@
return mAppInfo;
}
- private class PackageDeleteObserver extends IPackageDeleteObserver.Stub {
- public void packageDeleted(String packageName, int returnCode) {
- Message msg = mHandler.obtainMessage(UNINSTALL_COMPLETE);
- msg.arg1 = returnCode;
- msg.obj = packageName;
- mHandler.sendMessage(msg);
- }
+ @Override
+ public void onResult(int status, int legacyStatus, @Nullable String message, int serviceId) {
+ Message msg = mHandler.obtainMessage(UNINSTALL_COMPLETE);
+ msg.arg1 = legacyStatus;
+ msg.obj = mAppInfo.packageName;
+ mHandler.sendMessage(msg);
}
public void setResultAndFinish() {
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/television/UninstallAppProgressFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/television/UninstallAppProgressFragment.java
index af6d9c5..c2d95b2 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/television/UninstallAppProgressFragment.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/television/UninstallAppProgressFragment.java
@@ -16,7 +16,6 @@
package com.android.packageinstaller.television;
-import android.annotation.Nullable;
import android.app.Fragment;
import android.content.Intent;
import android.os.Bundle;
@@ -28,6 +27,8 @@
import android.widget.Button;
import android.widget.TextView;
+import androidx.annotation.Nullable;
+
import com.android.packageinstaller.PackageUtil;
import com.android.packageinstaller.R;
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/wear/PackageInstallerImpl.java b/packages/PackageInstaller/src/com/android/packageinstaller/wear/PackageInstallerImpl.java
index 063d789..8dd691d 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/wear/PackageInstallerImpl.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/wear/PackageInstallerImpl.java
@@ -265,7 +265,7 @@
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(action);
mContext.registerReceiver(broadcastReceiver, intentFilter,
- Context.RECEIVER_EXPORTED_UNAUDITED);
+ Context.RECEIVER_EXPORTED);
// Create a matching PendingIntent and use it to generate the IntentSender
Intent broadcastIntent = new Intent(action);
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageInstallerService.java b/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageInstallerService.java
index 06b1c16..959257f 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageInstallerService.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageInstallerService.java
@@ -19,14 +19,16 @@
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
+import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.FeatureInfo;
-import android.content.pm.IPackageDeleteObserver;
import android.content.pm.PackageInfo;
+import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
+import android.content.pm.VersionedPackage;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
@@ -43,13 +45,18 @@
import android.util.Log;
import android.util.Pair;
+import androidx.annotation.Nullable;
+
import com.android.packageinstaller.DeviceUtils;
+import com.android.packageinstaller.EventResultPersister;
import com.android.packageinstaller.PackageUtil;
import com.android.packageinstaller.R;
+import com.android.packageinstaller.UninstallEventReceiver;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Arrays;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -80,16 +87,30 @@
* adb shell am startservice -a com.android.packageinstaller.wear.RETRY_GMS \
* com.android.packageinstaller/com.android.packageinstaller.wear.WearPackageInstallerService
*/
-public class WearPackageInstallerService extends Service {
+public class WearPackageInstallerService extends Service
+ implements EventResultPersister.EventResultObserver {
private static final String TAG = "WearPkgInstallerService";
private static final String WEAR_APPS_CHANNEL = "wear_app_install_uninstall";
+ private static final String BROADCAST_ACTION =
+ "com.android.packageinstaller.ACTION_UNINSTALL_COMMIT";
private final int START_INSTALL = 1;
private final int START_UNINSTALL = 2;
private int mInstallNotificationId = 1;
private final Map<String, Integer> mNotifIdMap = new ArrayMap<>();
+ private final Map<Integer, UninstallParams> mServiceIdToParams = new HashMap<>();
+
+ private class UninstallParams {
+ public String mPackageName;
+ public PowerManager.WakeLock mLock;
+
+ UninstallParams(String packageName, PowerManager.WakeLock lock) {
+ mPackageName = packageName;
+ mLock = lock;
+ }
+ }
private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
@@ -211,7 +232,6 @@
}
final PackageManager pm = getPackageManager();
File tempFile = null;
- int installFlags = 0;
PowerManager.WakeLock lock = getLock(this.getApplicationContext());
boolean messageSent = false;
try {
@@ -220,17 +240,14 @@
existingPkgInfo = pm.getPackageInfo(packageName,
PackageManager.MATCH_ANY_USER | PackageManager.GET_PERMISSIONS);
if (existingPkgInfo != null) {
- installFlags |= PackageManager.INSTALL_REPLACE_EXISTING;
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Replacing package:" + packageName);
+ }
}
} catch (PackageManager.NameNotFoundException e) {
// Ignore this exception. We could not find the package, will treat as a new
// installation.
}
- if ((installFlags & PackageManager.INSTALL_REPLACE_EXISTING) != 0) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Replacing package:" + packageName);
- }
- }
// TODO(28021618): This was left as a temp file due to the fact that this code is being
// deprecated and that we need the bare minimum to continue working moving forward
// If this code is used as reference, this permission logic might want to be
@@ -366,21 +383,60 @@
final String packageName = WearPackageArgs.getPackageName(argsBundle);
PowerManager.WakeLock lock = getLock(this.getApplicationContext());
+
+ UninstallParams params = new UninstallParams(packageName, lock);
+ mServiceIdToParams.put(startId, params);
+
final PackageManager pm = getPackageManager();
try {
PackageInfo pkgInfo = pm.getPackageInfo(packageName, 0);
getLabelAndUpdateNotification(packageName,
getString(R.string.uninstalling_app, pkgInfo.applicationInfo.loadLabel(pm)));
+ int uninstallId = UninstallEventReceiver.addObserver(this,
+ EventResultPersister.GENERATE_NEW_ID, this);
+
+ Intent broadcastIntent = new Intent(BROADCAST_ACTION);
+ broadcastIntent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+ broadcastIntent.putExtra(EventResultPersister.EXTRA_ID, uninstallId);
+ broadcastIntent.putExtra(EventResultPersister.EXTRA_SERVICE_ID, startId);
+ broadcastIntent.setPackage(getPackageName());
+
+ PendingIntent pendingIntent = PendingIntent.getBroadcast(this, uninstallId,
+ broadcastIntent, PendingIntent.FLAG_UPDATE_CURRENT
+ | PendingIntent.FLAG_MUTABLE);
+
// Found package, send uninstall request.
- pm.deletePackage(packageName, new PackageDeleteObserver(lock, startId),
- PackageManager.DELETE_ALL_USERS);
+ pm.getPackageInstaller().uninstall(
+ new VersionedPackage(packageName, PackageManager.VERSION_CODE_HIGHEST),
+ PackageManager.DELETE_ALL_USERS,
+ pendingIntent.getIntentSender());
Log.i(TAG, "Sent delete request for " + packageName);
} catch (IllegalArgumentException | PackageManager.NameNotFoundException e) {
// Couldn't find the package, no need to call uninstall.
Log.w(TAG, "Could not find package, not deleting " + packageName, e);
finishService(lock, startId);
+ } catch (EventResultPersister.OutOfIdsException e) {
+ Log.e(TAG, "Fails to start uninstall", e);
+ finishService(lock, startId);
+ }
+ }
+
+ @Override
+ public void onResult(int status, int legacyStatus, @Nullable String message, int serviceId) {
+ if (mServiceIdToParams.containsKey(serviceId)) {
+ UninstallParams params = mServiceIdToParams.get(serviceId);
+ try {
+ if (status == PackageInstaller.STATUS_SUCCESS) {
+ Log.i(TAG, "Package " + params.mPackageName + " was uninstalled.");
+ } else {
+ Log.e(TAG, "Package uninstall failed " + params.mPackageName
+ + ", returnCode " + legacyStatus);
+ }
+ } finally {
+ finishService(params.mLock, serviceId);
+ }
}
}
@@ -537,29 +593,6 @@
}
}
- private class PackageDeleteObserver extends IPackageDeleteObserver.Stub {
- private PowerManager.WakeLock mWakeLock;
- private int mStartId;
-
- private PackageDeleteObserver(PowerManager.WakeLock wakeLock, int startId) {
- mWakeLock = wakeLock;
- mStartId = startId;
- }
-
- public void packageDeleted(String packageName, int returnCode) {
- try {
- if (returnCode >= 0) {
- Log.i(TAG, "Package " + packageName + " was uninstalled.");
- } else {
- Log.e(TAG, "Package uninstall failed " + packageName + ", returnCode " +
- returnCode);
- }
- } finally {
- finishService(mWakeLock, mStartId);
- }
- }
- }
-
private synchronized Pair<Integer, Notification> buildNotification(final String packageName,
final String title) {
int notifId;
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsController.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsController.kt
index c08169e..e878804 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsController.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsController.kt
@@ -41,6 +41,7 @@
context: Context,
private val app: ApplicationInfo,
private val op: Int,
+ private val setModeByUid: Boolean = false,
) : IAppOpsController {
private val appOpsManager = context.appOpsManager
@@ -49,7 +50,11 @@
override fun setAllowed(allowed: Boolean) {
val mode = if (allowed) MODE_ALLOWED else MODE_ERRORED
- appOpsManager.setMode(op, app.uid, app.packageName, mode)
+ if (setModeByUid) {
+ appOpsManager.setUidMode(op, app.uid, mode)
+ } else {
+ appOpsManager.setMode(op, app.uid, app.packageName, mode)
+ }
_mode.postValue(mode)
}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppList.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppList.kt
index a357832..ee21b81 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppList.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppList.kt
@@ -49,6 +49,13 @@
abstract val appOp: Int
abstract val permission: String
+ /**
+ * Use AppOpsManager#setUidMode() instead of AppOpsManager#setMode() when set allowed.
+ *
+ * Security related app-ops should be set with setUidMode() instead of setMode().
+ */
+ open val setModeByUid = false
+
/** These not changeable packages will also be hidden from app list. */
private val notChangeablePackages =
setOf("android", "com.android.systemui", context.packageName)
@@ -61,7 +68,7 @@
AppOpPermissionRecord(
app = app,
hasRequestPermission = app.packageName in packageNames,
- appOpsController = AppOpsController(context = context, app = app, op = appOp),
+ appOpsController = createAppOpsController(app),
)
}
}
@@ -69,7 +76,14 @@
override fun transformItem(app: ApplicationInfo) = AppOpPermissionRecord(
app = app,
hasRequestPermission = with(packageManagers) { app.hasRequestPermission(permission) },
- appOpsController = AppOpsController(context = context, app = app, op = appOp),
+ appOpsController = createAppOpsController(app),
+ )
+
+ private fun createAppOpsController(app: ApplicationInfo) = AppOpsController(
+ context = context,
+ app = app,
+ op = appOp,
+ setModeByUid = setModeByUid,
)
override fun filter(userIdFlow: Flow<Int>, recordListFlow: Flow<List<AppOpPermissionRecord>>) =
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppOpsControllerTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppOpsControllerTest.kt
new file mode 100644
index 0000000..668bfdf
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppOpsControllerTest.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spaprivileged.model.app
+
+import android.app.AppOpsManager
+import android.app.AppOpsManager.MODE_ALLOWED
+import android.app.AppOpsManager.MODE_ERRORED
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spaprivileged.framework.common.appOpsManager
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.Spy
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.Mockito.`when` as whenever
+
+@RunWith(AndroidJUnit4::class)
+class AppOpsControllerTest {
+ @get:Rule
+ val mockito: MockitoRule = MockitoJUnit.rule()
+
+ @Spy
+ private val context: Context = ApplicationProvider.getApplicationContext()
+
+ @Mock
+ private lateinit var appOpsManager: AppOpsManager
+
+ @Before
+ fun setUp() {
+ whenever(context.appOpsManager).thenReturn(appOpsManager)
+ }
+
+ @Test
+ fun setAllowed_setToTrue() {
+ val controller = AppOpsController(context = context, app = APP, op = OP)
+
+ controller.setAllowed(true)
+
+ verify(appOpsManager).setMode(OP, APP.uid, APP.packageName, MODE_ALLOWED)
+ }
+
+ @Test
+ fun setAllowed_setToFalse() {
+ val controller = AppOpsController(context = context, app = APP, op = OP)
+
+ controller.setAllowed(false)
+
+ verify(appOpsManager).setMode(OP, APP.uid, APP.packageName, MODE_ERRORED)
+ }
+
+ @Test
+ fun setAllowed_setToTrueByUid() {
+ val controller =
+ AppOpsController(context = context, app = APP, op = OP, setModeByUid = true)
+
+ controller.setAllowed(true)
+
+ verify(appOpsManager).setUidMode(OP, APP.uid, MODE_ALLOWED)
+ }
+
+ @Test
+ fun setAllowed_setToFalseByUid() {
+ val controller =
+ AppOpsController(context = context, app = APP, op = OP, setModeByUid = true)
+
+ controller.setAllowed(false)
+
+ verify(appOpsManager).setUidMode(OP, APP.uid, MODE_ERRORED)
+ }
+
+ @Test
+ fun getMode() {
+ whenever(
+ appOpsManager.checkOpNoThrow(OP, APP.uid, APP.packageName)
+ ).thenReturn(MODE_ALLOWED)
+ val controller = AppOpsController(context = context, app = APP, op = OP)
+
+ val mode = controller.getMode()
+
+ assertThat(mode).isEqualTo(MODE_ALLOWED)
+ }
+
+ private companion object {
+ const val OP = 1
+ val APP = ApplicationInfo().apply {
+ packageName = "package.name"
+ uid = 123
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt
index cd9c048..966b869 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt
@@ -25,6 +25,7 @@
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull
+import com.android.settingslib.spaprivileged.framework.common.appOpsManager
import com.android.settingslib.spaprivileged.model.app.IAppOpsController
import com.android.settingslib.spaprivileged.model.app.IPackageManagers
import com.android.settingslib.spaprivileged.test.R
@@ -37,6 +38,8 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.Spy
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
import org.mockito.Mockito.`when` as whenever
@@ -50,17 +53,20 @@
@get:Rule
val composeTestRule = createComposeRule()
+ @Spy
private val context: Context = ApplicationProvider.getApplicationContext()
@Mock
private lateinit var packageManagers: IPackageManagers
+ @Mock
+ private lateinit var appOpsManager: AppOpsManager
+
private lateinit var listModel: TestAppOpPermissionAppListModel
@Before
- fun setUp() = runTest {
- whenever(packageManagers.getAppOpPermissionPackages(USER_ID, PERMISSION))
- .thenReturn(emptySet())
+ fun setUp() {
+ whenever(context.appOpsManager).thenReturn(appOpsManager)
listModel = TestAppOpPermissionAppListModel()
}
@@ -221,6 +227,16 @@
assertThat(appOpsController.setAllowedCalledWith).isTrue()
}
+ @Test
+ fun setAllowed_setModeByUid() {
+ listModel.setModeByUid = true
+ val record = listModel.transformItem(APP)
+
+ listModel.setAllowed(record = record, newAllowed = true)
+
+ verify(appOpsManager).setUidMode(listModel.appOp, APP.uid, AppOpsManager.MODE_ALLOWED)
+ }
+
private fun getIsAllowed(record: AppOpPermissionRecord): Boolean? {
lateinit var isAllowedState: State<Boolean?>
composeTestRule.setContent {
@@ -236,6 +252,7 @@
override val footerResId = R.string.test_app_op_permission_footer
override val appOp = AppOpsManager.OP_MANAGE_MEDIA
override val permission = PERMISSION
+ override var setModeByUid = false
}
private companion object {
diff --git a/packages/SettingsLib/res/layout/grant_admin_dialog_content.xml b/packages/SettingsLib/res/layout/grant_admin_dialog_content.xml
new file mode 100644
index 0000000..d6acac2
--- /dev/null
+++ b/packages/SettingsLib/res/layout/grant_admin_dialog_content.xml
@@ -0,0 +1,37 @@
+<!--
+ ~ 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.
+ -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:padding="@dimen/grant_admin_dialog_padding">
+ <RadioGroup
+ android:id="@+id/choose_admin"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+ <RadioButton
+ android:id="@+id/grant_admin_yes"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/grant_admin"/>
+ <RadioButton
+ android:id="@+id/grant_admin_no"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/not_grant_admin"/>
+ </RadioGroup>
+</LinearLayout>
diff --git a/packages/SettingsLib/res/values/dimens.xml b/packages/SettingsLib/res/values/dimens.xml
index 3ef3d36..dbfd1c2 100644
--- a/packages/SettingsLib/res/values/dimens.xml
+++ b/packages/SettingsLib/res/values/dimens.xml
@@ -109,4 +109,7 @@
<dimen name="broadcast_dialog_btn_text_size">16sp</dimen>
<dimen name="broadcast_dialog_btn_minHeight">44dp</dimen>
<dimen name="broadcast_dialog_margin">16dp</dimen>
+
+ <!-- Size of grant admin privileges dialog padding -->
+ <dimen name="grant_admin_dialog_padding">16dp</dimen>
</resources>
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index e675db4..2845916 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -1368,6 +1368,10 @@
<string name="user_add_user_message_long">You can share this device with other people by creating additional users. Each user has their own space, which they can customize with apps, wallpaper, and so on. Users can also adjust device settings like Wi\u2011Fi that affect everyone.\n\nWhen you add a new user, that person needs to set up their space.\n\nAny user can update apps for all other users. Accessibility settings and services may not transfer to the new user.</string>
<!-- Message for add user confirmation dialog - short version. [CHAR LIMIT=none] -->
<string name="user_add_user_message_short">When you add a new user, that person needs to set up their space.\n\nAny user can update apps for all other users. </string>
+ <!-- Title for grant user admin privileges dialog [CHAR LIMIT=30] -->
+ <string name="user_grant_admin_title">Give this user admin privileges?</string>
+ <!-- Message for grant admin privileges dialog. [CHAR LIMIT=none] -->
+ <string name="user_grant_admin_message">As an admin, they will be able to manage other users, modify device settings and factory reset the device.</string>
<!-- Title of dialog to setup a new user [CHAR LIMIT=30] -->
<string name="user_setup_dialog_title">Set up user now?</string>
<!-- Message in dialog to setup a new user after creation [CHAR LIMIT=none] -->
@@ -1434,6 +1438,10 @@
<!-- Dialog message on action exit guest (ephemeral guest) [CHAR LIMIT=80] -->
<string name="guest_exit_dialog_message">This will delete
apps and data from the current guest session</string>
+ <!-- Dialog message on action grant admin privileges [CHAR LIMIT=60] -->
+ <string name="grant_admin">Give this user admin privileges</string>
+ <!-- Dialog message on action not grant admin privileges [CHAR LIMIT=60] -->
+ <string name="not_grant_admin">Do not give user admin privileges</string>
<!-- Dialog button on action exit guest (ephemeral guest) [CHAR LIMIT=80] -->
<string name="guest_exit_dialog_button">Exit</string>
<!-- Dialog title on action exit guest (non-ephemeral guest) [CHAR LIMIT=32] -->
diff --git a/packages/SettingsLib/src/com/android/settingslib/Utils.java b/packages/SettingsLib/src/com/android/settingslib/Utils.java
index 8a30a3b..888b09f 100644
--- a/packages/SettingsLib/src/com/android/settingslib/Utils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/Utils.java
@@ -653,6 +653,9 @@
}
for (UsbPort usbPort : usbPortList) {
Log.d(tag, "usbPort: " + usbPort);
+ if (!usbPort.supportsComplianceWarnings()) {
+ continue;
+ }
final UsbPortStatus usbStatus = usbPort.getStatus();
if (usbStatus == null || !usbStatus.isConnected()) {
continue;
diff --git a/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryKey.java b/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryKey.java
index 4da47fd..db224be 100644
--- a/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryKey.java
+++ b/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryKey.java
@@ -66,6 +66,8 @@
"com.android.settings.category.ia.battery_saver_settings";
public static final String CATEGORY_SMART_BATTERY_SETTINGS =
"com.android.settings.category.ia.smart_battery_settings";
+ public static final String CATEGORY_COMMUNAL_SETTINGS =
+ "com.android.settings.category.ia.communal";
public static final Map<String, String> KEY_COMPAT_MAP;
diff --git a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java
index 3e710e4..28353ab 100644
--- a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java
@@ -22,6 +22,7 @@
import android.os.Bundle;
import android.os.PowerManager;
import android.os.UserHandle;
+import android.provider.Settings;
import android.provider.Settings.Global;
import android.provider.Settings.Secure;
import android.util.KeyValueListParser;
@@ -55,6 +56,10 @@
public static final String EXTRA_POWER_SAVE_MODE_TRIGGER_LEVEL =
"extra_power_save_mode_trigger_level";
+ /** Battery saver schedule keys. */
+ public static final String KEY_NO_SCHEDULE = "key_battery_saver_no_schedule";
+ public static final String KEY_PERCENTAGE = "key_battery_saver_percentage";
+
private BatterySaverUtils() {
}
@@ -108,7 +113,6 @@
* - If it's 4th time through 8th time, show the schedule suggestion notification.
*
* @param enable true to enable battery saver.
- *
* @return true if the request succeeded.
*/
public static synchronized boolean setPowerSaveMode(Context context,
@@ -154,10 +158,10 @@
* Shows the battery saver confirmation warning if it hasn't been acknowledged by the user in
* the past before. Various extras can be provided that will change the behavior of this
* notification as well as the ui for it.
- * @param context A valid context
- * @param extras Any extras to include in the intent to trigger this confirmation that will
- * help the system disambiguate what to show/do
*
+ * @param context A valid context
+ * @param extras Any extras to include in the intent to trigger this confirmation that will
+ * help the system disambiguate what to show/do
* @return True if it showed the notification because it has not been previously acknowledged.
* @see #EXTRA_CONFIRM_TEXT_ONLY
* @see #EXTRA_POWER_SAVE_MODE_TRIGGER
@@ -221,6 +225,7 @@
/**
* Reverts battery saver schedule mode to none if routine mode is selected.
+ *
* @param context a valid context
*/
public static void revertScheduleToNoneIfNeeded(Context context) {
@@ -233,4 +238,50 @@
PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE);
}
}
+
+ /**
+ * Gets battery saver schedule mode.
+ *
+ * @param context a valid context
+ * @return battery saver schedule key
+ */
+ public static String getBatterySaverScheduleKey(Context context) {
+ final ContentResolver resolver = context.getContentResolver();
+ final int mode = Settings.Global.getInt(resolver, Global.AUTOMATIC_POWER_SAVE_MODE,
+ PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE);
+ if (mode == PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE) {
+ final int threshold =
+ Settings.Global.getInt(resolver, Global.LOW_POWER_MODE_TRIGGER_LEVEL, 0);
+ return threshold <= 0 ? KEY_NO_SCHEDULE : KEY_PERCENTAGE;
+ }
+ revertScheduleToNoneIfNeeded(context);
+ return KEY_NO_SCHEDULE;
+ }
+
+ /**
+ * Sets battery saver schedule mode.
+ *
+ * @param context a valid context
+ * @param scheduleKey {@link #KEY_NO_SCHEDULE} and {@link #KEY_PERCENTAGE}
+ * @param triggerLevel for automatic battery saver trigger level
+ */
+ public static void setBatterySaverScheduleMode(Context context, String scheduleKey,
+ int triggerLevel) {
+ final ContentResolver resolver = context.getContentResolver();
+ switch (scheduleKey) {
+ case KEY_NO_SCHEDULE:
+ Settings.Global.putInt(resolver, Global.AUTOMATIC_POWER_SAVE_MODE,
+ PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE);
+ Settings.Global.putInt(resolver, Settings.Global.LOW_POWER_MODE_TRIGGER_LEVEL, 0);
+ break;
+ case KEY_PERCENTAGE:
+ Settings.Global.putInt(resolver, Global.AUTOMATIC_POWER_SAVE_MODE,
+ PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE);
+ Settings.Global.putInt(resolver,
+ Settings.Global.LOW_POWER_MODE_TRIGGER_LEVEL, triggerLevel);
+ break;
+ default:
+ throw new IllegalStateException("Not a valid schedule key");
+ }
+ }
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/users/GrantAdminDialogController.java b/packages/SettingsLib/src/com/android/settingslib/users/GrantAdminDialogController.java
new file mode 100644
index 0000000..5cc8d5e
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/users/GrantAdminDialogController.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.users;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.RadioButton;
+import android.widget.RadioGroup;
+
+import com.android.settingslib.R;
+
+import java.util.function.Consumer;
+
+/**
+ * This class encapsulates a Dialog for choosing whether to grant admin privileges.
+ */
+public class GrantAdminDialogController {
+
+ /**
+ * Creates a dialog with option to grant user admin privileges.
+ */
+ public Dialog createDialog(Activity activity,
+ Consumer<Boolean> successCallback, Runnable cancelCallback) {
+ LayoutInflater inflater = LayoutInflater.from(activity);
+ View content = inflater.inflate(R.layout.grant_admin_dialog_content, null);
+ RadioGroup radioGroup = content.findViewById(R.id.choose_admin);
+ RadioButton radioButton = radioGroup.findViewById(R.id.grant_admin_yes);
+ radioButton.setChecked(true);
+ Dialog dlg = new AlertDialog.Builder(activity)
+ .setView(content)
+ .setTitle(R.string.user_grant_admin_title)
+ .setMessage(R.string.user_grant_admin_message)
+ .setPositiveButton(android.R.string.ok,
+ (dialog, which) -> {
+ if (successCallback != null) {
+ successCallback.accept(radioButton.isChecked());
+ }
+ })
+ .setNegativeButton(android.R.string.cancel, (dialog, which) -> {
+ if (cancelCallback != null) {
+ cancelCallback.run();
+ }
+ })
+ .setOnCancelListener(dialog -> {
+ if (cancelCallback != null) {
+ cancelCallback.run();
+ }
+ })
+ .create();
+
+ return dlg;
+ }
+
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java
index 68a1e19..dce1e20 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java
@@ -448,25 +448,41 @@
@Test
public void containsIncompatibleChargers_returnTrue() {
- final List<UsbPort> usbPorts = new ArrayList<>();
- usbPorts.add(mUsbPort);
- when(mUsbManager.getPorts()).thenReturn(usbPorts);
- when(mUsbPort.getStatus()).thenReturn(mUsbPortStatus);
- when(mUsbPortStatus.isConnected()).thenReturn(true);
- when(mUsbPortStatus.getComplianceWarnings()).thenReturn(new int[]{1});
-
+ setupIncompatibleCharging();
assertThat(Utils.containsIncompatibleChargers(mContext, "tag")).isTrue();
}
@Test
public void containsIncompatibleChargers_emptyComplianceWarnings_returnFalse() {
+ setupIncompatibleCharging();
+ when(mUsbPortStatus.getComplianceWarnings()).thenReturn(new int[1]);
+
+ assertThat(Utils.containsIncompatibleChargers(mContext, "tag")).isFalse();
+ }
+
+ @Test
+ public void containsIncompatibleChargers_notSupportComplianceWarnings_returnFalse() {
+ setupIncompatibleCharging();
+ when(mUsbPort.supportsComplianceWarnings()).thenReturn(false);
+
+ assertThat(Utils.containsIncompatibleChargers(mContext, "tag")).isFalse();
+ }
+
+ @Test
+ public void containsIncompatibleChargers_usbNotConnected_returnFalse() {
+ setupIncompatibleCharging();
+ when(mUsbPortStatus.isConnected()).thenReturn(false);
+
+ assertThat(Utils.containsIncompatibleChargers(mContext, "tag")).isFalse();
+ }
+
+ private void setupIncompatibleCharging() {
final List<UsbPort> usbPorts = new ArrayList<>();
usbPorts.add(mUsbPort);
when(mUsbManager.getPorts()).thenReturn(usbPorts);
when(mUsbPort.getStatus()).thenReturn(mUsbPortStatus);
+ when(mUsbPort.supportsComplianceWarnings()).thenReturn(true);
when(mUsbPortStatus.isConnected()).thenReturn(true);
- when(mUsbPortStatus.getComplianceWarnings()).thenReturn(new int[1]);
-
- assertThat(Utils.containsIncompatibleChargers(mContext, "tag")).isFalse();
+ when(mUsbPortStatus.getComplianceWarnings()).thenReturn(new int[]{1});
}
}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/CategoryKeyTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/CategoryKeyTest.java
index 340a6c7..c9dc1ba 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/CategoryKeyTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/CategoryKeyTest.java
@@ -60,8 +60,9 @@
allKeys.add(CategoryKey.CATEGORY_GESTURES);
allKeys.add(CategoryKey.CATEGORY_NIGHT_DISPLAY);
allKeys.add(CategoryKey.CATEGORY_SMART_BATTERY_SETTINGS);
+ allKeys.add(CategoryKey.CATEGORY_COMMUNAL_SETTINGS);
// DO NOT REMOVE ANYTHING ABOVE
- assertThat(allKeys.size()).isEqualTo(19);
+ assertThat(allKeys.size()).isEqualTo(20);
}
}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/BatterySaverUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/BatterySaverUtilsTest.java
index 2bb3c2a..a15fe9f 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/BatterySaverUtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/BatterySaverUtilsTest.java
@@ -16,6 +16,9 @@
package com.android.settingslib.fuelgauge;
+import static com.android.settingslib.fuelgauge.BatterySaverUtils.KEY_NO_SCHEDULE;
+import static com.android.settingslib.fuelgauge.BatterySaverUtils.KEY_PERCENTAGE;
+
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
@@ -186,4 +189,46 @@
assertThat(Secure.getInt(mMockResolver, Secure.SUPPRESS_AUTO_BATTERY_SAVER_SUGGESTION, -1))
.isEqualTo(1);
}
+
+ @Test
+ public void testGetBatterySaverScheduleKey_returnExpectedKey() {
+ Global.putInt(mMockResolver, Global.LOW_POWER_MODE_TRIGGER_LEVEL, 0);
+ Global.putInt(mMockResolver, Global.AUTOMATIC_POWER_SAVE_MODE,
+ PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE);
+
+ assertThat(BatterySaverUtils.getBatterySaverScheduleKey(mMockContext)).isEqualTo(
+ KEY_NO_SCHEDULE);
+
+ Global.putInt(mMockResolver, Global.LOW_POWER_MODE_TRIGGER_LEVEL, 20);
+ Global.putInt(mMockResolver, Global.AUTOMATIC_POWER_SAVE_MODE,
+ PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE);
+
+ assertThat(BatterySaverUtils.getBatterySaverScheduleKey(mMockContext)).isEqualTo(
+ KEY_PERCENTAGE);
+
+ Global.putInt(mMockResolver, Global.LOW_POWER_MODE_TRIGGER_LEVEL, 20);
+ Global.putInt(mMockResolver, Global.AUTOMATIC_POWER_SAVE_MODE,
+ PowerManager.POWER_SAVE_MODE_TRIGGER_DYNAMIC);
+
+ assertThat(BatterySaverUtils.getBatterySaverScheduleKey(mMockContext)).isEqualTo(
+ KEY_NO_SCHEDULE);
+ }
+
+ @Test
+ public void testSetBatterySaverScheduleMode_setSchedule() {
+ BatterySaverUtils.setBatterySaverScheduleMode(mMockContext, KEY_NO_SCHEDULE, -1);
+
+ assertThat(Global.getInt(mMockResolver, Global.AUTOMATIC_POWER_SAVE_MODE, -1))
+ .isEqualTo(PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE);
+ assertThat(Global.getInt(mMockResolver, Global.LOW_POWER_MODE_TRIGGER_LEVEL, -1))
+ .isEqualTo(0);
+
+ BatterySaverUtils.setBatterySaverScheduleMode(mMockContext, KEY_PERCENTAGE, 20);
+
+ assertThat(Global.getInt(mMockResolver, Global.AUTOMATIC_POWER_SAVE_MODE, -1))
+ .isEqualTo(PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE);
+ assertThat(Global.getInt(mMockResolver, Global.LOW_POWER_MODE_TRIGGER_LEVEL, -1))
+ .isEqualTo(20);
+
+ }
}
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index ed1a0f3..1356e1d 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -96,6 +96,7 @@
import android.provider.Settings.Global;
import android.provider.Settings.Secure;
import android.provider.Settings.SetAllResult;
+import android.provider.UpdatableDeviceConfigServiceReadiness;
import android.provider.settings.validators.SystemSettingsValidators;
import android.provider.settings.validators.Validator;
import android.text.TextUtils;
@@ -416,10 +417,16 @@
startWatchingUserRestrictionChanges();
});
ServiceManager.addService("settings", new SettingsService(this));
- ServiceManager.addService("device_config", new DeviceConfigService(this));
+ addDeviceConfigServiceIfNeeded();
return true;
}
+ private void addDeviceConfigServiceIfNeeded() {
+ if (!UpdatableDeviceConfigServiceReadiness.shouldStartUpdatableService()) {
+ ServiceManager.addService("device_config", new DeviceConfigService(this));
+ }
+ }
+
@Override
public Bundle call(String method, String name, Bundle args) {
final int requestingUserId = getRequestingUserId(args);
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index e1000e0..220c16a 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -31,6 +31,52 @@
],
}
+// Opt-in configuration for code depending on Jetpack Compose.
+soong_config_module_type {
+ name: "systemui_compose_java_defaults",
+ module_type: "java_defaults",
+ config_namespace: "ANDROID",
+ bool_variables: ["SYSTEMUI_USE_COMPOSE"],
+ properties: [
+ "srcs",
+ "static_libs",
+ ],
+}
+
+systemui_compose_java_defaults {
+ name: "SystemUI_compose_defaults",
+ soong_config_variables: {
+ SYSTEMUI_USE_COMPOSE: {
+ // Because files in compose/features/ depend on SystemUI
+ // code, we compile those files when compiling SystemUI-core.
+ // We also compile the ComposeFacade in
+ // compose/facade/enabled/.
+ srcs: [
+ "compose/features/src/**/*.kt",
+ "compose/facade/enabled/src/**/*.kt",
+ ],
+
+ // The dependencies needed by SystemUIComposeFeatures,
+ // except for SystemUI-core.
+ // Copied from compose/features/Android.bp.
+ static_libs: [
+ "SystemUIComposeCore",
+
+ "androidx.compose.runtime_runtime",
+ "androidx.compose.material3_material3",
+ "androidx.activity_activity-compose",
+ ],
+
+ // By default, Compose is disabled and we compile the ComposeFacade
+ // in compose/facade/disabled/.
+ conditions_default: {
+ srcs: ["compose/facade/disabled/src/**/*.kt"],
+ static_libs: [],
+ },
+ },
+ },
+}
+
java_library {
name: "SystemUI-proto",
@@ -68,6 +114,9 @@
android_library {
name: "SystemUI-core",
+ defaults: [
+ "SystemUI_compose_defaults",
+ ],
srcs: [
"src/**/*.kt",
"src/**/*.java",
@@ -228,6 +277,9 @@
android_library {
name: "SystemUI-tests",
+ defaults: [
+ "SystemUI_compose_defaults",
+ ],
manifest: "tests/AndroidManifest-base.xml",
additional_manifests: ["tests/AndroidManifest.xml"],
srcs: [
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewRootSync.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewRootSync.kt
index 163cab4..fd9355d 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewRootSync.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewRootSync.kt
@@ -28,7 +28,7 @@
return
}
- val syncGroup = SurfaceSyncGroup()
+ val syncGroup = SurfaceSyncGroup("SysUIAnimation")
syncGroup.addSyncCompleteCallback(view.context.mainExecutor) { then() }
syncGroup.addToSync(view.rootSurfaceControl)
syncGroup.addToSync(otherView.rootSurfaceControl)
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt
index f55fb97..9058510 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt
@@ -169,11 +169,9 @@
setFloatUniform("in_progress", value)
val curvedProg = 1 - (1 - value) * (1 - value) * (1 - value)
- setFloatUniform(
- "in_size",
- /* width= */ maxSize.x * curvedProg,
- /* height= */ maxSize.y * curvedProg
- )
+ currentWidth = maxSize.x * curvedProg
+ currentHeight = maxSize.y * curvedProg
+ setFloatUniform("in_size", /* width= */ currentWidth, /* height= */ currentHeight)
setFloatUniform("in_thickness", maxSize.y * curvedProg * 0.5f)
// radius should not exceed width and height values.
setFloatUniform("in_cornerRadius", Math.min(maxSize.x, maxSize.y) * curvedProg)
@@ -237,4 +235,10 @@
* False for a ring effect.
*/
var rippleFill: Boolean = false
+
+ var currentWidth: Float = 0f
+ private set
+
+ var currentHeight: Float = 0f
+ private set
}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt
index ae28a8b..b37c734 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt
@@ -36,7 +36,7 @@
*/
open class RippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
- private lateinit var rippleShader: RippleShader
+ protected lateinit var rippleShader: RippleShader
lateinit var rippleShape: RippleShape
private set
diff --git a/packages/SystemUI/compose/core/src/com/android/systemui/compose/runtime/MovableContent.kt b/packages/SystemUI/compose/core/src/com/android/systemui/compose/runtime/MovableContent.kt
new file mode 100644
index 0000000..3f2f96b
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/systemui/compose/runtime/MovableContent.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.compose.runtime
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.InternalComposeApi
+import androidx.compose.runtime.MovableContent
+import androidx.compose.runtime.currentComposer
+
+/**
+ * An overload of [androidx.compose.runtime.movableContentOf] with 5 parameters.
+ *
+ * @see androidx.compose.runtime.movableContentOf
+ */
+@OptIn(InternalComposeApi::class)
+fun <P1, P2, P3, P4, P5> movableContentOf(
+ content: @Composable (P1, P2, P3, P4, P5) -> Unit
+): @Composable (P1, P2, P3, P4, P5) -> Unit {
+ val movableContent =
+ MovableContent<Pair<Triple<P1, P2, P3>, Pair<P4, P5>>> {
+ content(
+ it.first.first,
+ it.first.second,
+ it.first.third,
+ it.second.first,
+ it.second.second,
+ )
+ }
+ return { p1, p2, p3, p4, p5 ->
+ currentComposer.insertMovableContent(movableContent, Triple(p1, p2, p3) to (p4 to p5))
+ }
+}
diff --git a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
new file mode 100644
index 0000000..6e728ce
--- /dev/null
+++ b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.compose
+
+import androidx.activity.ComponentActivity
+import com.android.systemui.people.ui.viewmodel.PeopleViewModel
+
+/** The Compose facade, when Compose is *not* available. */
+object ComposeFacade : BaseComposeFacade {
+ override fun isComposeAvailable(): Boolean = false
+
+ override fun setPeopleSpaceActivityContent(
+ activity: ComponentActivity,
+ viewModel: PeopleViewModel,
+ onResult: (PeopleViewModel.Result) -> Unit,
+ ) {
+ throwComposeUnavailableError()
+ }
+
+ private fun throwComposeUnavailableError() {
+ error(
+ "Compose is not available. Make sure to check isComposeAvailable() before calling any" +
+ " other function on ComposeFacade."
+ )
+ }
+}
diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
new file mode 100644
index 0000000..16294d9
--- /dev/null
+++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.compose
+
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import com.android.systemui.compose.theme.SystemUITheme
+import com.android.systemui.people.ui.compose.PeopleScreen
+import com.android.systemui.people.ui.viewmodel.PeopleViewModel
+
+/** The Compose facade, when Compose is available. */
+object ComposeFacade : BaseComposeFacade {
+ override fun isComposeAvailable(): Boolean = true
+
+ override fun setPeopleSpaceActivityContent(
+ activity: ComponentActivity,
+ viewModel: PeopleViewModel,
+ onResult: (PeopleViewModel.Result) -> Unit,
+ ) {
+ activity.setContent { SystemUITheme { PeopleScreen(viewModel, onResult) } }
+ }
+}
diff --git a/packages/SystemUI/compose/features/Android.bp b/packages/SystemUI/compose/features/Android.bp
index 325ede6..4533330 100644
--- a/packages/SystemUI/compose/features/Android.bp
+++ b/packages/SystemUI/compose/features/Android.bp
@@ -35,6 +35,7 @@
"androidx.compose.runtime_runtime",
"androidx.compose.material3_material3",
+ "androidx.activity_activity-compose",
],
kotlincflags: ["-Xjvm-default=all"],
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
index 2aac46e..4a56b02 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
@@ -139,11 +139,20 @@
bottom = PeopleSpacePadding,
start = 8.dp,
end = 8.dp,
- )
+ ),
) {
- ConversationList(R.string.priority_conversations, priorityTiles, onTileClicked)
- item { Spacer(Modifier.height(35.dp)) }
- ConversationList(R.string.recent_conversations, recentTiles, onTileClicked)
+ val hasPriorityConversations = priorityTiles.isNotEmpty()
+ if (hasPriorityConversations) {
+ ConversationList(R.string.priority_conversations, priorityTiles, onTileClicked)
+ }
+
+ if (recentTiles.isNotEmpty()) {
+ if (hasPriorityConversations) {
+ item { Spacer(Modifier.height(35.dp)) }
+ }
+
+ ConversationList(R.string.recent_conversations, recentTiles, onTileClicked)
+ }
}
}
}
diff --git a/packages/SystemUI/res/drawable/media_ttt_chip_background_receiver.xml b/packages/SystemUI/res/drawable/media_ttt_chip_background_receiver.xml
index 708bc1a..8aae276 100644
--- a/packages/SystemUI/res/drawable/media_ttt_chip_background_receiver.xml
+++ b/packages/SystemUI/res/drawable/media_ttt_chip_background_receiver.xml
@@ -19,8 +19,8 @@
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:shape="oval">
<size
- android:height="@dimen/media_ttt_chip_size_receiver"
- android:width="@dimen/media_ttt_chip_size_receiver"
+ android:height="@dimen/media_ttt_icon_size_receiver"
+ android:width="@dimen/media_ttt_icon_size_receiver"
/>
- <solid android:color="?androidprv:attr/colorSurface" />
+ <solid android:color="?androidprv:attr/colorAccentPrimary" />
</shape>
diff --git a/packages/SystemUI/res/layout/screenshot_static.xml b/packages/SystemUI/res/layout/screenshot_static.xml
index efd683f..e4e0bd4 100644
--- a/packages/SystemUI/res/layout/screenshot_static.xml
+++ b/packages/SystemUI/res/layout/screenshot_static.xml
@@ -190,7 +190,6 @@
app:layout_constraintBottom_toBottomOf="parent"
android:contentDescription="@string/screenshot_dismiss_work_profile">
<ImageView
- android:id="@+id/screenshot_dismiss_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="@dimen/overlay_dismiss_button_margin"
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 6a9149e..3b17bce 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1077,11 +1077,12 @@
<dimen name="media_ttt_last_item_start_margin">12dp</dimen>
<!-- Media tap-to-transfer chip for receiver device -->
- <dimen name="media_ttt_chip_size_receiver">100dp</dimen>
- <dimen name="media_ttt_icon_size_receiver">95dp</dimen>
+ <dimen name="media_ttt_icon_size_receiver">112dp</dimen>
<!-- Add some padding for the generic icon so it doesn't go all the way to the border. -->
- <dimen name="media_ttt_generic_icon_padding">12dp</dimen>
- <dimen name="media_ttt_receiver_vert_translation">20dp</dimen>
+ <!-- The generic icon should be 40dp, and the full icon is 112dp, so the padding should be
+ (112 - 40) / 2 = 36dp -->
+ <dimen name="media_ttt_generic_icon_padding">36dp</dimen>
+ <dimen name="media_ttt_receiver_vert_translation">40dp</dimen>
<!-- Window magnification -->
<dimen name="magnification_border_drag_size">35dp</dimen>
@@ -1286,6 +1287,12 @@
translate into their final position. -->
<dimen name="lockscreen_shade_keyguard_transition_distance">@dimen/lockscreen_shade_media_transition_distance</dimen>
+ <!-- DREAMING -> LOCKSCREEN transition: Amount to shift lockscreen content on entering -->
+ <dimen name="dreaming_to_lockscreen_transition_lockscreen_translation_y">40dp</dimen>
+
+ <!-- OCCLUDED -> LOCKSCREEN transition: Amount to shift lockscreen content on entering -->
+ <dimen name="occluded_to_lockscreen_transition_lockscreen_translation_y">40dp</dimen>
+
<!-- The amount of vertical offset for the keyguard during the full shade transition. -->
<dimen name="lockscreen_shade_keyguard_transition_vertical_offset">0dp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index ce3084c..2eb58b9 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2870,7 +2870,7 @@
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>
+ <string name="keyguard_affordance_press_too_short">Touch & hold to open</string>
<!-- Text for education page of cancel button to hide the page. [CHAR_LIMIT=NONE] -->
<string name="rear_display_bottom_sheet_cancel">Cancel</string>
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 b057fe4..cc3d7a8 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
@@ -57,7 +57,7 @@
private static final boolean DEBUG = false;
- private static final String TAG = "PluginInstanceManager";
+ private static final String TAG = "PluginActionManager";
public static final String PLUGIN_PERMISSION = "com.android.systemui.permission.PLUGIN";
private final Context mContext;
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java
index 82d70116..e08a604 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java
@@ -43,6 +43,10 @@
InteractionJankMonitor.CUJ_LAUNCHER_QUICK_SWITCH;
public static final int CUJ_OPEN_ALL_APPS =
InteractionJankMonitor.CUJ_LAUNCHER_OPEN_ALL_APPS;
+ public static final int CUJ_CLOSE_ALL_APPS_SWIPE =
+ InteractionJankMonitor.CUJ_LAUNCHER_CLOSE_ALL_APPS_SWIPE;
+ public static final int CUJ_CLOSE_ALL_APPS_TO_HOME =
+ InteractionJankMonitor.CUJ_LAUNCHER_CLOSE_ALL_APPS_TO_HOME;
public static final int CUJ_ALL_APPS_SCROLL =
InteractionJankMonitor.CUJ_LAUNCHER_ALL_APPS_SCROLL;
public static final int CUJ_APP_LAUNCH_FROM_WIDGET =
@@ -65,7 +69,10 @@
CUJ_APP_LAUNCH_FROM_WIDGET,
CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION,
CUJ_RECENTS_SCROLLING,
- CUJ_APP_SWIPE_TO_RECENTS
+ CUJ_APP_SWIPE_TO_RECENTS,
+ CUJ_OPEN_ALL_APPS,
+ CUJ_CLOSE_ALL_APPS_SWIPE,
+ CUJ_CLOSE_ALL_APPS_TO_HOME
})
@Retention(RetentionPolicy.SOURCE)
public @interface CujType {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java
index 8b9823b..b8e196f 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java
@@ -16,6 +16,8 @@
package com.android.keyguard;
+import static java.util.Collections.emptySet;
+
import android.content.Context;
import android.os.Trace;
import android.util.AttributeSet;
@@ -88,8 +90,9 @@
}
/** Sets a translationY value on every child view except for the media view. */
- public void setChildrenTranslationYExcludingMediaView(float translationY) {
- setChildrenTranslationYExcluding(translationY, Set.of(mMediaHostContainer));
+ public void setChildrenTranslationY(float translationY, boolean excludeMedia) {
+ setChildrenTranslationYExcluding(translationY,
+ excludeMedia ? Set.of(mMediaHostContainer) : emptySet());
}
/** Sets a translationY value on every view except for the views in the provided set. */
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
index 7849747..aec3063 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
@@ -20,6 +20,8 @@
import android.util.Slog;
import com.android.keyguard.KeyguardClockSwitch.ClockSize;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.plugins.ClockAnimations;
import com.android.systemui.statusbar.notification.AnimatableProperty;
import com.android.systemui.statusbar.notification.PropertyAnimator;
@@ -59,6 +61,7 @@
KeyguardUpdateMonitor keyguardUpdateMonitor,
ConfigurationController configurationController,
DozeParameters dozeParameters,
+ FeatureFlags featureFlags,
ScreenOffAnimationController screenOffAnimationController) {
super(keyguardStatusView);
mKeyguardSliceViewController = keyguardSliceViewController;
@@ -67,6 +70,8 @@
mConfigurationController = configurationController;
mKeyguardVisibilityHelper = new KeyguardVisibilityHelper(mView, keyguardStateController,
dozeParameters, screenOffAnimationController, /* animateYPos= */ true);
+ mKeyguardVisibilityHelper.setOcclusionTransitionFlagEnabled(
+ featureFlags.isEnabled(Flags.UNOCCLUSION_TRANSITION));
}
@Override
@@ -115,8 +120,8 @@
/**
* Sets a translationY on the views on the keyguard, except on the media view.
*/
- public void setTranslationYExcludingMedia(float translationY) {
- mView.setChildrenTranslationYExcludingMediaView(translationY);
+ public void setTranslationY(float translationY, boolean excludeMedia) {
+ mView.setChildrenTranslationY(translationY, excludeMedia);
}
/**
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt
index f974e27..edd150c 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt
@@ -19,6 +19,8 @@
import android.content.Context
import android.view.ViewGroup
import com.android.systemui.R
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.StatusBarState.KEYGUARD
import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator
import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.Direction.END
import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.Direction.START
@@ -36,27 +38,29 @@
@Inject
constructor(
private val context: Context,
- unfoldProgressProvider: NaturalRotationUnfoldProgressProvider
+ statusBarStateController: StatusBarStateController,
+ unfoldProgressProvider: NaturalRotationUnfoldProgressProvider,
) {
/** Certain views only need to move if they are not currently centered */
var statusViewCentered = false
- private val filterSplitShadeOnly = { !statusViewCentered }
- private val filterNever = { true }
+ private val filterKeyguardAndSplitShadeOnly: () -> Boolean = {
+ statusBarStateController.getState() == KEYGUARD && !statusViewCentered }
+ private val filterKeyguard: () -> Boolean = { statusBarStateController.getState() == KEYGUARD }
private val translateAnimator by lazy {
UnfoldConstantTranslateAnimator(
viewsIdToTranslate =
setOf(
- ViewIdToTranslate(R.id.keyguard_status_area, START, filterNever),
+ ViewIdToTranslate(R.id.keyguard_status_area, START, filterKeyguard),
ViewIdToTranslate(
- R.id.lockscreen_clock_view_large, START, filterSplitShadeOnly),
- ViewIdToTranslate(R.id.lockscreen_clock_view, START, filterNever),
+ R.id.lockscreen_clock_view_large, START, filterKeyguardAndSplitShadeOnly),
+ ViewIdToTranslate(R.id.lockscreen_clock_view, START, filterKeyguard),
ViewIdToTranslate(
- R.id.notification_stack_scroller, END, filterSplitShadeOnly),
- ViewIdToTranslate(R.id.start_button, START, filterNever),
- ViewIdToTranslate(R.id.end_button, END, filterNever)),
+ R.id.notification_stack_scroller, END, filterKeyguardAndSplitShadeOnly),
+ ViewIdToTranslate(R.id.start_button, START, filterKeyguard),
+ ViewIdToTranslate(R.id.end_button, END, filterKeyguard)),
progressProvider = unfoldProgressProvider)
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index e3c58ce..271fc7b 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -875,7 +875,7 @@
Assert.isMainThread();
if (mWakeOnFingerprintAcquiredStart && acquireInfo == FINGERPRINT_ACQUIRED_START) {
mPowerManager.wakeUp(
- SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_GESTURE,
+ SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_BIOMETRIC,
"com.android.systemui.keyguard:FINGERPRINT_ACQUIRED_START");
}
for (int i = 0; i < mCallbacks.size(); i++) {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java b/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
index 498304b..bde0692 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
@@ -44,6 +44,7 @@
private boolean mAnimateYPos;
private boolean mKeyguardViewVisibilityAnimating;
private boolean mLastOccludedState = false;
+ private boolean mIsUnoccludeTransitionFlagEnabled = false;
private final AnimationProperties mAnimationProperties = new AnimationProperties();
public KeyguardVisibilityHelper(View view,
@@ -62,6 +63,10 @@
return mKeyguardViewVisibilityAnimating;
}
+ public void setOcclusionTransitionFlagEnabled(boolean enabled) {
+ mIsUnoccludeTransitionFlagEnabled = enabled;
+ }
+
/**
* Set the visibility of a keyguard view based on some new state.
*/
@@ -129,7 +134,7 @@
// since it may need to be cancelled due to keyguard lifecycle events.
mScreenOffAnimationController.animateInKeyguard(
mView, mAnimateKeyguardStatusViewVisibleEndRunnable);
- } else if (mLastOccludedState && !isOccluded) {
+ } else if (!mIsUnoccludeTransitionFlagEnabled && mLastOccludedState && !isOccluded) {
// An activity was displayed over the lock screen, and has now gone away
mView.setVisibility(View.VISIBLE);
mView.setAlpha(0f);
@@ -142,7 +147,9 @@
.start();
} else {
mView.setVisibility(View.VISIBLE);
- mView.setAlpha(1f);
+ if (!mIsUnoccludeTransitionFlagEnabled) {
+ mView.setAlpha(1f);
+ }
}
} else {
mView.setVisibility(View.GONE);
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index 815ac68..e42f051 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
@@ -120,6 +120,7 @@
private final Interpolator mLinearOutSlowIn;
private final LockPatternUtils mLockPatternUtils;
private final WakefulnessLifecycle mWakefulnessLifecycle;
+ private final AuthDialogPanelInteractionDetector mPanelInteractionDetector;
private final InteractionJankMonitor mInteractionJankMonitor;
// TODO: these should be migrated out once ready
@@ -141,7 +142,6 @@
private final OnBackInvokedCallback mBackCallback = this::onBackInvoked;
private final @Background DelayableExecutor mBackgroundExecutor;
- private boolean mIsOrientationChanged = false;
// Non-null only if the dialog is in the act of dismissing and has not sent the reason yet.
@Nullable @AuthDialogCallback.DismissedReason private Integer mPendingCallbackReason;
@@ -235,6 +235,7 @@
@Nullable List<FingerprintSensorPropertiesInternal> fpProps,
@Nullable List<FaceSensorPropertiesInternal> faceProps,
@NonNull WakefulnessLifecycle wakefulnessLifecycle,
+ @NonNull AuthDialogPanelInteractionDetector panelInteractionDetector,
@NonNull UserManager userManager,
@NonNull LockPatternUtils lockPatternUtils,
@NonNull InteractionJankMonitor jankMonitor,
@@ -242,8 +243,9 @@
@NonNull Provider<CredentialViewModel> credentialViewModelProvider) {
mConfig.mSensorIds = sensorIds;
return new AuthContainerView(mConfig, fpProps, faceProps, wakefulnessLifecycle,
- userManager, lockPatternUtils, jankMonitor, biometricPromptInteractor,
- credentialViewModelProvider, new Handler(Looper.getMainLooper()), bgExecutor);
+ panelInteractionDetector, userManager, lockPatternUtils, jankMonitor,
+ biometricPromptInteractor, credentialViewModelProvider,
+ new Handler(Looper.getMainLooper()), bgExecutor);
}
}
@@ -331,6 +333,7 @@
@Nullable List<FingerprintSensorPropertiesInternal> fpProps,
@Nullable List<FaceSensorPropertiesInternal> faceProps,
@NonNull WakefulnessLifecycle wakefulnessLifecycle,
+ @NonNull AuthDialogPanelInteractionDetector panelInteractionDetector,
@NonNull UserManager userManager,
@NonNull LockPatternUtils lockPatternUtils,
@NonNull InteractionJankMonitor jankMonitor,
@@ -346,6 +349,7 @@
mHandler = mainHandler;
mWindowManager = mContext.getSystemService(WindowManager.class);
mWakefulnessLifecycle = wakefulnessLifecycle;
+ mPanelInteractionDetector = panelInteractionDetector;
mTranslationY = getResources()
.getDimension(R.dimen.biometric_dialog_animation_translation_offset);
@@ -490,22 +494,6 @@
@Override
public void onOrientationChanged() {
maybeUpdatePositionForUdfps(true /* invalidate */);
- mIsOrientationChanged = true;
- }
-
- @Override
- public void onWindowFocusChanged(boolean hasWindowFocus) {
- super.onWindowFocusChanged(hasWindowFocus);
- if (!hasWindowFocus) {
- //it's a workaround to avoid closing BP incorrectly
- //BP gets a onWindowFocusChanged(false) and then gets a onWindowFocusChanged(true)
- if (mIsOrientationChanged) {
- mIsOrientationChanged = false;
- return;
- }
- Log.v(TAG, "Lost window focus, dismissing the dialog");
- animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED);
- }
}
@Override
@@ -513,6 +501,8 @@
super.onAttachedToWindow();
mWakefulnessLifecycle.addObserver(this);
+ mPanelInteractionDetector.enable(
+ () -> animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED));
if (Utils.isBiometricAllowed(mConfig.mPromptInfo)) {
mBiometricScrollView.addView(mBiometricView);
@@ -666,11 +656,6 @@
mBiometricView.restoreState(savedState);
}
- if (savedState != null) {
- mIsOrientationChanged = savedState.getBoolean(
- AuthDialog.KEY_BIOMETRIC_ORIENTATION_CHANGED);
- }
-
wm.addView(this, getLayoutParams(mWindowToken, mConfig.mPromptInfo.getTitle()));
}
@@ -689,6 +674,7 @@
@Override
public void dismissWithoutCallback(boolean animate) {
+ mPanelInteractionDetector.disable();
if (animate) {
animateAway(false /* sendReason */, 0 /* reason */);
} else {
@@ -699,6 +685,7 @@
@Override
public void dismissFromSystemServer() {
+ mPanelInteractionDetector.disable();
animateAway(false /* sendReason */, 0 /* reason */);
}
@@ -761,8 +748,6 @@
mBiometricView != null && mCredentialView == null);
outState.putBoolean(AuthDialog.KEY_CREDENTIAL_SHOWING, mCredentialView != null);
- outState.putBoolean(AuthDialog.KEY_BIOMETRIC_ORIENTATION_CHANGED, mIsOrientationChanged);
-
if (mBiometricView != null) {
mBiometricView.onSaveState(outState);
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index a0f3ecb0..dad6ebe 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -164,6 +164,7 @@
@NonNull private final SparseBooleanArray mSfpsEnrolledForUser;
@NonNull private final SensorPrivacyManager mSensorPrivacyManager;
private final WakefulnessLifecycle mWakefulnessLifecycle;
+ private final AuthDialogPanelInteractionDetector mPanelInteractionDetector;
private boolean mAllFingerprintAuthenticatorsRegistered;
@NonNull private final UserManager mUserManager;
@NonNull private final LockPatternUtils mLockPatternUtils;
@@ -721,6 +722,7 @@
Provider<SideFpsController> sidefpsControllerFactory,
@NonNull DisplayManager displayManager,
@NonNull WakefulnessLifecycle wakefulnessLifecycle,
+ @NonNull AuthDialogPanelInteractionDetector panelInteractionDetector,
@NonNull UserManager userManager,
@NonNull LockPatternUtils lockPatternUtils,
@NonNull UdfpsLogger udfpsLogger,
@@ -767,6 +769,8 @@
});
mWakefulnessLifecycle = wakefulnessLifecycle;
+ mPanelInteractionDetector = panelInteractionDetector;
+
mFaceProps = mFaceManager != null ? mFaceManager.getSensorPropertiesInternal() : null;
int[] faceAuthLocation = context.getResources().getIntArray(
@@ -1149,6 +1153,7 @@
requestId,
multiSensorConfig,
mWakefulnessLifecycle,
+ mPanelInteractionDetector,
mUserManager,
mLockPatternUtils);
@@ -1239,6 +1244,7 @@
String opPackageName, boolean skipIntro, long operationId, long requestId,
@BiometricMultiSensorMode int multiSensorConfig,
@NonNull WakefulnessLifecycle wakefulnessLifecycle,
+ @NonNull AuthDialogPanelInteractionDetector panelInteractionDetector,
@NonNull UserManager userManager,
@NonNull LockPatternUtils lockPatternUtils) {
return new AuthContainerView.Builder(mContext)
@@ -1253,8 +1259,9 @@
.setMultiSensorConfig(multiSensorConfig)
.setScaleFactorProvider(() -> getScaleFactor())
.build(bgExecutor, sensorIds, mFpProps, mFaceProps, wakefulnessLifecycle,
- userManager, lockPatternUtils, mInteractionJankMonitor,
- mBiometricPromptInteractor, mCredentialViewModelProvider);
+ panelInteractionDetector, userManager, lockPatternUtils,
+ mInteractionJankMonitor, mBiometricPromptInteractor,
+ mCredentialViewModelProvider);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java
index cd0fc37..51f39b3 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java
@@ -48,8 +48,6 @@
String KEY_BIOMETRIC_SENSOR_TYPE = "sensor_type";
String KEY_BIOMETRIC_SENSOR_PROPS = "sensor_props";
- String KEY_BIOMETRIC_ORIENTATION_CHANGED = "orientation_changed";
-
int SIZE_UNKNOWN = 0;
/**
* Minimal UI, showing only biometric icon.
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetector.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetector.kt
new file mode 100644
index 0000000..64211b5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetector.kt
@@ -0,0 +1,53 @@
+package com.android.systemui.biometrics
+
+import android.annotation.AnyThread
+import android.annotation.MainThread
+import android.util.Log
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.shade.ShadeExpansionChangeEvent
+import com.android.systemui.shade.ShadeExpansionStateManager
+import java.util.concurrent.Executor
+import javax.inject.Inject
+
+class AuthDialogPanelInteractionDetector
+@Inject
+constructor(
+ private val shadeExpansionStateManager: ShadeExpansionStateManager,
+ @Main private val mainExecutor: Executor,
+) {
+ private var action: Action? = null
+
+ @MainThread
+ fun enable(onPanelInteraction: Runnable) {
+ if (action == null) {
+ action = Action(onPanelInteraction)
+ shadeExpansionStateManager.addExpansionListener(this::onPanelExpansionChanged)
+ } else {
+ Log.e(TAG, "Already enabled")
+ }
+ }
+
+ @MainThread
+ fun disable() {
+ if (action != null) {
+ action = null
+ shadeExpansionStateManager.removeExpansionListener(this::onPanelExpansionChanged)
+ }
+ }
+
+ @AnyThread
+ private fun onPanelExpansionChanged(event: ShadeExpansionChangeEvent) =
+ mainExecutor.execute {
+ action?.let {
+ if (event.tracking) {
+ Log.v(TAG, "Detected panel interaction, event: $event")
+ it.onPanelInteraction.run()
+ disable()
+ }
+ }
+ }
+}
+
+private data class Action(val onPanelInteraction: Runnable)
+
+private const val TAG = "AuthDialogPanelInteractionDetector"
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java
index 82e5704..805a20a 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java
@@ -16,15 +16,21 @@
package com.android.systemui.clipboardoverlay;
+import static android.content.ClipDescription.CLASSIFICATION_COMPLETE;
+
import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CLIPBOARD_OVERLAY_ENABLED;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ENTERED;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_UPDATED;
+import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_TOAST_SHOWN;
+
+import static com.google.android.setupcompat.util.WizardManagerHelper.SETTINGS_SECURE_USER_SETUP_COMPLETE;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.os.SystemProperties;
import android.provider.DeviceConfig;
+import android.provider.Settings;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
@@ -56,6 +62,7 @@
private final DeviceConfigProxy mDeviceConfig;
private final Provider<ClipboardOverlayController> mOverlayProvider;
private final ClipboardOverlayControllerLegacyFactory mOverlayFactory;
+ private final ClipboardToast mClipboardToast;
private final ClipboardManager mClipboardManager;
private final UiEventLogger mUiEventLogger;
private final FeatureFlags mFeatureFlags;
@@ -66,6 +73,7 @@
public ClipboardListener(Context context, DeviceConfigProxy deviceConfigProxy,
Provider<ClipboardOverlayController> clipboardOverlayControllerProvider,
ClipboardOverlayControllerLegacyFactory overlayFactory,
+ ClipboardToast clipboardToast,
ClipboardManager clipboardManager,
UiEventLogger uiEventLogger,
FeatureFlags featureFlags) {
@@ -73,6 +81,7 @@
mDeviceConfig = deviceConfigProxy;
mOverlayProvider = clipboardOverlayControllerProvider;
mOverlayFactory = overlayFactory;
+ mClipboardToast = clipboardToast;
mClipboardManager = clipboardManager;
mUiEventLogger = uiEventLogger;
mFeatureFlags = featureFlags;
@@ -102,6 +111,15 @@
return;
}
+ if (!isUserSetupComplete()) {
+ // just show a toast, user should not access intents from this state
+ if (shouldShowToast(clipData)) {
+ mUiEventLogger.log(CLIPBOARD_TOAST_SHOWN, 0, clipSource);
+ mClipboardToast.showCopiedToast();
+ }
+ return;
+ }
+
boolean enabled = mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR);
if (mClipboardOverlay == null || enabled != mUsingNewOverlay) {
mUsingNewOverlay = enabled;
@@ -136,10 +154,26 @@
return clipData.getDescription().getExtras().getBoolean(EXTRA_SUPPRESS_OVERLAY, false);
}
+ boolean shouldShowToast(ClipData clipData) {
+ if (clipData == null) {
+ return false;
+ } else if (clipData.getDescription().getClassificationStatus() == CLASSIFICATION_COMPLETE) {
+ // only show for classification complete if we aren't already showing a toast, to ignore
+ // the duplicate ClipData with classification
+ return !mClipboardToast.isShowing();
+ }
+ return true;
+ }
+
private static boolean isEmulator() {
return SystemProperties.getBoolean("ro.boot.qemu", false);
}
+ private boolean isUserSetupComplete() {
+ return Settings.Secure.getInt(mContext.getContentResolver(),
+ SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1;
+ }
+
interface ClipboardOverlay {
void setClipData(ClipData clipData, String clipSource);
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayEvent.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayEvent.java
index 9917507..4b5f876 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayEvent.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayEvent.java
@@ -43,7 +43,9 @@
@UiEvent(doc = "clipboard overlay tapped outside")
CLIPBOARD_OVERLAY_TAP_OUTSIDE(1077),
@UiEvent(doc = "clipboard overlay dismissed, miscellaneous reason")
- CLIPBOARD_OVERLAY_DISMISSED_OTHER(1078);
+ CLIPBOARD_OVERLAY_DISMISSED_OTHER(1078),
+ @UiEvent(doc = "clipboard toast shown")
+ CLIPBOARD_TOAST_SHOWN(1270);
private final int mId;
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardToast.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardToast.java
new file mode 100644
index 0000000..0ed7d27
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardToast.java
@@ -0,0 +1,56 @@
+/*
+ * 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.clipboardoverlay;
+
+import android.content.Context;
+import android.widget.Toast;
+
+import com.android.systemui.R;
+
+import javax.inject.Inject;
+
+/**
+ * Utility class for showing a simple clipboard toast on copy.
+ */
+class ClipboardToast extends Toast.Callback {
+ private final Context mContext;
+ private Toast mCopiedToast;
+
+ @Inject
+ ClipboardToast(Context context) {
+ mContext = context;
+ }
+
+ void showCopiedToast() {
+ if (mCopiedToast != null) {
+ mCopiedToast.cancel();
+ }
+ mCopiedToast = Toast.makeText(mContext,
+ R.string.clipboard_overlay_text_copied, Toast.LENGTH_SHORT);
+ mCopiedToast.show();
+ }
+
+ boolean isShowing() {
+ return mCopiedToast != null;
+ }
+
+ @Override // Toast.Callback
+ public void onToastHidden() {
+ super.onToastHidden();
+ mCopiedToast = null;
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
new file mode 100644
index 0000000..e5ec727
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.compose
+
+import androidx.activity.ComponentActivity
+import com.android.systemui.people.ui.viewmodel.PeopleViewModel
+
+/**
+ * A facade to interact with Compose, when it is available.
+ *
+ * You should access this facade by calling the static methods on
+ * [com.android.systemui.compose.ComposeFacade] directly.
+ */
+interface BaseComposeFacade {
+ /**
+ * Whether Compose is currently available. This function should be checked before calling any
+ * other functions on this facade.
+ *
+ * This value will never change at runtime.
+ */
+ fun isComposeAvailable(): Boolean
+
+ /** Bind the content of [activity] to [viewModel]. */
+ fun setPeopleSpaceActivityContent(
+ activity: ComponentActivity,
+ viewModel: PeopleViewModel,
+ onResult: (PeopleViewModel.Result) -> Unit,
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index 96707f4..59f68f7 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -44,6 +44,7 @@
import com.android.systemui.statusbar.notification.fsi.FsiChromeRepo
import com.android.systemui.statusbar.notification.InstantAppNotifier
import com.android.systemui.statusbar.notification.fsi.FsiChromeViewModelFactory
+import com.android.systemui.statusbar.notification.fsi.FsiChromeViewBinder
import com.android.systemui.statusbar.phone.KeyguardLiftController
import com.android.systemui.stylus.StylusUsiPowerStartable
import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
@@ -94,6 +95,12 @@
@ClassKey(FsiChromeViewModelFactory::class)
abstract fun bindFSIChromeWindowViewModel(sysui: FsiChromeViewModelFactory): CoreStartable
+ /** Inject into FsiChromeWindowBinder. */
+ @Binds
+ @IntoMap
+ @ClassKey(FsiChromeViewBinder::class)
+ abstract fun bindFsiChromeWindowBinder(sysui: FsiChromeViewBinder): CoreStartable
+
/** Inject into GarbageMonitor.Service. */
@Binds
@IntoMap
diff --git a/packages/SystemUI/src/com/android/systemui/demomode/DemoModeAvailabilityTracker.kt b/packages/SystemUI/src/com/android/systemui/demomode/DemoModeAvailabilityTracker.kt
index 16f4150..c746efd 100644
--- a/packages/SystemUI/src/com/android/systemui/demomode/DemoModeAvailabilityTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/demomode/DemoModeAvailabilityTracker.kt
@@ -20,7 +20,7 @@
import android.database.ContentObserver
import android.os.Handler
import android.os.Looper
-import android.provider.Settings
+import com.android.systemui.util.settings.GlobalSettings
/**
* Class to track the availability of [DemoMode]. Use this class to track the availability and
@@ -29,7 +29,10 @@
* This class works by wrapping a content observer for the relevant keys related to DemoMode
* availability and current on/off state, and triggering callbacks.
*/
-abstract class DemoModeAvailabilityTracker(val context: Context) {
+abstract class DemoModeAvailabilityTracker(
+ val context: Context,
+ val globalSettings: GlobalSettings,
+) {
var isInDemoMode = false
var isDemoModeAvailable = false
@@ -41,9 +44,9 @@
fun startTracking() {
val resolver = context.contentResolver
resolver.registerContentObserver(
- Settings.Global.getUriFor(DEMO_MODE_ALLOWED), false, allowedObserver)
+ globalSettings.getUriFor(DEMO_MODE_ALLOWED), false, allowedObserver)
resolver.registerContentObserver(
- Settings.Global.getUriFor(DEMO_MODE_ON), false, onObserver)
+ globalSettings.getUriFor(DEMO_MODE_ON), false, onObserver)
}
fun stopTracking() {
@@ -57,12 +60,11 @@
abstract fun onDemoModeFinished()
private fun checkIsDemoModeAllowed(): Boolean {
- return Settings.Global
- .getInt(context.contentResolver, DEMO_MODE_ALLOWED, 0) != 0
+ return globalSettings.getInt(DEMO_MODE_ALLOWED, 0) != 0
}
private fun checkIsDemoModeOn(): Boolean {
- return Settings.Global.getInt(context.contentResolver, DEMO_MODE_ON, 0) != 0
+ return globalSettings.getInt(DEMO_MODE_ON, 0) != 0
}
private val allowedObserver = object : ContentObserver(Handler(Looper.getMainLooper())) {
diff --git a/packages/SystemUI/src/com/android/systemui/demomode/DemoModeController.kt b/packages/SystemUI/src/com/android/systemui/demomode/DemoModeController.kt
index 000bbe6..84f83f1 100644
--- a/packages/SystemUI/src/com/android/systemui/demomode/DemoModeController.kt
+++ b/packages/SystemUI/src/com/android/systemui/demomode/DemoModeController.kt
@@ -24,22 +24,28 @@
import android.os.UserHandle
import android.util.Log
import com.android.systemui.Dumpable
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.demomode.DemoMode.ACTION_DEMO
import com.android.systemui.dump.DumpManager
import com.android.systemui.statusbar.policy.CallbackController
import com.android.systemui.util.Assert
import com.android.systemui.util.settings.GlobalSettings
import java.io.PrintWriter
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
/**
* Handles system broadcasts for [DemoMode]
*
* Injected via [DemoModeModule]
*/
-class DemoModeController constructor(
+class DemoModeController
+constructor(
private val context: Context,
private val dumpManager: DumpManager,
- private val globalSettings: GlobalSettings
+ private val globalSettings: GlobalSettings,
+ private val broadcastDispatcher: BroadcastDispatcher,
) : CallbackController<DemoMode>, Dumpable {
// Var updated when the availability tracker changes, or when we enter/exit demo mode in-process
@@ -58,9 +64,7 @@
requestFinishDemoMode()
val m = mutableMapOf<String, MutableList<DemoMode>>()
- DemoMode.COMMANDS.map { command ->
- m.put(command, mutableListOf())
- }
+ DemoMode.COMMANDS.map { command -> m.put(command, mutableListOf()) }
receiverMap = m
}
@@ -71,7 +75,7 @@
initialized = true
- dumpManager.registerDumpable(TAG, this)
+ dumpManager.registerNormalDumpable(TAG, this)
// Due to DemoModeFragment running in systemui:tuner process, we have to observe for
// content changes to know if the setting turned on or off
@@ -81,8 +85,13 @@
val demoFilter = IntentFilter()
demoFilter.addAction(ACTION_DEMO)
- context.registerReceiverAsUser(broadcastReceiver, UserHandle.ALL, demoFilter,
- android.Manifest.permission.DUMP, null, Context.RECEIVER_EXPORTED)
+
+ broadcastDispatcher.registerReceiver(
+ receiver = broadcastReceiver,
+ filter = demoFilter,
+ user = UserHandle.ALL,
+ permission = android.Manifest.permission.DUMP,
+ )
}
override fun addCallback(listener: DemoMode) {
@@ -91,16 +100,15 @@
commands.forEach { command ->
if (!receiverMap.containsKey(command)) {
- throw IllegalStateException("Command ($command) not recognized. " +
- "See DemoMode.java for valid commands")
+ throw IllegalStateException(
+ "Command ($command) not recognized. " + "See DemoMode.java for valid commands"
+ )
}
receiverMap[command]!!.add(listener)
}
- synchronized(this) {
- receivers.add(listener)
- }
+ synchronized(this) { receivers.add(listener) }
if (isInDemoMode) {
listener.onDemoModeStarted()
@@ -109,14 +117,46 @@
override fun removeCallback(listener: DemoMode) {
synchronized(this) {
- listener.demoCommands().forEach { command ->
- receiverMap[command]!!.remove(listener)
- }
+ listener.demoCommands().forEach { command -> receiverMap[command]!!.remove(listener) }
receivers.remove(listener)
}
}
+ /**
+ * Create a [Flow] for the stream of demo mode arguments that come in for the given [command]
+ *
+ * This is equivalent of creating a listener manually and adding an event handler for the given
+ * command, like so:
+ *
+ * ```
+ * class Demoable {
+ * private val demoHandler = object : DemoMode {
+ * override fun demoCommands() = listOf(<command>)
+ *
+ * override fun dispatchDemoCommand(command: String, args: Bundle) {
+ * handleDemoCommand(args)
+ * }
+ * }
+ * }
+ * ```
+ *
+ * @param command The top-level demo mode command you want a stream for
+ */
+ fun demoFlowForCommand(command: String): Flow<Bundle> = conflatedCallbackFlow {
+ val callback =
+ object : DemoMode {
+ override fun demoCommands(): List<String> = listOf(command)
+
+ override fun dispatchDemoCommand(command: String, args: Bundle) {
+ trySend(args)
+ }
+ }
+
+ addCallback(callback)
+ awaitClose { removeCallback(callback) }
+ }
+
private fun setIsDemoModeAllowed(enabled: Boolean) {
// Turn off demo mode if it was on
if (isInDemoMode && !enabled) {
@@ -129,13 +169,9 @@
Assert.isMainThread()
val copy: List<DemoModeCommandReceiver>
- synchronized(this) {
- copy = receivers.toList()
- }
+ synchronized(this) { copy = receivers.toList() }
- copy.forEach { r ->
- r.onDemoModeStarted()
- }
+ copy.forEach { r -> r.onDemoModeStarted() }
}
private fun exitDemoMode() {
@@ -143,18 +179,13 @@
Assert.isMainThread()
val copy: List<DemoModeCommandReceiver>
- synchronized(this) {
- copy = receivers.toList()
- }
+ synchronized(this) { copy = receivers.toList() }
- copy.forEach { r ->
- r.onDemoModeFinished()
- }
+ copy.forEach { r -> r.onDemoModeFinished() }
}
fun dispatchDemoCommand(command: String, args: Bundle) {
Assert.isMainThread()
-
if (DEBUG) {
Log.d(TAG, "dispatchDemoCommand: $command, args=$args")
}
@@ -172,9 +203,7 @@
}
// See? demo mode is easy now, you just notify the listeners when their command is called
- receiverMap[command]!!.forEach { receiver ->
- receiver.dispatchDemoCommand(command, args)
- }
+ receiverMap[command]!!.forEach { receiver -> receiver.dispatchDemoCommand(command, args) }
}
override fun dump(pw: PrintWriter, args: Array<out String>) {
@@ -183,65 +212,64 @@
pw.println(" isDemoModeAllowed=$isAvailable")
pw.print(" receivers=[")
val copy: List<DemoModeCommandReceiver>
- synchronized(this) {
- copy = receivers.toList()
- }
- copy.forEach { recv ->
- pw.print(" ${recv.javaClass.simpleName}")
- }
+ synchronized(this) { copy = receivers.toList() }
+ copy.forEach { recv -> pw.print(" ${recv.javaClass.simpleName}") }
pw.println(" ]")
pw.println(" receiverMap= [")
receiverMap.keys.forEach { command ->
pw.print(" $command : [")
- val recvs = receiverMap[command]!!.map { receiver ->
- receiver.javaClass.simpleName
- }.joinToString(",")
+ val recvs =
+ receiverMap[command]!!
+ .map { receiver -> receiver.javaClass.simpleName }
+ .joinToString(",")
pw.println("$recvs ]")
}
}
- private val tracker = object : DemoModeAvailabilityTracker(context) {
- override fun onDemoModeAvailabilityChanged() {
- setIsDemoModeAllowed(isDemoModeAvailable)
- }
+ private val tracker =
+ object : DemoModeAvailabilityTracker(context, globalSettings) {
+ override fun onDemoModeAvailabilityChanged() {
+ setIsDemoModeAllowed(isDemoModeAvailable)
+ }
- override fun onDemoModeStarted() {
- if (this@DemoModeController.isInDemoMode != isInDemoMode) {
- enterDemoMode()
+ override fun onDemoModeStarted() {
+ if (this@DemoModeController.isInDemoMode != isInDemoMode) {
+ enterDemoMode()
+ }
+ }
+
+ override fun onDemoModeFinished() {
+ if (this@DemoModeController.isInDemoMode != isInDemoMode) {
+ exitDemoMode()
+ }
}
}
- override fun onDemoModeFinished() {
- if (this@DemoModeController.isInDemoMode != isInDemoMode) {
- exitDemoMode()
+ private val broadcastReceiver =
+ object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ if (DEBUG) {
+ Log.v(TAG, "onReceive: $intent")
+ }
+
+ val action = intent.action
+ if (!ACTION_DEMO.equals(action)) {
+ return
+ }
+
+ val bundle = intent.extras ?: return
+ val command = bundle.getString("command", "").trim().lowercase()
+ if (command.isEmpty()) {
+ return
+ }
+
+ try {
+ dispatchDemoCommand(command, bundle)
+ } catch (t: Throwable) {
+ Log.w(TAG, "Error running demo command, intent=$intent $t")
+ }
}
}
- }
-
- private val broadcastReceiver = object : BroadcastReceiver() {
- override fun onReceive(context: Context, intent: Intent) {
- if (DEBUG) {
- Log.v(TAG, "onReceive: $intent")
- }
-
- val action = intent.action
- if (!ACTION_DEMO.equals(action)) {
- return
- }
-
- val bundle = intent.extras ?: return
- val command = bundle.getString("command", "").trim().toLowerCase()
- if (command.length == 0) {
- return
- }
-
- try {
- dispatchDemoCommand(command, bundle)
- } catch (t: Throwable) {
- Log.w(TAG, "Error running demo command, intent=$intent $t")
- }
- }
- }
fun requestSetDemoModeAllowed(allowed: Boolean) {
setGlobal(DEMO_MODE_ALLOWED, if (allowed) 1 else 0)
@@ -258,10 +286,12 @@
private fun setGlobal(key: String, value: Int) {
globalSettings.putInt(key, value)
}
+
+ companion object {
+ const val DEMO_MODE_ALLOWED = "sysui_demo_allowed"
+ const val DEMO_MODE_ON = "sysui_tuner_demo_on"
+ }
}
private const val TAG = "DemoModeController"
-private const val DEMO_MODE_ALLOWED = "sysui_demo_allowed"
-private const val DEMO_MODE_ON = "sysui_tuner_demo_on"
-
private const val DEBUG = false
diff --git a/packages/SystemUI/src/com/android/systemui/demomode/dagger/DemoModeModule.java b/packages/SystemUI/src/com/android/systemui/demomode/dagger/DemoModeModule.java
index de9affb..b84fa5a 100644
--- a/packages/SystemUI/src/com/android/systemui/demomode/dagger/DemoModeModule.java
+++ b/packages/SystemUI/src/com/android/systemui/demomode/dagger/DemoModeModule.java
@@ -18,6 +18,7 @@
import android.content.Context;
+import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.demomode.DemoModeController;
import com.android.systemui.dump.DumpManager;
@@ -37,8 +38,14 @@
static DemoModeController provideDemoModeController(
Context context,
DumpManager dumpManager,
- GlobalSettings globalSettings) {
- DemoModeController dmc = new DemoModeController(context, dumpManager, globalSettings);
+ GlobalSettings globalSettings,
+ BroadcastDispatcher broadcastDispatcher
+ ) {
+ DemoModeController dmc = new DemoModeController(
+ context,
+ dumpManager,
+ globalSettings,
+ broadcastDispatcher);
dmc.initialize();
return /*run*/dmc;
}
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
index 5d21349..5b90ef2 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
@@ -16,7 +16,14 @@
package com.android.systemui.doze;
+import static android.os.PowerManager.WAKE_REASON_BIOMETRIC;
+import static android.os.PowerManager.WAKE_REASON_GESTURE;
+import static android.os.PowerManager.WAKE_REASON_LIFT;
+import static android.os.PowerManager.WAKE_REASON_PLUGGED_IN;
+import static android.os.PowerManager.WAKE_REASON_TAP;
+
import android.annotation.IntDef;
+import android.os.PowerManager;
import android.util.TimeUtils;
import androidx.annotation.NonNull;
@@ -511,6 +518,25 @@
}
}
+ /**
+ * Converts {@link Reason} to {@link PowerManager.WakeReason}.
+ */
+ public static @PowerManager.WakeReason int getPowerManagerWakeReason(@Reason int wakeReason) {
+ switch (wakeReason) {
+ case REASON_SENSOR_DOUBLE_TAP:
+ case REASON_SENSOR_TAP:
+ return WAKE_REASON_TAP;
+ case REASON_SENSOR_PICKUP:
+ return WAKE_REASON_LIFT;
+ case REASON_SENSOR_UDFPS_LONG_PRESS:
+ return WAKE_REASON_BIOMETRIC;
+ case PULSE_REASON_DOCKING:
+ return WAKE_REASON_PLUGGED_IN;
+ default:
+ return WAKE_REASON_GESTURE;
+ }
+ }
+
@Retention(RetentionPolicy.SOURCE)
@IntDef({PULSE_REASON_NONE, PULSE_REASON_INTENT, PULSE_REASON_NOTIFICATION,
PULSE_REASON_SENSOR_SIGMOTION, REASON_SENSOR_PICKUP, REASON_SENSOR_DOUBLE_TAP,
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
index f64d918..c73387b 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
@@ -24,6 +24,7 @@
import static com.android.systemui.plugins.SensorManagerPlugin.Sensor.TYPE_WAKE_LOCK_SCREEN;
import android.annotation.AnyThread;
+import android.content.res.Resources;
import android.database.ContentObserver;
import android.hardware.Sensor;
import android.hardware.SensorManager;
@@ -43,6 +44,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
+import com.android.internal.R;
import com.android.internal.logging.UiEvent;
import com.android.internal.logging.UiEventLogger;
import com.android.internal.logging.UiEventLoggerImpl;
@@ -143,6 +145,7 @@
}
DozeSensors(
+ Resources resources,
AsyncSensorManager sensorManager,
DozeParameters dozeParameters,
AmbientDisplayConfiguration config,
@@ -188,7 +191,8 @@
new TriggerSensor(
mSensorManager.getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE),
Settings.Secure.DOZE_PICK_UP_GESTURE,
- true /* settingDef */,
+ resources.getBoolean(
+ R.bool.config_dozePickupGestureEnabled) /* settingDef */,
config.dozePickupSensorAvailable(),
DozeLog.REASON_SENSOR_PICKUP, false /* touchCoords */,
false /* touchscreen */,
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
index f8bd1e7..ba38ab0 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
@@ -116,8 +116,8 @@
@Override
public void requestWakeUp(@DozeLog.Reason int reason) {
- PowerManager pm = getSystemService(PowerManager.class);
- pm.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_GESTURE,
+ final PowerManager pm = getSystemService(PowerManager.class);
+ pm.wakeUp(SystemClock.uptimeMillis(), DozeLog.getPowerManagerWakeReason(reason),
"com.android.systemui:NODOZE " + DozeLog.reasonToString(reason));
}
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
index 3f9f14c..b95c3f3 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
@@ -210,7 +210,7 @@
mAllowPulseTriggers = true;
mSessionTracker = sessionTracker;
- mDozeSensors = new DozeSensors(mSensorManager, dozeParameters,
+ mDozeSensors = new DozeSensors(mContext.getResources(), mSensorManager, dozeParameters,
config, wakeLock, this::onSensor, this::onProximityFar, dozeLog, proximitySensor,
secureSettings, authController, devicePostureController, userTracker);
mDockManager = dockManager;
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamCallbackController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamCallbackController.kt
deleted file mode 100644
index ab4632b..0000000
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamCallbackController.kt
+++ /dev/null
@@ -1,45 +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.systemui.dreams
-
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.statusbar.policy.CallbackController
-import javax.inject.Inject
-
-/** Dream-related callback information */
-@SysUISingleton
-class DreamCallbackController @Inject constructor() :
- CallbackController<DreamCallbackController.DreamCallback> {
-
- private val callbacks = mutableSetOf<DreamCallbackController.DreamCallback>()
-
- override fun addCallback(callback: DreamCallbackController.DreamCallback) {
- callbacks.add(callback)
- }
-
- override fun removeCallback(callback: DreamCallbackController.DreamCallback) {
- callbacks.remove(callback)
- }
-
- fun onWakeUp() {
- callbacks.forEach { it.onWakeUp() }
- }
-
- interface DreamCallback {
- fun onWakeUp()
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
index abe9355..c882f8a 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
@@ -101,13 +101,11 @@
transitionViewModel.dreamOverlayTranslationY(it.translationYPx)
}
.collect { px ->
- setElementsTranslationYAtPosition(
- px,
- ComplicationLayoutParams.POSITION_TOP
- )
- setElementsTranslationYAtPosition(
- px,
- ComplicationLayoutParams.POSITION_BOTTOM
+ ComplicationLayoutParams.iteratePositions(
+ { position: Int ->
+ setElementsTranslationYAtPosition(px, position)
+ },
+ POSITION_TOP or POSITION_BOTTOM
)
}
}
@@ -115,15 +113,15 @@
/* Alpha animations, when moving from DREAMING->LOCKSCREEN state */
launch {
transitionViewModel.dreamOverlayAlpha.collect { alpha ->
- setElementsAlphaAtPosition(
- alpha = alpha,
- position = ComplicationLayoutParams.POSITION_TOP,
- fadingOut = true,
- )
- setElementsAlphaAtPosition(
- alpha = alpha,
- position = ComplicationLayoutParams.POSITION_BOTTOM,
- fadingOut = true,
+ ComplicationLayoutParams.iteratePositions(
+ { position: Int ->
+ setElementsAlphaAtPosition(
+ alpha = alpha,
+ position = position,
+ fadingOut = true,
+ )
+ },
+ POSITION_TOP or POSITION_BOTTOM
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayCallbackController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayCallbackController.kt
new file mode 100644
index 0000000..d5ff8f2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayCallbackController.kt
@@ -0,0 +1,58 @@
+/*
+ * 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 com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.policy.CallbackController
+import javax.inject.Inject
+
+/** Dream overlay-related callback information */
+@SysUISingleton
+class DreamOverlayCallbackController @Inject constructor() :
+ CallbackController<DreamOverlayCallbackController.Callback> {
+
+ private val callbacks = mutableSetOf<DreamOverlayCallbackController.Callback>()
+
+ var isDreaming = false
+ private set
+
+ override fun addCallback(callback: DreamOverlayCallbackController.Callback) {
+ callbacks.add(callback)
+ }
+
+ override fun removeCallback(callback: DreamOverlayCallbackController.Callback) {
+ callbacks.remove(callback)
+ }
+
+ fun onWakeUp() {
+ isDreaming = false
+ callbacks.forEach { it.onWakeUp() }
+ }
+
+ fun onStartDream() {
+ isDreaming = true
+ callbacks.forEach { it.onStartDream() }
+ }
+
+ interface Callback {
+ /** Dream overlay has ended */
+ fun onWakeUp()
+
+ /** Dream overlay has started */
+ fun onStartDream()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
index 1763dd9..fdc115b 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
@@ -70,7 +70,7 @@
// A controller for the dream overlay container view (which contains both the status bar and the
// content area).
private DreamOverlayContainerViewController mDreamOverlayContainerViewController;
- private final DreamCallbackController mDreamCallbackController;
+ private final DreamOverlayCallbackController mDreamOverlayCallbackController;
private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
@Nullable
private final ComponentName mLowLightDreamComponent;
@@ -151,7 +151,7 @@
TouchInsetManager touchInsetManager,
@Nullable @Named(LowLightDreamModule.LOW_LIGHT_DREAM_COMPONENT)
ComponentName lowLightDreamComponent,
- DreamCallbackController dreamCallbackController) {
+ DreamOverlayCallbackController dreamOverlayCallbackController) {
mContext = context;
mExecutor = executor;
mWindowManager = windowManager;
@@ -160,7 +160,7 @@
mKeyguardUpdateMonitor.registerCallback(mKeyguardCallback);
mStateController = stateController;
mUiEventLogger = uiEventLogger;
- mDreamCallbackController = dreamCallbackController;
+ mDreamOverlayCallbackController = dreamOverlayCallbackController;
final ViewModelStore viewModelStore = new ViewModelStore();
final Complication.Host host =
@@ -229,6 +229,7 @@
dreamComponent != null && dreamComponent.equals(mLowLightDreamComponent));
mUiEventLogger.log(DreamOverlayEvent.DREAM_OVERLAY_COMPLETE_START);
+ mDreamOverlayCallbackController.onStartDream();
mStarted = true;
});
}
@@ -245,7 +246,7 @@
public void onWakeUp(@NonNull Runnable onCompletedCallback) {
mExecutor.execute(() -> {
if (mDreamOverlayContainerViewController != null) {
- mDreamCallbackController.onWakeUp();
+ mDreamOverlayCallbackController.onWakeUp();
mDreamOverlayContainerViewController.wakeUp(onCompletedCallback, mExecutor);
}
});
@@ -255,6 +256,7 @@
* Inserts {@link Window} to host the dream overlay into the dream's parent window. Must be
* called from the main executing thread. The window attributes closely mirror those that are
* set by the {@link android.service.dreams.DreamService} on the dream Window.
+ *
* @param layoutParams The {@link android.view.WindowManager.LayoutParams} which allow inserting
* into the dream window.
*/
@@ -301,7 +303,11 @@
private void resetCurrentDreamOverlayLocked() {
if (mStarted && mWindow != null) {
- mWindowManager.removeView(mWindow.getDecorView());
+ try {
+ mWindowManager.removeView(mWindow.getDecorView());
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Error removing decor view when resetting overlay", e);
+ }
}
mStateController.setOverlayActive(false);
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 25fa915..7b876d0 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -188,10 +188,6 @@
// TODO(b/260619425): Tracking Bug
@JvmField val MODERN_ALTERNATE_BOUNCER = unreleasedFlag(219, "modern_alternate_bouncer")
- // TODO(b/262780002): Tracking Bug
- @JvmField
- val REVAMPED_WALLPAPER_UI = unreleasedFlag(222, "revamped_wallpaper_ui", teamfood = false)
-
/** Flag to control the migration of face auth to modern architecture. */
// TODO(b/262838215): Tracking bug
@JvmField val FACE_AUTH_REFACTOR = unreleasedFlag(220, "face_auth_refactor")
@@ -200,6 +196,15 @@
// TODO(b/244313043): Tracking bug
@JvmField val BIOMETRICS_ANIMATION_REVAMP = unreleasedFlag(221, "biometrics_animation_revamp")
+ // TODO(b/262780002): Tracking Bug
+ @JvmField
+ val REVAMPED_WALLPAPER_UI = unreleasedFlag(222, "revamped_wallpaper_ui", teamfood = false)
+
+ /** A different path for unocclusion transitions back to keyguard */
+ // TODO(b/262859270): Tracking Bug
+ @JvmField
+ val UNOCCLUSION_TRANSITION = unreleasedFlag(223, "unocclusion_transition", teamfood = false)
+
// 300 - power menu
// TODO(b/254512600): Tracking Bug
@JvmField val POWER_MENU_LITE = releasedFlag(300, "power_menu_lite")
@@ -315,12 +320,18 @@
// TODO(b/261734857): Tracking Bug
@JvmField val UMO_TURBULENCE_NOISE = unreleasedFlag(909, "umo_turbulence_noise")
+ // TODO(b/263272731): Tracking Bug
+ val MEDIA_TTT_RECEIVER_SUCCESS_RIPPLE =
+ unreleasedFlag(910, "media_ttt_receiver_success_ripple", teamfood = true)
+
// 1000 - dock
val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging")
// TODO(b/254512758): Tracking Bug
@JvmField val ROUNDED_BOX_RIPPLE = releasedFlag(1002, "rounded_box_ripple")
+ val SHOW_LOWLIGHT_ON_DIRECT_BOOT = unreleasedFlag(1003, "show_lowlight_on_direct_boot")
+
// 1100 - windowing
@Keep
@JvmField
@@ -447,11 +458,11 @@
@JvmField val NOTE_TASKS = unreleasedFlag(1900, "keycode_flag")
// 2000 - device controls
- @Keep @JvmField val USE_APP_PANELS = unreleasedFlag(2000, "use_app_panels", teamfood = true)
+ @Keep @JvmField val USE_APP_PANELS = releasedFlag(2000, "use_app_panels", teamfood = true)
@JvmField
val APP_PANELS_ALL_APPS_ALLOWED =
- unreleasedFlag(2001, "app_panels_all_apps_allowed", teamfood = true)
+ releasedFlag(2001, "app_panels_all_apps_allowed", teamfood = true)
// 2100 - Falsing Manager
@JvmField val FALSING_FOR_LONG_TAPS = releasedFlag(2100, "falsing_for_long_taps")
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index d597455..3beec36 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -33,7 +33,7 @@
import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT;
import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE;
import static com.android.systemui.DejankUtils.whitelistIpcs;
-import static com.android.systemui.keyguard.domain.interactor.DreamingTransitionInteractor.TO_LOCKSCREEN_DURATION_MS;
+import static com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.LOCKSCREEN_ANIMATION_DURATION_MS;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -1233,7 +1233,7 @@
mDreamOpenAnimationDuration = context.getResources().getInteger(
com.android.internal.R.integer.config_dreamOpenAnimationDuration);
- mDreamCloseAnimationDuration = (int) TO_LOCKSCREEN_DURATION_MS;
+ mDreamCloseAnimationDuration = (int) LOCKSCREEN_ANIMATION_DURATION_MS;
}
public void userActivity() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
index 9a0fbbf..a4fd087 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
@@ -29,8 +29,7 @@
import com.android.systemui.doze.DozeMachine
import com.android.systemui.doze.DozeTransitionCallback
import com.android.systemui.doze.DozeTransitionListener
-import com.android.systemui.dreams.DreamCallbackController
-import com.android.systemui.dreams.DreamCallbackController.DreamCallback
+import com.android.systemui.dreams.DreamOverlayCallbackController
import com.android.systemui.keyguard.WakefulnessLifecycle
import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
import com.android.systemui.keyguard.shared.model.BiometricUnlockSource
@@ -49,7 +48,6 @@
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.merge
/** Defines interface for classes that encapsulate application state for the keyguard. */
interface KeyguardRepository {
@@ -81,6 +79,9 @@
*/
val isKeyguardShowing: Flow<Boolean>
+ /** Is an activity showing over the keyguard? */
+ val isKeyguardOccluded: Flow<Boolean>
+
/** Observable for the signal that keyguard is about to go away. */
val isKeyguardGoingAway: Flow<Boolean>
@@ -107,6 +108,9 @@
*/
val isDreaming: Flow<Boolean>
+ /** Observable for whether the device is dreaming with an overlay, see [DreamOverlayService] */
+ val isDreamingWithOverlay: Flow<Boolean>
+
/**
* Observable for the amount of doze we are currently in.
*
@@ -179,7 +183,7 @@
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
private val dozeTransitionListener: DozeTransitionListener,
private val authController: AuthController,
- private val dreamCallbackController: DreamCallbackController,
+ private val dreamOverlayCallbackController: DreamOverlayCallbackController,
) : KeyguardRepository {
private val _animateBottomAreaDozingTransitions = MutableStateFlow(false)
override val animateBottomAreaDozingTransitions =
@@ -191,28 +195,55 @@
private val _clockPosition = MutableStateFlow(Position(0, 0))
override val clockPosition = _clockPosition.asStateFlow()
- override val isKeyguardShowing: Flow<Boolean> = conflatedCallbackFlow {
- val callback =
- object : KeyguardStateController.Callback {
- override fun onKeyguardShowingChanged() {
- trySendWithFailureLogging(
- keyguardStateController.isShowing,
- TAG,
- "updated isKeyguardShowing"
- )
- }
+ override val isKeyguardShowing: Flow<Boolean> =
+ conflatedCallbackFlow {
+ val callback =
+ object : KeyguardStateController.Callback {
+ override fun onKeyguardShowingChanged() {
+ trySendWithFailureLogging(
+ keyguardStateController.isShowing,
+ TAG,
+ "updated isKeyguardShowing"
+ )
+ }
+ }
+
+ keyguardStateController.addCallback(callback)
+ // Adding the callback does not send an initial update.
+ trySendWithFailureLogging(
+ keyguardStateController.isShowing,
+ TAG,
+ "initial isKeyguardShowing"
+ )
+
+ awaitClose { keyguardStateController.removeCallback(callback) }
}
+ .distinctUntilChanged()
- keyguardStateController.addCallback(callback)
- // Adding the callback does not send an initial update.
- trySendWithFailureLogging(
- keyguardStateController.isShowing,
- TAG,
- "initial isKeyguardShowing"
- )
+ override val isKeyguardOccluded: Flow<Boolean> =
+ conflatedCallbackFlow {
+ val callback =
+ object : KeyguardStateController.Callback {
+ override fun onKeyguardShowingChanged() {
+ trySendWithFailureLogging(
+ keyguardStateController.isOccluded,
+ TAG,
+ "updated isKeyguardOccluded"
+ )
+ }
+ }
- awaitClose { keyguardStateController.removeCallback(callback) }
- }
+ keyguardStateController.addCallback(callback)
+ // Adding the callback does not send an initial update.
+ trySendWithFailureLogging(
+ keyguardStateController.isOccluded,
+ TAG,
+ "initial isKeyguardOccluded"
+ )
+
+ awaitClose { keyguardStateController.removeCallback(callback) }
+ }
+ .distinctUntilChanged()
override val isKeyguardGoingAway: Flow<Boolean> = conflatedCallbackFlow {
val callback =
@@ -279,36 +310,45 @@
}
.distinctUntilChanged()
+ override val isDreamingWithOverlay: Flow<Boolean> =
+ conflatedCallbackFlow {
+ val callback =
+ object : DreamOverlayCallbackController.Callback {
+ override fun onStartDream() {
+ trySendWithFailureLogging(true, TAG, "updated isDreamingWithOverlay")
+ }
+ override fun onWakeUp() {
+ trySendWithFailureLogging(false, TAG, "updated isDreamingWithOverlay")
+ }
+ }
+ dreamOverlayCallbackController.addCallback(callback)
+ trySendWithFailureLogging(
+ dreamOverlayCallbackController.isDreaming,
+ TAG,
+ "initial isDreamingWithOverlay",
+ )
+
+ awaitClose { dreamOverlayCallbackController.removeCallback(callback) }
+ }
+ .distinctUntilChanged()
+
override val isDreaming: Flow<Boolean> =
- merge(
- conflatedCallbackFlow {
- val callback =
- object : KeyguardUpdateMonitorCallback() {
- override fun onDreamingStateChanged(isDreaming: Boolean) {
- trySendWithFailureLogging(isDreaming, TAG, "updated isDreaming")
- }
+ conflatedCallbackFlow {
+ val callback =
+ object : KeyguardUpdateMonitorCallback() {
+ override fun onDreamingStateChanged(isDreaming: Boolean) {
+ trySendWithFailureLogging(isDreaming, TAG, "updated isDreaming")
}
- keyguardUpdateMonitor.registerCallback(callback)
- trySendWithFailureLogging(
- keyguardUpdateMonitor.isDreaming,
- TAG,
- "initial isDreaming",
- )
+ }
+ keyguardUpdateMonitor.registerCallback(callback)
+ trySendWithFailureLogging(
+ keyguardUpdateMonitor.isDreaming,
+ TAG,
+ "initial isDreaming",
+ )
- awaitClose { keyguardUpdateMonitor.removeCallback(callback) }
- },
- conflatedCallbackFlow {
- val callback =
- object : DreamCallback {
- override fun onWakeUp() {
- trySendWithFailureLogging(false, TAG, "updated isDreaming")
- }
- }
- dreamCallbackController.addCallback(callback)
-
- awaitClose { dreamCallbackController.removeCallback(callback) }
- }
- )
+ awaitClose { keyguardUpdateMonitor.removeCallback(callback) }
+ }
.distinctUntilChanged()
override val linearDozeAmount: Flow<Float> = conflatedCallbackFlow {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
index d72d7183b..343c2dc 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
@@ -131,6 +131,10 @@
}
override fun startTransition(info: TransitionInfo): UUID? {
+ if (lastStep.from == info.from && lastStep.to == info.to) {
+ Log.i(TAG, "Duplicate call to start the transition, rejecting: $info")
+ return null
+ }
if (lastStep.transitionState != TransitionState.FINISHED) {
Log.i(TAG, "Transition still active: $lastStep, canceling")
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodToGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodToGoneTransitionInteractor.kt
deleted file mode 100644
index dad166f..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodToGoneTransitionInteractor.kt
+++ /dev/null
@@ -1,75 +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.systemui.keyguard.domain.interactor
-
-import android.animation.ValueAnimator
-import com.android.systemui.animation.Interpolators
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
-import com.android.systemui.keyguard.shared.model.BiometricUnlockModel.Companion.isWakeAndUnlock
-import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.keyguard.shared.model.TransitionInfo
-import com.android.systemui.util.kotlin.sample
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.collect
-import kotlinx.coroutines.launch
-
-@SysUISingleton
-class AodToGoneTransitionInteractor
-@Inject
-constructor(
- @Application private val scope: CoroutineScope,
- private val keyguardInteractor: KeyguardInteractor,
- private val keyguardTransitionRepository: KeyguardTransitionRepository,
- private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
-) : TransitionInteractor(AodToGoneTransitionInteractor::class.simpleName!!) {
-
- override fun start() {
- scope.launch {
- keyguardInteractor.biometricUnlockState
- .sample(keyguardTransitionInteractor.finishedKeyguardState, { a, b -> Pair(a, b) })
- .collect { pair ->
- val (biometricUnlockState, keyguardState) = pair
- if (
- keyguardState == KeyguardState.AOD && isWakeAndUnlock(biometricUnlockState)
- ) {
- keyguardTransitionRepository.startTransition(
- TransitionInfo(
- name,
- KeyguardState.AOD,
- KeyguardState.GONE,
- getAnimator(),
- )
- )
- }
- }
- }
- }
-
- private fun getAnimator(): ValueAnimator {
- return ValueAnimator().apply {
- setInterpolator(Interpolators.LINEAR)
- setDuration(TRANSITION_DURATION_MS)
- }
- }
-
- companion object {
- private const val TRANSITION_DURATION_MS = 500L
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
similarity index 76%
rename from packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt
rename to packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
index f3d2905..c2d139c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
@@ -21,6 +21,7 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.BiometricUnlockModel.Companion.isWakeAndUnlock
import com.android.systemui.keyguard.shared.model.DozeStateModel
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionInfo
@@ -30,33 +31,33 @@
import kotlinx.coroutines.launch
@SysUISingleton
-class AodLockscreenTransitionInteractor
+class FromAodTransitionInteractor
@Inject
constructor(
@Application private val scope: CoroutineScope,
private val keyguardInteractor: KeyguardInteractor,
private val keyguardTransitionRepository: KeyguardTransitionRepository,
private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
-) : TransitionInteractor(AodLockscreenTransitionInteractor::class.simpleName!!) {
+) : TransitionInteractor(FromAodTransitionInteractor::class.simpleName!!) {
override fun start() {
- listenForTransitionToAodFromLockscreen()
- listenForTransitionToLockscreenFromDozeStates()
+ listenForAodToLockscreen()
+ listenForAodToGone()
}
- private fun listenForTransitionToAodFromLockscreen() {
+ private fun listenForAodToLockscreen() {
scope.launch {
keyguardInteractor
- .dozeTransitionTo(DozeStateModel.DOZE_AOD)
+ .dozeTransitionTo(DozeStateModel.FINISH)
.sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
.collect { pair ->
val (dozeToAod, lastStartedStep) = pair
- if (lastStartedStep.to == KeyguardState.LOCKSCREEN) {
+ if (lastStartedStep.to == KeyguardState.AOD) {
keyguardTransitionRepository.startTransition(
TransitionInfo(
name,
- KeyguardState.LOCKSCREEN,
KeyguardState.AOD,
+ KeyguardState.LOCKSCREEN,
getAnimator(),
)
)
@@ -65,20 +66,20 @@
}
}
- private fun listenForTransitionToLockscreenFromDozeStates() {
- val canGoToLockscreen = setOf(KeyguardState.AOD, KeyguardState.DOZING)
+ private fun listenForAodToGone() {
scope.launch {
- keyguardInteractor
- .dozeTransitionTo(DozeStateModel.FINISH)
- .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+ keyguardInteractor.biometricUnlockState
+ .sample(keyguardTransitionInteractor.finishedKeyguardState, ::Pair)
.collect { pair ->
- val (dozeToAod, lastStartedStep) = pair
- if (canGoToLockscreen.contains(lastStartedStep.to)) {
+ val (biometricUnlockState, keyguardState) = pair
+ if (
+ keyguardState == KeyguardState.AOD && isWakeAndUnlock(biometricUnlockState)
+ ) {
keyguardTransitionRepository.startTransition(
TransitionInfo(
name,
- lastStartedStep.to,
- KeyguardState.LOCKSCREEN,
+ KeyguardState.AOD,
+ KeyguardState.GONE,
getAnimator(),
)
)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerToGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromBouncerTransitionInteractor.kt
similarity index 60%
rename from packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerToGoneTransitionInteractor.kt
rename to packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromBouncerTransitionInteractor.kt
index 056c44d..0e9c447 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerToGoneTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromBouncerTransitionInteractor.kt
@@ -23,16 +23,18 @@
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.keyguard.shared.model.WakefulnessState
import com.android.systemui.shade.data.repository.ShadeRepository
import com.android.systemui.util.kotlin.sample
import java.util.UUID
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
@SysUISingleton
-class BouncerToGoneTransitionInteractor
+class FromBouncerTransitionInteractor
@Inject
constructor(
@Application private val scope: CoroutineScope,
@@ -40,15 +42,54 @@
private val shadeRepository: ShadeRepository,
private val keyguardTransitionRepository: KeyguardTransitionRepository,
private val keyguardTransitionInteractor: KeyguardTransitionInteractor
-) : TransitionInteractor(BouncerToGoneTransitionInteractor::class.simpleName!!) {
+) : TransitionInteractor(FromBouncerTransitionInteractor::class.simpleName!!) {
private var transitionId: UUID? = null
override fun start() {
- listenForKeyguardGoingAway()
+ listenForBouncerToGone()
+ listenForBouncerToLockscreenOrAod()
}
- private fun listenForKeyguardGoingAway() {
+ private fun listenForBouncerToLockscreenOrAod() {
+ scope.launch {
+ keyguardInteractor.isBouncerShowing
+ .sample(
+ combine(
+ keyguardInteractor.wakefulnessModel,
+ keyguardTransitionInteractor.startedKeyguardTransitionStep,
+ ::Pair
+ ),
+ ::toTriple
+ )
+ .collect { triple ->
+ val (isBouncerShowing, wakefulnessState, lastStartedTransitionStep) = triple
+ if (
+ !isBouncerShowing && lastStartedTransitionStep.to == KeyguardState.BOUNCER
+ ) {
+ val to =
+ if (
+ wakefulnessState.state == WakefulnessState.STARTING_TO_SLEEP ||
+ wakefulnessState.state == WakefulnessState.ASLEEP
+ ) {
+ KeyguardState.AOD
+ } else {
+ KeyguardState.LOCKSCREEN
+ }
+ keyguardTransitionRepository.startTransition(
+ TransitionInfo(
+ ownerName = name,
+ from = KeyguardState.BOUNCER,
+ to = to,
+ animator = getAnimator(),
+ )
+ )
+ }
+ }
+ }
+ }
+
+ private fun listenForBouncerToGone() {
scope.launch {
keyguardInteractor.isKeyguardGoingAway
.sample(keyguardTransitionInteractor.finishedKeyguardState, { a, b -> Pair(a, b) })
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
similarity index 70%
rename from packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenGoneTransitionInteractor.kt
rename to packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
index 95d9602..fd2d271 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenGoneTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
@@ -21,36 +21,46 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.DozeStateModel.Companion.isDozeOff
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionInfo
import com.android.systemui.util.kotlin.sample
import javax.inject.Inject
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
@SysUISingleton
-class LockscreenGoneTransitionInteractor
+class FromDozingTransitionInteractor
@Inject
constructor(
@Application private val scope: CoroutineScope,
private val keyguardInteractor: KeyguardInteractor,
- private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
private val keyguardTransitionRepository: KeyguardTransitionRepository,
-) : TransitionInteractor(LockscreenGoneTransitionInteractor::class.simpleName!!) {
+ private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+) : TransitionInteractor(FromDozingTransitionInteractor::class.simpleName!!) {
override fun start() {
+ listenForDozingToLockscreen()
+ }
+
+ private fun listenForDozingToLockscreen() {
scope.launch {
- keyguardInteractor.isKeyguardGoingAway
+ keyguardInteractor.dozeTransitionModel
.sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
.collect { pair ->
- val (isKeyguardGoingAway, lastStartedStep) = pair
- if (isKeyguardGoingAway && lastStartedStep.to == KeyguardState.LOCKSCREEN) {
+ val (dozeTransitionModel, lastStartedTransition) = pair
+ if (
+ isDozeOff(dozeTransitionModel.to) &&
+ lastStartedTransition.to == KeyguardState.DOZING
+ ) {
keyguardTransitionRepository.startTransition(
TransitionInfo(
name,
+ KeyguardState.DOZING,
KeyguardState.LOCKSCREEN,
- KeyguardState.GONE,
getAnimator(),
)
)
@@ -59,14 +69,14 @@
}
}
- private fun getAnimator(): ValueAnimator {
+ private fun getAnimator(duration: Duration = DEFAULT_DURATION): ValueAnimator {
return ValueAnimator().apply {
setInterpolator(Interpolators.LINEAR)
- setDuration(TRANSITION_DURATION_MS)
+ setDuration(duration.inWholeMilliseconds)
}
}
companion object {
- private const val TRANSITION_DURATION_MS = 10L
+ private val DEFAULT_DURATION = 500.milliseconds
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
similarity index 81%
rename from packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingTransitionInteractor.kt
rename to packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
index 4d60579..3b09ae7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
@@ -36,75 +36,40 @@
import kotlinx.coroutines.launch
@SysUISingleton
-class DreamingTransitionInteractor
+class FromDreamingTransitionInteractor
@Inject
constructor(
@Application private val scope: CoroutineScope,
private val keyguardInteractor: KeyguardInteractor,
private val keyguardTransitionRepository: KeyguardTransitionRepository,
private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
-) : TransitionInteractor(DreamingTransitionInteractor::class.simpleName!!) {
-
- private val canDreamFrom =
- setOf(KeyguardState.LOCKSCREEN, KeyguardState.GONE, KeyguardState.DOZING)
+) : TransitionInteractor(FromDreamingTransitionInteractor::class.simpleName!!) {
override fun start() {
- listenForEntryToDreaming()
listenForDreamingToLockscreen()
+ listenForDreamingToOccluded()
listenForDreamingToGone()
listenForDreamingToDozing()
}
- private fun listenForEntryToDreaming() {
- scope.launch {
- keyguardInteractor.isDreaming
- .sample(
- combine(
- keyguardInteractor.dozeTransitionModel,
- keyguardTransitionInteractor.finishedKeyguardState,
- ::Pair
- ),
- ::toTriple
- )
- .collect { triple ->
- val (isDreaming, dozeTransitionModel, keyguardState) = triple
- // Dozing/AOD and dreaming have overlapping events. If the state remains in
- // FINISH, it means that doze mode is not running and DREAMING is ok to
- // commence.
- if (
- isDozeOff(dozeTransitionModel.to) &&
- isDreaming &&
- canDreamFrom.contains(keyguardState)
- ) {
- keyguardTransitionRepository.startTransition(
- TransitionInfo(
- name,
- keyguardState,
- KeyguardState.DREAMING,
- getAnimator(),
- )
- )
- }
- }
- }
- }
-
private fun listenForDreamingToLockscreen() {
scope.launch {
- keyguardInteractor.isDreaming
+ // Using isDreamingWithOverlay provides an optimized path to LOCKSCREEN state, which
+ // otherwise would have gone through OCCLUDED first
+ keyguardInteractor.isDreamingWithOverlay
.sample(
combine(
keyguardInteractor.dozeTransitionModel,
keyguardTransitionInteractor.startedKeyguardTransitionStep,
- ::Pair,
+ ::Pair
),
::toTriple
)
.collect { triple ->
val (isDreaming, dozeTransitionModel, lastStartedTransition) = triple
if (
- isDozeOff(dozeTransitionModel.to) &&
- !isDreaming &&
+ !isDreaming &&
+ isDozeOff(dozeTransitionModel.to) &&
lastStartedTransition.to == KeyguardState.DREAMING
) {
keyguardTransitionRepository.startTransition(
@@ -120,6 +85,42 @@
}
}
+ private fun listenForDreamingToOccluded() {
+ scope.launch {
+ keyguardInteractor.isDreaming
+ .sample(
+ combine(
+ keyguardInteractor.isKeyguardOccluded,
+ keyguardTransitionInteractor.startedKeyguardTransitionStep,
+ ::Pair,
+ ),
+ ::toTriple
+ )
+ .collect { triple ->
+ val (isDreaming, isOccluded, lastStartedTransition) = triple
+ if (
+ isOccluded &&
+ !isDreaming &&
+ (lastStartedTransition.to == KeyguardState.DREAMING ||
+ lastStartedTransition.to == KeyguardState.LOCKSCREEN)
+ ) {
+ // At the moment, checking for LOCKSCREEN state above provides a corrective
+ // action. There's no great signal to determine when the dream is ending
+ // and a transition to OCCLUDED is beginning directly. For now, the solution
+ // is DREAMING->LOCKSCREEN->OCCLUDED
+ keyguardTransitionRepository.startTransition(
+ TransitionInfo(
+ name,
+ lastStartedTransition.to,
+ KeyguardState.OCCLUDED,
+ getAnimator(),
+ )
+ )
+ }
+ }
+ }
+ }
+
private fun listenForDreamingToGone() {
scope.launch {
keyguardInteractor.biometricUnlockState
@@ -179,6 +180,5 @@
companion object {
private val DEFAULT_DURATION = 500.milliseconds
val TO_LOCKSCREEN_DURATION = 1183.milliseconds
- @JvmField val TO_LOCKSCREEN_DURATION_MS = TO_LOCKSCREEN_DURATION.inWholeMilliseconds
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GoneAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
similarity index 72%
rename from packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GoneAodTransitionInteractor.kt
rename to packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
index a50e759..553fafe 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GoneAodTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
@@ -30,19 +30,44 @@
import kotlinx.coroutines.launch
@SysUISingleton
-class GoneAodTransitionInteractor
+class FromGoneTransitionInteractor
@Inject
constructor(
@Application private val scope: CoroutineScope,
private val keyguardInteractor: KeyguardInteractor,
private val keyguardTransitionRepository: KeyguardTransitionRepository,
private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
-) : TransitionInteractor(GoneAodTransitionInteractor::class.simpleName!!) {
+) : TransitionInteractor(FromGoneTransitionInteractor::class.simpleName!!) {
override fun start() {
+ listenForGoneToAod()
+ listenForGoneToDreaming()
+ }
+
+ private fun listenForGoneToDreaming() {
+ scope.launch {
+ keyguardInteractor.isAbleToDream
+ .sample(keyguardTransitionInteractor.finishedKeyguardState, ::Pair)
+ .collect { pair ->
+ val (isAbleToDream, keyguardState) = pair
+ if (isAbleToDream && keyguardState == KeyguardState.GONE) {
+ keyguardTransitionRepository.startTransition(
+ TransitionInfo(
+ name,
+ KeyguardState.GONE,
+ KeyguardState.DREAMING,
+ getAnimator(),
+ )
+ )
+ }
+ }
+ }
+ }
+
+ private fun listenForGoneToAod() {
scope.launch {
keyguardInteractor.wakefulnessModel
- .sample(keyguardTransitionInteractor.finishedKeyguardState, { a, b -> Pair(a, b) })
+ .sample(keyguardTransitionInteractor.finishedKeyguardState, ::Pair)
.collect { pair ->
val (wakefulnessState, keyguardState) = pair
if (
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
new file mode 100644
index 0000000..326acc9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
@@ -0,0 +1,236 @@
+/*
+ * 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.domain.interactor
+
+import android.animation.ValueAnimator
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.DozeStateModel
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.StatusBarState.KEYGUARD
+import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.shade.data.repository.ShadeRepository
+import com.android.systemui.util.kotlin.sample
+import java.util.UUID
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.launch
+
+@SysUISingleton
+class FromLockscreenTransitionInteractor
+@Inject
+constructor(
+ @Application private val scope: CoroutineScope,
+ private val keyguardInteractor: KeyguardInteractor,
+ private val shadeRepository: ShadeRepository,
+ private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+ private val keyguardTransitionRepository: KeyguardTransitionRepository,
+) : TransitionInteractor(FromLockscreenTransitionInteractor::class.simpleName!!) {
+
+ private var transitionId: UUID? = null
+
+ override fun start() {
+ listenForLockscreenToGone()
+ listenForLockscreenToOccluded()
+ listenForLockscreenToAod()
+ listenForLockscreenToBouncer()
+ listenForLockscreenToDreaming()
+ listenForLockscreenToBouncerDragging()
+ }
+
+ private fun listenForLockscreenToDreaming() {
+ scope.launch {
+ keyguardInteractor.isAbleToDream
+ .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+ .collect { pair ->
+ val (isAbleToDream, lastStartedTransition) = pair
+ if (isAbleToDream && lastStartedTransition.to == KeyguardState.LOCKSCREEN) {
+ keyguardTransitionRepository.startTransition(
+ TransitionInfo(
+ name,
+ KeyguardState.LOCKSCREEN,
+ KeyguardState.DREAMING,
+ getAnimator(),
+ )
+ )
+ }
+ }
+ }
+ }
+
+ private fun listenForLockscreenToBouncer() {
+ scope.launch {
+ keyguardInteractor.isBouncerShowing
+ .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+ .collect { pair ->
+ val (isBouncerShowing, lastStartedTransitionStep) = pair
+ if (
+ isBouncerShowing && lastStartedTransitionStep.to == KeyguardState.LOCKSCREEN
+ ) {
+ keyguardTransitionRepository.startTransition(
+ TransitionInfo(
+ ownerName = name,
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.BOUNCER,
+ animator = getAnimator(),
+ )
+ )
+ }
+ }
+ }
+ }
+
+ /* Starts transitions when manually dragging up the bouncer from the lockscreen. */
+ private fun listenForLockscreenToBouncerDragging() {
+ scope.launch {
+ shadeRepository.shadeModel
+ .sample(
+ combine(
+ keyguardTransitionInteractor.finishedKeyguardState,
+ keyguardInteractor.statusBarState,
+ ::Pair
+ ),
+ ::toTriple
+ )
+ .collect { triple ->
+ val (shadeModel, keyguardState, statusBarState) = triple
+
+ val id = transitionId
+ if (id != null) {
+ // An existing `id` means a transition is started, and calls to
+ // `updateTransition` will control it until FINISHED
+ keyguardTransitionRepository.updateTransition(
+ id,
+ 1f - shadeModel.expansionAmount,
+ if (
+ shadeModel.expansionAmount == 0f || shadeModel.expansionAmount == 1f
+ ) {
+ transitionId = null
+ TransitionState.FINISHED
+ } else {
+ TransitionState.RUNNING
+ }
+ )
+ } else {
+ // TODO (b/251849525): Remove statusbarstate check when that state is
+ // integrated into KeyguardTransitionRepository
+ if (
+ keyguardState == KeyguardState.LOCKSCREEN &&
+ shadeModel.isUserDragging &&
+ statusBarState == KEYGUARD
+ ) {
+ transitionId =
+ keyguardTransitionRepository.startTransition(
+ TransitionInfo(
+ ownerName = name,
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.BOUNCER,
+ animator = null,
+ )
+ )
+ }
+ }
+ }
+ }
+ }
+
+ private fun listenForLockscreenToGone() {
+ scope.launch {
+ keyguardInteractor.isKeyguardGoingAway
+ .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+ .collect { pair ->
+ val (isKeyguardGoingAway, lastStartedStep) = pair
+ if (isKeyguardGoingAway && lastStartedStep.to == KeyguardState.LOCKSCREEN) {
+ keyguardTransitionRepository.startTransition(
+ TransitionInfo(
+ name,
+ KeyguardState.LOCKSCREEN,
+ KeyguardState.GONE,
+ getAnimator(),
+ )
+ )
+ }
+ }
+ }
+ }
+
+ private fun listenForLockscreenToOccluded() {
+ scope.launch {
+ keyguardInteractor.isKeyguardOccluded
+ .sample(
+ combine(
+ keyguardTransitionInteractor.finishedKeyguardState,
+ keyguardInteractor.isDreaming,
+ ::Pair
+ ),
+ ::toTriple
+ )
+ .collect { triple ->
+ val (isOccluded, keyguardState, isDreaming) = triple
+ // Occlusion signals come from the framework, and should interrupt any
+ // existing transition
+ if (isOccluded && !isDreaming) {
+ keyguardTransitionRepository.startTransition(
+ TransitionInfo(
+ name,
+ keyguardState,
+ KeyguardState.OCCLUDED,
+ getAnimator(),
+ )
+ )
+ }
+ }
+ }
+ }
+
+ private fun listenForLockscreenToAod() {
+ scope.launch {
+ keyguardInteractor
+ .dozeTransitionTo(DozeStateModel.DOZE_AOD)
+ .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+ .collect { pair ->
+ val (dozeToAod, lastStartedStep) = pair
+ if (lastStartedStep.to == KeyguardState.LOCKSCREEN) {
+ keyguardTransitionRepository.startTransition(
+ TransitionInfo(
+ name,
+ KeyguardState.LOCKSCREEN,
+ KeyguardState.AOD,
+ getAnimator(),
+ )
+ )
+ }
+ }
+ }
+ }
+
+ private fun getAnimator(): ValueAnimator {
+ return ValueAnimator().apply {
+ setInterpolator(Interpolators.LINEAR)
+ setDuration(TRANSITION_DURATION_MS)
+ }
+ }
+
+ companion object {
+ private const val TRANSITION_DURATION_MS = 500L
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
similarity index 62%
copy from packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt
copy to packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
index f3d2905..8878901 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
@@ -21,42 +21,43 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
-import com.android.systemui.keyguard.shared.model.DozeStateModel
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionInfo
import com.android.systemui.util.kotlin.sample
import javax.inject.Inject
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
@SysUISingleton
-class AodLockscreenTransitionInteractor
+class FromOccludedTransitionInteractor
@Inject
constructor(
@Application private val scope: CoroutineScope,
private val keyguardInteractor: KeyguardInteractor,
private val keyguardTransitionRepository: KeyguardTransitionRepository,
private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
-) : TransitionInteractor(AodLockscreenTransitionInteractor::class.simpleName!!) {
+) : TransitionInteractor(FromOccludedTransitionInteractor::class.simpleName!!) {
override fun start() {
- listenForTransitionToAodFromLockscreen()
- listenForTransitionToLockscreenFromDozeStates()
+ listenForOccludedToLockscreen()
+ listenForOccludedToDreaming()
}
- private fun listenForTransitionToAodFromLockscreen() {
+ private fun listenForOccludedToDreaming() {
scope.launch {
- keyguardInteractor
- .dozeTransitionTo(DozeStateModel.DOZE_AOD)
- .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+ keyguardInteractor.isAbleToDream
+ .sample(keyguardTransitionInteractor.finishedKeyguardState, ::Pair)
.collect { pair ->
- val (dozeToAod, lastStartedStep) = pair
- if (lastStartedStep.to == KeyguardState.LOCKSCREEN) {
+ val (isAbleToDream, keyguardState) = pair
+ if (isAbleToDream && keyguardState == KeyguardState.OCCLUDED) {
keyguardTransitionRepository.startTransition(
TransitionInfo(
name,
- KeyguardState.LOCKSCREEN,
- KeyguardState.AOD,
+ KeyguardState.OCCLUDED,
+ KeyguardState.DREAMING,
getAnimator(),
)
)
@@ -65,21 +66,21 @@
}
}
- private fun listenForTransitionToLockscreenFromDozeStates() {
- val canGoToLockscreen = setOf(KeyguardState.AOD, KeyguardState.DOZING)
+ private fun listenForOccludedToLockscreen() {
scope.launch {
- keyguardInteractor
- .dozeTransitionTo(DozeStateModel.FINISH)
+ keyguardInteractor.isKeyguardOccluded
.sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
.collect { pair ->
- val (dozeToAod, lastStartedStep) = pair
- if (canGoToLockscreen.contains(lastStartedStep.to)) {
+ val (isOccluded, lastStartedKeyguardState) = pair
+ // Occlusion signals come from the framework, and should interrupt any
+ // existing transition
+ if (!isOccluded && lastStartedKeyguardState.to == KeyguardState.OCCLUDED) {
keyguardTransitionRepository.startTransition(
TransitionInfo(
name,
- lastStartedStep.to,
+ KeyguardState.OCCLUDED,
KeyguardState.LOCKSCREEN,
- getAnimator(),
+ getAnimator(TO_LOCKSCREEN_DURATION),
)
)
}
@@ -87,14 +88,15 @@
}
}
- private fun getAnimator(): ValueAnimator {
+ private fun getAnimator(duration: Duration = DEFAULT_DURATION): ValueAnimator {
return ValueAnimator().apply {
setInterpolator(Interpolators.LINEAR)
- setDuration(TRANSITION_DURATION_MS)
+ setDuration(duration.inWholeMilliseconds)
}
}
companion object {
- private const val TRANSITION_DURATION_MS = 500L
+ private val DEFAULT_DURATION = 500.milliseconds
+ val TO_LOCKSCREEN_DURATION = 933.milliseconds
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
index 6912e1d..402c179 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -22,12 +22,16 @@
import com.android.systemui.keyguard.data.repository.KeyguardRepository
import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
import com.android.systemui.keyguard.shared.model.DozeStateModel
+import com.android.systemui.keyguard.shared.model.DozeStateModel.Companion.isDozeOff
import com.android.systemui.keyguard.shared.model.DozeTransitionModel
import com.android.systemui.keyguard.shared.model.StatusBarState
import com.android.systemui.keyguard.shared.model.WakefulnessModel
+import com.android.systemui.util.kotlin.sample
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.merge
/**
* Encapsulates business-logic related to the keyguard but not to a more specific part within it.
@@ -52,8 +56,27 @@
* but not vice-versa.
*/
val isDreaming: Flow<Boolean> = repository.isDreaming
+ /** Whether the system is dreaming with an overlay active */
+ val isDreamingWithOverlay: Flow<Boolean> = repository.isDreamingWithOverlay
+
+ /**
+ * Dozing and dreaming have overlapping events. If the doze state remains in FINISH, it means
+ * that doze mode is not running and DREAMING is ok to commence.
+ */
+ val isAbleToDream: Flow<Boolean> =
+ merge(isDreaming, isDreamingWithOverlay)
+ .sample(
+ dozeTransitionModel,
+ { isDreaming, dozeTransitionModel ->
+ isDreaming && isDozeOff(dozeTransitionModel.to)
+ }
+ )
+ .distinctUntilChanged()
+
/** Whether the keyguard is showing or not. */
val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing
+ /** Whether the keyguard is occluded (covered by an activity). */
+ val isKeyguardOccluded: Flow<Boolean> = repository.isKeyguardOccluded
/** Whether the keyguard is going away. */
val isKeyguardGoingAway: Flow<Boolean> = repository.isKeyguardGoingAway
/** Whether the bouncer is showing or not. */
@@ -74,8 +97,8 @@
/** The approximate location on the screen of the face unlock sensor, if one is available. */
val faceSensorLocation: Flow<Point?> = repository.faceSensorLocation
- fun dozeTransitionTo(state: DozeStateModel): Flow<DozeTransitionModel> {
- return dozeTransitionModel.filter { it.to == state }
+ fun dozeTransitionTo(vararg states: DozeStateModel): Flow<DozeTransitionModel> {
+ return dozeTransitionModel.filter { states.contains(it.to) }
}
fun isKeyguardShowing(): Boolean {
return repository.isKeyguardShowing()
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
index bb8b79a..fbed446 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
@@ -37,13 +37,13 @@
// exhaustive
val ret =
when (it) {
- is LockscreenBouncerTransitionInteractor -> Log.d(TAG, "Started $it")
- is AodLockscreenTransitionInteractor -> Log.d(TAG, "Started $it")
- is GoneAodTransitionInteractor -> Log.d(TAG, "Started $it")
- is LockscreenGoneTransitionInteractor -> Log.d(TAG, "Started $it")
- is AodToGoneTransitionInteractor -> Log.d(TAG, "Started $it")
- is BouncerToGoneTransitionInteractor -> Log.d(TAG, "Started $it")
- is DreamingTransitionInteractor -> Log.d(TAG, "Started $it")
+ is FromBouncerTransitionInteractor -> Log.d(TAG, "Started $it")
+ is FromAodTransitionInteractor -> Log.d(TAG, "Started $it")
+ is FromGoneTransitionInteractor -> Log.d(TAG, "Started $it")
+ is FromLockscreenTransitionInteractor -> Log.d(TAG, "Started $it")
+ is FromDreamingTransitionInteractor -> Log.d(TAG, "Started $it")
+ is FromOccludedTransitionInteractor -> Log.d(TAG, "Started $it")
+ is FromDozingTransitionInteractor -> Log.d(TAG, "Started $it")
}
it.start()
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
index 3b9d6f5..04024be 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -24,7 +24,9 @@
import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING
import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
+import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED
import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
import com.android.systemui.keyguard.shared.model.TransitionStep
import javax.inject.Inject
import kotlin.time.Duration
@@ -50,6 +52,10 @@
val dreamingToLockscreenTransition: Flow<TransitionStep> =
repository.transition(DREAMING, LOCKSCREEN)
+ /** OCCLUDED->LOCKSCREEN transition information. */
+ val occludedToLockscreenTransition: Flow<TransitionStep> =
+ repository.transition(OCCLUDED, LOCKSCREEN)
+
/** (any)->AOD transition information */
val anyStateToAodTransition: Flow<TransitionStep> =
repository.transitions.filter { step -> step.to == KeyguardState.AOD }
@@ -93,7 +99,14 @@
val start = (params.startTime / totalDuration).toFloat()
val chunks = (totalDuration / params.duration).toFloat()
return flow
- .map { step -> (step.value - start) * chunks }
+ // When starting, emit a value of 0f to give animations a chance to set initial state
+ .map { step ->
+ if (step.transitionState == STARTED) {
+ 0f
+ } else {
+ (step.value - start) * chunks
+ }
+ }
.filter { value -> value >= 0f && value <= 1f }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
index 6e25200..a59c407 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
@@ -89,6 +89,7 @@
KeyguardState.BOUNCER -> true
KeyguardState.LOCKSCREEN -> true
KeyguardState.GONE -> true
+ KeyguardState.OCCLUDED -> true
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt
deleted file mode 100644
index 5cb7d70..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt
+++ /dev/null
@@ -1,170 +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.systemui.keyguard.domain.interactor
-
-import android.animation.ValueAnimator
-import com.android.systemui.animation.Interpolators
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
-import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.keyguard.shared.model.StatusBarState.KEYGUARD
-import com.android.systemui.keyguard.shared.model.TransitionInfo
-import com.android.systemui.keyguard.shared.model.TransitionState
-import com.android.systemui.keyguard.shared.model.WakefulnessState
-import com.android.systemui.shade.data.repository.ShadeRepository
-import com.android.systemui.util.kotlin.sample
-import java.util.UUID
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.collect
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.launch
-
-@SysUISingleton
-class LockscreenBouncerTransitionInteractor
-@Inject
-constructor(
- @Application private val scope: CoroutineScope,
- private val keyguardInteractor: KeyguardInteractor,
- private val shadeRepository: ShadeRepository,
- private val keyguardTransitionRepository: KeyguardTransitionRepository,
- private val keyguardTransitionInteractor: KeyguardTransitionInteractor
-) : TransitionInteractor(LockscreenBouncerTransitionInteractor::class.simpleName!!) {
-
- private var transitionId: UUID? = null
-
- override fun start() {
- listenForDraggingUpToBouncer()
- listenForBouncer()
- }
-
- private fun listenForBouncer() {
- scope.launch {
- keyguardInteractor.isBouncerShowing
- .sample(
- combine(
- keyguardInteractor.wakefulnessModel,
- keyguardTransitionInteractor.startedKeyguardTransitionStep,
- ::Pair
- ),
- ::toTriple
- )
- .collect { triple ->
- val (isBouncerShowing, wakefulnessState, lastStartedTransitionStep) = triple
- if (
- !isBouncerShowing && lastStartedTransitionStep.to == KeyguardState.BOUNCER
- ) {
- val to =
- if (
- wakefulnessState.state == WakefulnessState.STARTING_TO_SLEEP ||
- wakefulnessState.state == WakefulnessState.ASLEEP
- ) {
- KeyguardState.AOD
- } else {
- KeyguardState.LOCKSCREEN
- }
- keyguardTransitionRepository.startTransition(
- TransitionInfo(
- ownerName = name,
- from = KeyguardState.BOUNCER,
- to = to,
- animator = getAnimator(),
- )
- )
- } else if (
- isBouncerShowing && lastStartedTransitionStep.to == KeyguardState.LOCKSCREEN
- ) {
- keyguardTransitionRepository.startTransition(
- TransitionInfo(
- ownerName = name,
- from = KeyguardState.LOCKSCREEN,
- to = KeyguardState.BOUNCER,
- animator = getAnimator(),
- )
- )
- }
- Unit
- }
- }
- }
-
- /* Starts transitions when manually dragging up the bouncer from the lockscreen. */
- private fun listenForDraggingUpToBouncer() {
- scope.launch {
- shadeRepository.shadeModel
- .sample(
- combine(
- keyguardTransitionInteractor.finishedKeyguardState,
- keyguardInteractor.statusBarState,
- ::Pair
- ),
- ::toTriple
- )
- .collect { triple ->
- val (shadeModel, keyguardState, statusBarState) = triple
-
- val id = transitionId
- if (id != null) {
- // An existing `id` means a transition is started, and calls to
- // `updateTransition` will control it until FINISHED
- keyguardTransitionRepository.updateTransition(
- id,
- 1f - shadeModel.expansionAmount,
- if (
- shadeModel.expansionAmount == 0f || shadeModel.expansionAmount == 1f
- ) {
- transitionId = null
- TransitionState.FINISHED
- } else {
- TransitionState.RUNNING
- }
- )
- } else {
- // TODO (b/251849525): Remove statusbarstate check when that state is
- // integrated into KeyguardTransitionRepository
- if (
- keyguardState == KeyguardState.LOCKSCREEN &&
- shadeModel.isUserDragging &&
- statusBarState == KEYGUARD
- ) {
- transitionId =
- keyguardTransitionRepository.startTransition(
- TransitionInfo(
- ownerName = name,
- from = KeyguardState.LOCKSCREEN,
- to = KeyguardState.BOUNCER,
- animator = null,
- )
- )
- }
- }
- }
- }
- }
-
- private fun getAnimator(): ValueAnimator {
- return ValueAnimator().apply {
- setInterpolator(Interpolators.LINEAR)
- setDuration(TRANSITION_DURATION_MS)
- }
- }
-
- companion object {
- private const val TRANSITION_DURATION_MS = 300L
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt
index 5f63ae7..81fa233 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt
@@ -32,25 +32,25 @@
@Binds
@IntoSet
- abstract fun lockscreenBouncer(
- impl: LockscreenBouncerTransitionInteractor
- ): TransitionInteractor
+ abstract fun fromBouncer(impl: FromBouncerTransitionInteractor): TransitionInteractor
@Binds
@IntoSet
- abstract fun aodLockscreen(impl: AodLockscreenTransitionInteractor): TransitionInteractor
+ abstract fun fromLockscreen(impl: FromLockscreenTransitionInteractor): TransitionInteractor
- @Binds @IntoSet abstract fun goneAod(impl: GoneAodTransitionInteractor): TransitionInteractor
+ @Binds @IntoSet abstract fun fromAod(impl: FromAodTransitionInteractor): TransitionInteractor
- @Binds @IntoSet abstract fun aodGone(impl: AodToGoneTransitionInteractor): TransitionInteractor
+ @Binds @IntoSet abstract fun fromGone(impl: FromGoneTransitionInteractor): TransitionInteractor
@Binds
@IntoSet
- abstract fun bouncerGone(impl: BouncerToGoneTransitionInteractor): TransitionInteractor
+ abstract fun fromDreaming(impl: FromDreamingTransitionInteractor): TransitionInteractor
@Binds
@IntoSet
- abstract fun lockscreenGone(impl: LockscreenGoneTransitionInteractor): TransitionInteractor
+ abstract fun fromOccluded(impl: FromOccludedTransitionInteractor): TransitionInteractor
- @Binds @IntoSet abstract fun dreaming(impl: DreamingTransitionInteractor): TransitionInteractor
+ @Binds
+ @IntoSet
+ abstract fun fromDozing(impl: FromDozingTransitionInteractor): TransitionInteractor
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
index 08ad3d5..4d24c14 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
@@ -31,4 +31,6 @@
abstract fun start()
fun <A, B, C> toTriple(a: A, bc: Pair<B, C>) = Triple(a, bc.first, bc.second)
+
+ fun <A, B, C> toTriple(ab: Pair<A, B>, c: C) = Triple(ab.first, ab.second, c)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt
index dd908c4..c757986 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt
@@ -57,4 +57,8 @@
* with SWIPE security method or face unlock without bypass.
*/
GONE,
+ /*
+ * An activity is displaying over the keyguard.
+ */
+ OCCLUDED,
}
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 ae8edfe..b19795c 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
@@ -18,6 +18,7 @@
import android.annotation.SuppressLint
import android.graphics.drawable.Animatable2
+import android.os.VibrationEffect
import android.util.Size
import android.util.TypedValue
import android.view.MotionEvent
@@ -43,8 +44,11 @@
import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.statusbar.VibratorHelper
+import com.android.systemui.util.kotlin.pairwise
import kotlin.math.pow
import kotlin.math.sqrt
+import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
@@ -93,6 +97,7 @@
view: ViewGroup,
viewModel: KeyguardBottomAreaViewModel,
falsingManager: FalsingManager?,
+ vibratorHelper: VibratorHelper?,
messageDisplayer: (Int) -> Unit,
): Binding {
val indicationArea: View = view.requireViewById(R.id.keyguard_indication_area)
@@ -118,22 +123,48 @@
viewModel = buttonModel,
falsingManager = falsingManager,
messageDisplayer = messageDisplayer,
+ vibratorHelper = vibratorHelper,
)
}
}
launch {
+ viewModel.startButton
+ .map { it.isActivated }
+ .pairwise()
+ .collect { (prev, next) ->
+ when {
+ !prev && next -> vibratorHelper?.vibrate(Vibrations.Activated)
+ prev && !next -> vibratorHelper?.vibrate(Vibrations.Deactivated)
+ }
+ }
+ }
+
+ launch {
viewModel.endButton.collect { buttonModel ->
updateButton(
view = endButton,
viewModel = buttonModel,
falsingManager = falsingManager,
messageDisplayer = messageDisplayer,
+ vibratorHelper = vibratorHelper,
)
}
}
launch {
+ viewModel.endButton
+ .map { it.isActivated }
+ .pairwise()
+ .collect { (prev, next) ->
+ when {
+ !prev && next -> vibratorHelper?.vibrate(Vibrations.Activated)
+ prev && !next -> vibratorHelper?.vibrate(Vibrations.Deactivated)
+ }
+ }
+ }
+
+ launch {
viewModel.isOverlayContainerVisible.collect { isVisible ->
overlayContainer.visibility =
if (isVisible) {
@@ -239,6 +270,7 @@
viewModel: KeyguardQuickAffordanceViewModel,
falsingManager: FalsingManager?,
messageDisplayer: (Int) -> Unit,
+ vibratorHelper: VibratorHelper?,
) {
if (!viewModel.isVisible) {
view.isVisible = false
@@ -312,7 +344,9 @@
view.isClickable = viewModel.isClickable
if (viewModel.isClickable) {
if (viewModel.useLongPress) {
- view.setOnTouchListener(OnTouchListener(view, viewModel, messageDisplayer))
+ view.setOnTouchListener(
+ OnTouchListener(view, viewModel, messageDisplayer, vibratorHelper)
+ )
} else {
view.setOnClickListener(OnClickListener(viewModel, checkNotNull(falsingManager)))
}
@@ -328,6 +362,7 @@
private val view: View,
private val viewModel: KeyguardQuickAffordanceViewModel,
private val messageDisplayer: (Int) -> Unit,
+ private val vibratorHelper: VibratorHelper?,
) : View.OnTouchListener {
private val longPressDurationMs = ViewConfiguration.getLongPressTimeout().toLong()
@@ -376,25 +411,38 @@
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
+ cancel(
+ onAnimationEnd =
+ if (System.currentTimeMillis() - downTimestamp < longPressDurationMs) {
+ Runnable {
+ messageDisplayer.invoke(
+ R.string.keyguard_affordance_press_too_short
)
- .toFloat(),
- 0f,
- )
- shakeAnimator.duration = 300
- shakeAnimator.interpolator = CycleInterpolator(5f)
- shakeAnimator.start()
- }
- cancel()
+ val amplitude =
+ view.context.resources
+ .getDimensionPixelSize(
+ R.dimen.keyguard_affordance_shake_amplitude
+ )
+ .toFloat()
+ val shakeAnimator =
+ ObjectAnimator.ofFloat(
+ view,
+ "translationX",
+ -amplitude / 2,
+ amplitude / 2,
+ )
+ shakeAnimator.duration =
+ ShakeAnimationDuration.inWholeMilliseconds
+ shakeAnimator.interpolator =
+ CycleInterpolator(ShakeAnimationCycles)
+ shakeAnimator.start()
+
+ vibratorHelper?.vibrate(Vibrations.Shake)
+ }
+ } else {
+ null
+ }
+ )
true
}
MotionEvent.ACTION_CANCEL -> {
@@ -405,11 +453,11 @@
}
}
- private fun cancel() {
+ private fun cancel(onAnimationEnd: Runnable? = null) {
downTimestamp = 0L
longPressAnimator?.cancel()
longPressAnimator = null
- view.animate().scaleX(1f).scaleY(1f)
+ view.animate().scaleX(1f).scaleY(1f).withEndAction(onAnimationEnd)
}
companion object {
@@ -461,4 +509,58 @@
val indicationTextSizePx: Int,
val buttonSizePx: Size,
)
+
+ private val ShakeAnimationDuration = 300.milliseconds
+ private val ShakeAnimationCycles = 5f
+
+ object Vibrations {
+
+ private const val SmallVibrationScale = 0.3f
+ private const val BigVibrationScale = 0.6f
+
+ val Shake =
+ VibrationEffect.startComposition()
+ .apply {
+ val vibrationDelayMs =
+ (ShakeAnimationDuration.inWholeMilliseconds / (ShakeAnimationCycles * 2))
+ .toInt()
+ val vibrationCount = ShakeAnimationCycles.toInt() * 2
+ repeat(vibrationCount) {
+ addPrimitive(
+ VibrationEffect.Composition.PRIMITIVE_TICK,
+ SmallVibrationScale,
+ vibrationDelayMs,
+ )
+ }
+ }
+ .compose()
+
+ val Activated =
+ VibrationEffect.startComposition()
+ .addPrimitive(
+ VibrationEffect.Composition.PRIMITIVE_TICK,
+ BigVibrationScale,
+ 0,
+ )
+ .addPrimitive(
+ VibrationEffect.Composition.PRIMITIVE_QUICK_RISE,
+ 0.1f,
+ 0,
+ )
+ .compose()
+
+ val Deactivated =
+ VibrationEffect.startComposition()
+ .addPrimitive(
+ VibrationEffect.Composition.PRIMITIVE_TICK,
+ BigVibrationScale,
+ 0,
+ )
+ .addPrimitive(
+ VibrationEffect.Composition.PRIMITIVE_QUICK_FALL,
+ 0.1f,
+ 0,
+ )
+ .compose()
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
index 9b36e8c..e164f5d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
@@ -17,8 +17,9 @@
package com.android.systemui.keyguard.ui.viewmodel
import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE
+import com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.keyguard.domain.interactor.DreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
+import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.AnimationParams
import javax.inject.Inject
@@ -46,12 +47,11 @@
/** Dream overlay views alpha - fade out */
val dreamOverlayAlpha: Flow<Float> = flowForAnimation(DREAM_OVERLAY_ALPHA).map { 1f - it }
- /** Dream background alpha - fade out */
- val dreamBackgroundAlpha: Flow<Float> = flowForAnimation(DREAM_BACKGROUND_ALPHA).map { 1f - it }
-
/** Lockscreen views y-translation */
fun lockscreenTranslationY(translatePx: Int): Flow<Float> {
- return flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { it * translatePx }
+ return flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value ->
+ -translatePx + (EMPHASIZED_DECELERATE.getInterpolation(value) * translatePx)
+ }
}
/** Lockscreen views alpha */
@@ -68,10 +68,12 @@
companion object {
/* Length of time before ending the dream activity, in order to start unoccluding */
val DREAM_ANIMATION_DURATION = 250.milliseconds
+ @JvmField
+ val LOCKSCREEN_ANIMATION_DURATION_MS =
+ (TO_LOCKSCREEN_DURATION - DREAM_ANIMATION_DURATION).inWholeMilliseconds
- val DREAM_OVERLAY_TRANSLATION_Y = AnimationParams(duration = 500.milliseconds)
+ val DREAM_OVERLAY_TRANSLATION_Y = AnimationParams(duration = 600.milliseconds)
val DREAM_OVERLAY_ALPHA = AnimationParams(duration = 250.milliseconds)
- val DREAM_BACKGROUND_ALPHA = AnimationParams(duration = 250.milliseconds)
val LOCKSCREEN_TRANSLATION_Y = AnimationParams(duration = TO_LOCKSCREEN_DURATION)
val LOCKSCREEN_ALPHA =
AnimationParams(startTime = 233.milliseconds, duration = 250.milliseconds)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt
new file mode 100644
index 0000000..e804562
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt
@@ -0,0 +1,63 @@
+/*
+ * 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.viewmodel
+
+import com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.FromOccludedTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.AnimationParams
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+/**
+ * Breaks down OCCLUDED->LOCKSCREEN transition into discrete steps for corresponding views to
+ * consume.
+ */
+@SysUISingleton
+class OccludedToLockscreenTransitionViewModel
+@Inject
+constructor(
+ private val interactor: KeyguardTransitionInteractor,
+) {
+ /** Lockscreen views y-translation */
+ fun lockscreenTranslationY(translatePx: Int): Flow<Float> {
+ return flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value ->
+ -translatePx + (EMPHASIZED_DECELERATE.getInterpolation(value) * translatePx)
+ }
+ }
+
+ /** Lockscreen views alpha */
+ val lockscreenAlpha: Flow<Float> = flowForAnimation(LOCKSCREEN_ALPHA)
+
+ private fun flowForAnimation(params: AnimationParams): Flow<Float> {
+ return interactor.transitionStepAnimation(
+ interactor.occludedToLockscreenTransition,
+ params,
+ totalDuration = TO_LOCKSCREEN_DURATION
+ )
+ }
+
+ companion object {
+ @JvmField val LOCKSCREEN_ANIMATION_DURATION_MS = TO_LOCKSCREEN_DURATION.inWholeMilliseconds
+ val LOCKSCREEN_TRANSLATION_Y = AnimationParams(duration = TO_LOCKSCREEN_DURATION)
+ val LOCKSCREEN_ALPHA =
+ AnimationParams(startTime = 233.milliseconds, duration = 250.milliseconds)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilter.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilter.kt
index ab93b29..d6f941d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilter.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilter.kt
@@ -58,10 +58,10 @@
// Keep track of the key used for the session tokens. This information is used to know when to
// dispatch a removed event so that a media object for a local session will be removed.
- private val keyedTokens: MutableMap<String, MutableSet<MediaSession.Token>> = mutableMapOf()
+ private val keyedTokens: MutableMap<String, MutableSet<TokenId>> = mutableMapOf()
// Keep track of which media session tokens have associated notifications.
- private val tokensWithNotifications: MutableSet<MediaSession.Token> = mutableSetOf()
+ private val tokensWithNotifications: MutableSet<TokenId> = mutableSetOf()
private val sessionListener =
object : MediaSessionManager.OnActiveSessionsChangedListener {
@@ -101,15 +101,15 @@
isSsReactivated: Boolean
) {
backgroundExecutor.execute {
- data.token?.let { tokensWithNotifications.add(it) }
+ data.token?.let { tokensWithNotifications.add(TokenId(it)) }
val isMigration = oldKey != null && key != oldKey
if (isMigration) {
keyedTokens.remove(oldKey)?.let { removed -> keyedTokens.put(key, removed) }
}
if (data.token != null) {
- keyedTokens.get(key)?.let { tokens -> tokens.add(data.token) }
+ keyedTokens.get(key)?.let { tokens -> tokens.add(TokenId(data.token)) }
?: run {
- val tokens = mutableSetOf(data.token)
+ val tokens = mutableSetOf(TokenId(data.token))
keyedTokens.put(key, tokens)
}
}
@@ -125,7 +125,7 @@
isMigration ||
remote == null ||
remote.sessionToken == data.token ||
- !tokensWithNotifications.contains(remote.sessionToken)
+ !tokensWithNotifications.contains(TokenId(remote.sessionToken))
) {
// Not filtering in this case. Passing the event along to listeners.
dispatchMediaDataLoaded(key, oldKey, data, immediately)
@@ -136,7 +136,7 @@
// If the local session uses a different notification key, then lets go a step
// farther and dismiss the media data so that media controls for the local session
// don't hang around while casting.
- if (!keyedTokens.get(key)!!.contains(remote.sessionToken)) {
+ if (!keyedTokens.get(key)!!.contains(TokenId(remote.sessionToken))) {
dispatchMediaDataRemoved(key)
}
}
@@ -199,6 +199,15 @@
packageControllers.put(controller.packageName, tokens)
}
}
- tokensWithNotifications.retainAll(controllers.map { it.sessionToken })
+ tokensWithNotifications.retainAll(controllers.map { TokenId(it.sessionToken) })
+ }
+
+ /**
+ * Represents a unique identifier for a [MediaSession.Token].
+ *
+ * It's used to avoid storing strong binders for media session tokens.
+ */
+ private data class TokenId(val id: Int) {
+ constructor(token: MediaSession.Token) : this(token.hashCode())
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt
index 03bc935..8a565fa 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt
@@ -26,4 +26,8 @@
class MediaTttFlags @Inject constructor(private val featureFlags: FeatureFlags) {
/** */
fun isMediaTttEnabled(): Boolean = featureFlags.isEnabled(Flags.MEDIA_TAP_TO_TRANSFER)
+
+ /** Check whether the flag for the receiver success state is enabled. */
+ fun isMediaTttReceiverSuccessRippleEnabled(): Boolean =
+ featureFlags.isEnabled(Flags.MEDIA_TTT_RECEIVER_SUCCESS_RIPPLE)
}
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 358534c..889147b 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
@@ -114,6 +114,9 @@
}
}
+ private var maxRippleWidth: Float = 0f
+ private var maxRippleHeight: Float = 0f
+
private fun updateMediaTapToTransferReceiverDisplay(
@StatusBarManager.MediaTransferReceiverState displayState: Int,
routeInfo: MediaRoute2Info,
@@ -216,7 +219,7 @@
expandRipple(view.requireViewById(R.id.ripple))
}
- override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
+ override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) {
val appIconView = view.getAppIconView()
appIconView.animate()
.translationYBy(getTranslationAmount().toFloat())
@@ -226,7 +229,14 @@
.alpha(0f)
.setDuration(ICON_ALPHA_ANIM_DURATION)
.start()
- (view.requireViewById(R.id.ripple) as ReceiverChipRippleView).collapseRipple(onAnimationEnd)
+
+ val rippleView: ReceiverChipRippleView = view.requireViewById(R.id.ripple)
+ if (removalReason == ChipStateReceiver.TRANSFER_TO_RECEIVER_SUCCEEDED.name &&
+ mediaTttFlags.isMediaTttReceiverSuccessRippleEnabled()) {
+ expandRippleToFull(rippleView, onAnimationEnd)
+ } else {
+ rippleView.collapseRipple(onAnimationEnd)
+ }
}
override fun getTouchableRegion(view: View, outRect: Rect) {
@@ -271,12 +281,19 @@
})
}
- private fun layoutRipple(rippleView: ReceiverChipRippleView) {
+ private fun layoutRipple(rippleView: ReceiverChipRippleView, isFullScreen: Boolean = false) {
val windowBounds = windowManager.currentWindowMetrics.bounds
val height = windowBounds.height().toFloat()
val width = windowBounds.width().toFloat()
- rippleView.setMaxSize(width / 2f, height / 2f)
+ if (isFullScreen) {
+ maxRippleHeight = height * 2f
+ maxRippleWidth = width * 2f
+ } else {
+ maxRippleHeight = height / 2f
+ maxRippleWidth = width / 2f
+ }
+ rippleView.setMaxSize(maxRippleWidth, maxRippleHeight)
// Center the ripple on the bottom of the screen in the middle.
rippleView.setCenter(width * 0.5f, height)
val color = Utils.getColorAttrDefaultColor(context, R.attr.wallpaperTextColorAccent)
@@ -286,6 +303,11 @@
private fun View.getAppIconView(): CachingIconView {
return this.requireViewById(R.id.app_icon)
}
+
+ private fun expandRippleToFull(rippleView: ReceiverChipRippleView, onAnimationEnd: Runnable?) {
+ layoutRipple(rippleView, true)
+ rippleView.expandToFull(maxRippleHeight, onAnimationEnd)
+ }
}
val ICON_TRANSLATION_ANIM_DURATION = 30.frames
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
index 6e9fc5c..87b2528 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
@@ -22,6 +22,7 @@
import android.util.AttributeSet
import com.android.systemui.surfaceeffects.ripple.RippleShader
import com.android.systemui.surfaceeffects.ripple.RippleView
+import kotlin.math.pow
/**
* An expanding ripple effect for the media tap-to-transfer receiver chip.
@@ -59,4 +60,44 @@
})
animator.reverse()
}
+
+ // Expands the ripple to cover full screen.
+ fun expandToFull(newHeight: Float, onAnimationEnd: Runnable? = null) {
+ if (!isStarted) {
+ return
+ }
+ // Reset all listeners to animator.
+ animator.removeAllListeners()
+ animator.removeAllUpdateListeners()
+
+ // Only show the outline as ripple expands and disappears when animation ends.
+ setRippleFill(false)
+
+ val startingPercentage = calculateStartingPercentage(newHeight)
+ animator.addUpdateListener { updateListener ->
+ val now = updateListener.currentPlayTime
+ val progress = updateListener.animatedValue as Float
+ rippleShader.progress = startingPercentage + (progress * (1 - startingPercentage))
+ rippleShader.distortionStrength = 1 - rippleShader.progress
+ rippleShader.pixelDensity = 1 - rippleShader.progress
+ rippleShader.time = now.toFloat()
+ invalidate()
+ }
+ animator.addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator?) {
+ animation?.let { visibility = GONE }
+ onAnimationEnd?.run()
+ isStarted = false
+ }
+ })
+ animator.start()
+ }
+
+ // Calculates the actual starting percentage according to ripple shader progress set method.
+ // Check calculations in [RippleShader.progress]
+ fun calculateStartingPercentage(newHeight: Float): Float {
+ val ratio = rippleShader.currentHeight / newHeight
+ val remainingPercentage = (1 - ratio).toDouble().pow(1 / 3.toDouble()).toFloat()
+ return 1 - remainingPercentage
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java
index 7cc95a1..fba5f63 100644
--- a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java
@@ -27,11 +27,15 @@
import androidx.activity.ComponentActivity;
import androidx.lifecycle.ViewModelProvider;
+import com.android.systemui.compose.ComposeFacade;
import com.android.systemui.people.ui.view.PeopleViewBinder;
import com.android.systemui.people.ui.viewmodel.PeopleViewModel;
import javax.inject.Inject;
+import kotlin.Unit;
+import kotlin.jvm.functions.Function1;
+
/** People Tile Widget configuration activity that shows the user their conversation tiles. */
public class PeopleSpaceActivity extends ComponentActivity {
@@ -58,13 +62,18 @@
int widgetId = getIntent().getIntExtra(EXTRA_APPWIDGET_ID, INVALID_APPWIDGET_ID);
viewModel.onWidgetIdChanged(widgetId);
- ViewGroup view = PeopleViewBinder.create(this);
- PeopleViewBinder.bind(view, viewModel, /* lifecycleOwner= */ this,
- (result) -> {
- finishActivity(result);
- return null;
- });
- setContentView(view);
+ Function1<PeopleViewModel.Result, Unit> onResult = (result) -> {
+ finishActivity(result);
+ return null;
+ };
+
+ if (ComposeFacade.INSTANCE.isComposeAvailable()) {
+ ComposeFacade.INSTANCE.setPeopleSpaceActivityContent(this, viewModel, onResult);
+ } else {
+ ViewGroup view = PeopleViewBinder.create(this);
+ PeopleViewBinder.bind(view, viewModel, /* lifecycleOwner= */ this, onResult);
+ setContentView(view);
+ }
}
private void finishActivity(PeopleViewModel.Result result) {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index a6447a5..5716a1d72 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -448,6 +448,10 @@
// Any cleanup needed when the service is being destroyed.
void onDestroy() {
+ if (mSaveInBgTask != null) {
+ // just log success/failure for the pre-existing screenshot
+ mSaveInBgTask.setActionsReadyListener(this::logSuccessOnActionsReady);
+ }
removeWindow();
releaseMediaPlayer();
releaseContext();
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationController.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationController.kt
index ba779c6..639172f 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationController.kt
@@ -19,6 +19,9 @@
import android.content.Context
import android.view.ViewGroup
import com.android.systemui.R
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.StatusBarState.SHADE
+import com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED
import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator
import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.Direction.END
import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.Direction.START
@@ -30,17 +33,24 @@
@SysUIUnfoldScope
class NotificationPanelUnfoldAnimationController
@Inject
-constructor(private val context: Context, progressProvider: NaturalRotationUnfoldProgressProvider) {
+constructor(
+ private val context: Context,
+ statusBarStateController: StatusBarStateController,
+ progressProvider: NaturalRotationUnfoldProgressProvider,
+) {
+
+ private val filterShade: () -> Boolean = { statusBarStateController.getState() == SHADE ||
+ statusBarStateController.getState() == SHADE_LOCKED }
private val translateAnimator by lazy {
UnfoldConstantTranslateAnimator(
viewsIdToTranslate =
setOf(
- ViewIdToTranslate(R.id.quick_settings_panel, START),
- ViewIdToTranslate(R.id.notification_stack_scroller, END),
- ViewIdToTranslate(R.id.rightLayout, END),
- ViewIdToTranslate(R.id.clock, START),
- ViewIdToTranslate(R.id.date, START)),
+ ViewIdToTranslate(R.id.quick_settings_panel, START, filterShade),
+ ViewIdToTranslate(R.id.notification_stack_scroller, END, filterShade),
+ ViewIdToTranslate(R.id.rightLayout, END, filterShade),
+ ViewIdToTranslate(R.id.clock, START, filterShade),
+ ViewIdToTranslate(R.id.date, START, filterShade)),
progressProvider = progressProvider)
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 1709043..d711d15 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -44,6 +44,7 @@
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;
+import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
import static java.lang.Float.isNaN;
@@ -139,7 +140,12 @@
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.keyguard.shared.model.TransitionState;
+import com.android.systemui.keyguard.shared.model.TransitionStep;
+import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel;
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel;
+import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel;
import com.android.systemui.media.controls.pipeline.MediaDataManager;
import com.android.systemui.media.controls.ui.KeyguardMediaController;
import com.android.systemui.media.controls.ui.MediaHierarchyManager;
@@ -234,6 +240,8 @@
import javax.inject.Inject;
import javax.inject.Provider;
+import kotlinx.coroutines.CoroutineDispatcher;
+
@CentralSurfacesComponent.CentralSurfacesScope
public final class NotificationPanelViewController implements Dumpable {
@@ -679,6 +687,15 @@
private boolean mGestureWaitForTouchSlop;
private boolean mIgnoreXTouchSlop;
private boolean mExpandLatencyTracking;
+ private DreamingToLockscreenTransitionViewModel mDreamingToLockscreenTransitionViewModel;
+ private OccludedToLockscreenTransitionViewModel mOccludedToLockscreenTransitionViewModel;
+
+ private KeyguardTransitionInteractor mKeyguardTransitionInteractor;
+ private CoroutineDispatcher mMainDispatcher;
+ private boolean mIsToLockscreenTransitionRunning = false;
+ private int mDreamingToLockscreenTransitionTranslationY;
+ private int mOccludedToLockscreenTransitionTranslationY;
+ private boolean mUnocclusionTransitionFlagEnabled = false;
private final Runnable mFlingCollapseRunnable = () -> fling(0, false /* expand */,
mNextCollapseSpeedUpFactor, false /* expandBecauseOfFalsing */);
@@ -694,6 +711,18 @@
}
};
+ private final Consumer<TransitionStep> mDreamingToLockscreenTransition =
+ (TransitionStep step) -> {
+ mIsToLockscreenTransitionRunning =
+ step.getTransitionState() == TransitionState.RUNNING;
+ };
+
+ private final Consumer<TransitionStep> mOccludedToLockscreenTransition =
+ (TransitionStep step) -> {
+ mIsToLockscreenTransitionRunning =
+ step.getTransitionState() == TransitionState.RUNNING;
+ };
+
@Inject
public NotificationPanelViewController(NotificationPanelView view,
@Main Handler handler,
@@ -763,6 +792,10 @@
KeyguardBottomAreaViewModel keyguardBottomAreaViewModel,
KeyguardBottomAreaInteractor keyguardBottomAreaInteractor,
AlternateBouncerInteractor alternateBouncerInteractor,
+ DreamingToLockscreenTransitionViewModel dreamingToLockscreenTransitionViewModel,
+ OccludedToLockscreenTransitionViewModel occludedToLockscreenTransitionViewModel,
+ @Main CoroutineDispatcher mainDispatcher,
+ KeyguardTransitionInteractor keyguardTransitionInteractor,
DumpManager dumpManager) {
keyguardStateController.addCallback(new KeyguardStateController.Callback() {
@Override
@@ -778,6 +811,9 @@
mShadeLog = shadeLogger;
mShadeHeightLogger = shadeHeightLogger;
mGutsManager = gutsManager;
+ mDreamingToLockscreenTransitionViewModel = dreamingToLockscreenTransitionViewModel;
+ mOccludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel;
+ mKeyguardTransitionInteractor = keyguardTransitionInteractor;
mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
@@ -855,6 +891,7 @@
mFalsingCollector = falsingCollector;
mPowerManager = powerManager;
mWakeUpCoordinator = coordinator;
+ mMainDispatcher = mainDispatcher;
mAccessibilityManager = accessibilityManager;
mView.setAccessibilityPaneTitle(determineAccessibilityPaneTitle());
setPanelAlpha(255, false /* animate */);
@@ -923,6 +960,8 @@
mNotificationPanelUnfoldAnimationController = unfoldComponent.map(
SysUIUnfoldComponent::getNotificationPanelUnfoldAnimationController);
+ mUnocclusionTransitionFlagEnabled = featureFlags.isEnabled(Flags.UNOCCLUSION_TRANSITION);
+
mQsFrameTranslateController = qsFrameTranslateController;
updateUserSwitcherFlags();
mKeyguardBottomAreaViewModel = keyguardBottomAreaViewModel;
@@ -1076,6 +1115,30 @@
mKeyguardUnfoldTransition.ifPresent(u -> u.setup(mView));
mNotificationPanelUnfoldAnimationController.ifPresent(controller ->
controller.setup(mNotificationContainerParent));
+
+ if (mUnocclusionTransitionFlagEnabled) {
+ // Dreaming->Lockscreen
+ collectFlow(mView, mKeyguardTransitionInteractor.getDreamingToLockscreenTransition(),
+ mDreamingToLockscreenTransition, mMainDispatcher);
+ collectFlow(mView, mDreamingToLockscreenTransitionViewModel.getLockscreenAlpha(),
+ toLockscreenTransitionAlpha(mNotificationStackScrollLayoutController),
+ mMainDispatcher);
+ collectFlow(mView, mDreamingToLockscreenTransitionViewModel.lockscreenTranslationY(
+ mDreamingToLockscreenTransitionTranslationY),
+ toLockscreenTransitionY(mNotificationStackScrollLayoutController),
+ mMainDispatcher);
+
+ // Occluded->Lockscreen
+ collectFlow(mView, mKeyguardTransitionInteractor.getOccludedToLockscreenTransition(),
+ mOccludedToLockscreenTransition, mMainDispatcher);
+ collectFlow(mView, mOccludedToLockscreenTransitionViewModel.getLockscreenAlpha(),
+ toLockscreenTransitionAlpha(mNotificationStackScrollLayoutController),
+ mMainDispatcher);
+ collectFlow(mView, mOccludedToLockscreenTransitionViewModel.lockscreenTranslationY(
+ mOccludedToLockscreenTransitionTranslationY),
+ toLockscreenTransitionY(mNotificationStackScrollLayoutController),
+ mMainDispatcher);
+ }
}
@VisibleForTesting
@@ -1110,6 +1173,10 @@
mUdfpsMaxYBurnInOffset = mResources.getDimensionPixelSize(R.dimen.udfps_burn_in_offset_y);
mSplitShadeScrimTransitionDistance = mResources.getDimensionPixelSize(
R.dimen.split_shade_scrim_transition_distance);
+ mDreamingToLockscreenTransitionTranslationY = mResources.getDimensionPixelSize(
+ R.dimen.dreaming_to_lockscreen_transition_lockscreen_translation_y);
+ mOccludedToLockscreenTransitionTranslationY = mResources.getDimensionPixelSize(
+ R.dimen.occluded_to_lockscreen_transition_lockscreen_translation_y);
}
private void updateViewControllers(KeyguardStatusView keyguardStatusView,
@@ -1337,8 +1404,8 @@
mFalsingManager,
mLockIconViewController,
stringResourceId ->
- mKeyguardIndicationController.showTransientIndication(stringResourceId)
- );
+ mKeyguardIndicationController.showTransientIndication(stringResourceId),
+ mVibratorHelper);
}
@VisibleForTesting
@@ -1773,10 +1840,14 @@
}
private void updateClock() {
+ if (mIsToLockscreenTransitionRunning) {
+ return;
+ }
float alpha = mClockPositionResult.clockAlpha * mKeyguardOnlyContentAlpha;
mKeyguardStatusViewController.setAlpha(alpha);
mKeyguardStatusViewController
- .setTranslationYExcludingMedia(mKeyguardOnlyTransitionTranslationY);
+ .setTranslationY(mKeyguardOnlyTransitionTranslationY, /* excludeMedia= */true);
+
if (mKeyguardQsUserSwitchController != null) {
mKeyguardQsUserSwitchController.setAlpha(alpha);
}
@@ -2660,7 +2731,9 @@
} else if (statusBarState == KEYGUARD
|| statusBarState == StatusBarState.SHADE_LOCKED) {
mKeyguardBottomArea.setVisibility(View.VISIBLE);
- mKeyguardBottomArea.setAlpha(1f);
+ if (!mIsToLockscreenTransitionRunning) {
+ mKeyguardBottomArea.setAlpha(1f);
+ }
} else {
mKeyguardBottomArea.setVisibility(View.GONE);
}
@@ -3527,6 +3600,9 @@
}
private void updateNotificationTranslucency() {
+ if (mIsToLockscreenTransitionRunning) {
+ return;
+ }
float alpha = 1f;
if (mClosingWithAlphaFadeOut && !mExpandingFromHeadsUp
&& !mHeadsUpManager.hasPinnedHeadsUp()) {
@@ -3582,6 +3658,9 @@
}
private void updateKeyguardBottomAreaAlpha() {
+ if (mIsToLockscreenTransitionRunning) {
+ return;
+ }
// There are two possible panel expansion behaviors:
// • User dragging up to unlock: we want to fade out as quick as possible
// (ALPHA_EXPANSION_THRESHOLD) to avoid seeing the bouncer over the bottom area.
@@ -3614,7 +3693,9 @@
}
private void onExpandingFinished() {
- mScrimController.onExpandingFinished();
+ if (!mUnocclusionTransitionFlagEnabled) {
+ mScrimController.onExpandingFinished();
+ }
mNotificationStackScrollLayoutController.onExpansionStopped();
mHeadsUpManager.onExpandingFinished();
mConversationNotificationManager.onNotificationPanelExpandStateChanged(isFullyCollapsed());
@@ -5809,6 +5890,32 @@
mCurrentPanelState = state;
}
+ private Consumer<Float> toLockscreenTransitionAlpha(
+ NotificationStackScrollLayoutController stackScroller) {
+ return (Float alpha) -> {
+ mKeyguardStatusViewController.setAlpha(alpha);
+ stackScroller.setAlpha(alpha);
+
+ mKeyguardBottomAreaInteractor.setAlpha(alpha);
+ mLockIconViewController.setAlpha(alpha);
+
+ if (mKeyguardQsUserSwitchController != null) {
+ mKeyguardQsUserSwitchController.setAlpha(alpha);
+ }
+ if (mKeyguardUserSwitcherController != null) {
+ mKeyguardUserSwitcherController.setAlpha(alpha);
+ }
+ };
+ }
+
+ private Consumer<Float> toLockscreenTransitionY(
+ NotificationStackScrollLayoutController stackScroller) {
+ return (Float translationY) -> {
+ mKeyguardStatusViewController.setTranslationY(translationY, /* excludeMedia= */false);
+ stackScroller.setTranslationY(translationY);
+ };
+ }
+
@VisibleForTesting
StatusBarStateController getStatusBarStateController() {
return mStatusBarStateController;
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index 3a011c5..64b6e61 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -19,6 +19,7 @@
import android.app.StatusBarManager;
import android.media.AudioManager;
import android.media.session.MediaSessionLegacyHelper;
+import android.os.PowerManager;
import android.os.SystemClock;
import android.util.Log;
import android.view.GestureDetector;
@@ -242,7 +243,9 @@
() -> mService.wakeUpIfDozing(
SystemClock.uptimeMillis(),
mView,
- "LOCK_ICON_TOUCH"));
+ "LOCK_ICON_TOUCH",
+ PowerManager.WAKE_REASON_GESTURE)
+ );
// In case we start outside of the view bounds (below the status bar), we need to
// dispatch
diff --git a/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt b/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt
index bf622c9..db70065 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt
@@ -17,6 +17,7 @@
package com.android.systemui.shade
import android.hardware.display.AmbientDisplayConfiguration
+import android.os.PowerManager
import android.os.SystemClock
import android.os.UserHandle
import android.provider.Settings
@@ -89,7 +90,8 @@
centralSurfaces.wakeUpIfDozing(
SystemClock.uptimeMillis(),
notificationShadeWindowView,
- "PULSING_SINGLE_TAP"
+ "PULSING_SINGLE_TAP",
+ PowerManager.WAKE_REASON_TAP
)
}
return true
@@ -114,7 +116,9 @@
centralSurfaces.wakeUpIfDozing(
SystemClock.uptimeMillis(),
notificationShadeWindowView,
- "PULSING_DOUBLE_TAP")
+ "PULSING_DOUBLE_TAP",
+ PowerManager.WAKE_REASON_TAP
+ )
return true
}
return false
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
index 2334a4c..9421524 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
@@ -90,8 +90,13 @@
class LinearLightRevealEffect(private val isVertical: Boolean) : LightRevealEffect {
// Interpolator that reveals >80% of the content at 0.5 progress, makes revealing faster
- private val interpolator = PathInterpolator(/* controlX1= */ 0.4f, /* controlY1= */ 0f,
- /* controlX2= */ 0.2f, /* controlY2= */ 1f)
+ private val interpolator =
+ PathInterpolator(
+ /* controlX1= */ 0.4f,
+ /* controlY1= */ 0f,
+ /* controlX2= */ 0.2f,
+ /* controlY2= */ 1f
+ )
override fun setRevealAmountOnScrim(amount: Float, scrim: LightRevealScrim) {
val interpolatedAmount = interpolator.getInterpolation(amount)
@@ -116,17 +121,17 @@
if (isVertical) {
scrim.setRevealGradientBounds(
- left = scrim.width / 2 - (scrim.width / 2) * gradientBoundsAmount,
+ left = scrim.viewWidth / 2 - (scrim.viewWidth / 2) * gradientBoundsAmount,
top = 0f,
- right = scrim.width / 2 + (scrim.width / 2) * gradientBoundsAmount,
- bottom = scrim.height.toFloat()
+ right = scrim.viewWidth / 2 + (scrim.viewWidth / 2) * gradientBoundsAmount,
+ bottom = scrim.viewHeight.toFloat()
)
} else {
scrim.setRevealGradientBounds(
left = 0f,
- top = scrim.height / 2 - (scrim.height / 2) * gradientBoundsAmount,
- right = scrim.width.toFloat(),
- bottom = scrim.height / 2 + (scrim.height / 2) * gradientBoundsAmount
+ top = scrim.viewHeight / 2 - (scrim.viewHeight / 2) * gradientBoundsAmount,
+ right = scrim.viewWidth.toFloat(),
+ bottom = scrim.viewHeight / 2 + (scrim.viewHeight / 2) * gradientBoundsAmount
)
}
}
@@ -234,7 +239,14 @@
* transparent center. The center position, size, and stops of the gradient can be manipulated to
* reveal views below the scrim as if they are being 'lit up'.
*/
-class LightRevealScrim(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
+class LightRevealScrim
+@JvmOverloads
+constructor(
+ context: Context?,
+ attrs: AttributeSet?,
+ initialWidth: Int? = null,
+ initialHeight: Int? = null
+) : View(context, attrs) {
/** Listener that is called if the scrim's opaqueness changes */
lateinit var isScrimOpaqueChangedListener: Consumer<Boolean>
@@ -278,6 +290,17 @@
var revealGradientHeight: Float = 0f
/**
+ * Keeps the initial value until the view is measured. See [LightRevealScrim.onMeasure].
+ *
+ * Needed as the view dimensions are used before the onMeasure pass happens, and without preset
+ * width and height some flicker during fold/unfold happens.
+ */
+ internal var viewWidth: Int = initialWidth ?: 0
+ private set
+ internal var viewHeight: Int = initialHeight ?: 0
+ private set
+
+ /**
* Alpha of the fill that can be used in the beginning of the animation to hide the content.
* Normally the gradient bounds are animated from small size so the content is not visible, but
* if the start gradient bounds allow to see some content this could be used to make the reveal
@@ -375,6 +398,11 @@
invalidate()
}
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+ viewWidth = measuredWidth
+ viewHeight = measuredHeight
+ }
/**
* Sets bounds for the transparent oval gradient that reveals the views below the scrim. This is
* simply a helper method that sets [revealGradientCenter], [revealGradientWidth], and
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
index b8302d7..905cc3f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
@@ -5,6 +5,7 @@
import android.animation.ValueAnimator
import android.content.Context
import android.content.res.Configuration
+import android.os.PowerManager
import android.os.SystemClock
import android.util.IndentingPrintWriter
import android.util.MathUtils
@@ -272,7 +273,12 @@
// Bind the click listener of the shelf to go to the full shade
notificationShelfController.setOnClickListener {
if (statusBarStateController.state == StatusBarState.KEYGUARD) {
- centralSurfaces.wakeUpIfDozing(SystemClock.uptimeMillis(), it, "SHADE_CLICK")
+ centralSurfaces.wakeUpIfDozing(
+ SystemClock.uptimeMillis(),
+ it,
+ "SHADE_CLICK",
+ PowerManager.WAKE_REASON_GESTURE,
+ )
goToLockedShade(it)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java
index 56b689e..7d0ac18 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java
@@ -290,7 +290,8 @@
false,
null,
0,
- false
+ false,
+ 0
);
}
return ranking;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
index 3516037..8f9365c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
@@ -24,6 +24,7 @@
import android.content.Context;
import android.content.Intent;
import android.content.pm.UserInfo;
+import android.os.PowerManager;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemClock;
@@ -64,6 +65,8 @@
import com.android.systemui.util.DumpUtilsKt;
import com.android.systemui.util.ListenerSet;
+import dagger.Lazy;
+
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
@@ -71,8 +74,6 @@
import java.util.Optional;
import java.util.function.Consumer;
-import dagger.Lazy;
-
/**
* Class for handling remote input state over a set of notifications. This class handles things
* like keeping notifications temporarily that were cancelled as a response to a remote input
@@ -120,7 +121,8 @@
View view, PendingIntent pendingIntent, RemoteViews.RemoteResponse response) {
mCentralSurfacesOptionalLazy.get().ifPresent(
centralSurfaces -> centralSurfaces.wakeUpIfDozing(
- SystemClock.uptimeMillis(), view, "NOTIFICATION_CLICK"));
+ SystemClock.uptimeMillis(), view, "NOTIFICATION_CLICK",
+ PowerManager.WAKE_REASON_GESTURE));
final NotificationEntry entry = getNotificationForParent(view.getParent());
mLogger.logInitialClick(entry, pendingIntent);
@@ -464,9 +466,6 @@
riv.getController().setRemoteInputs(inputs);
riv.getController().setEditedSuggestionInfo(editedSuggestionInfo);
ViewGroup parent = view.getParent() != null ? (ViewGroup) view.getParent() : null;
- if (parent != null) {
- riv.setDefocusTargetHeight(parent.getHeight());
- }
riv.focusAnimated(parent);
if (userMessageContent != null) {
riv.setEditTextContent(userMessageContent);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt b/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt
index c630feb..976924a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt
@@ -22,7 +22,6 @@
import android.content.Context
import android.content.res.Configuration
import android.os.PowerManager
-import android.os.PowerManager.WAKE_REASON_GESTURE
import android.os.SystemClock
import android.util.IndentingPrintWriter
import android.view.MotionEvent
@@ -249,7 +248,7 @@
}
if (statusBarStateController.isDozing) {
wakeUpCoordinator.willWakeUp = true
- mPowerManager!!.wakeUp(SystemClock.uptimeMillis(), WAKE_REASON_GESTURE,
+ mPowerManager!!.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_GESTURE,
"com.android.systemui:PULSEDRAG")
}
lockscreenShadeTransitionController.goToLockedShade(startingChild,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
index b9074f0..6e74542 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
@@ -1301,7 +1301,7 @@
}
}
String wifi = args.getString("wifi");
- if (wifi != null) {
+ if (wifi != null && !mStatusBarPipelineFlags.runNewWifiIconBackend()) {
boolean show = wifi.equals("show");
String level = args.getString("level");
if (level != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java
index c3ce593..705cf92 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.notification;
import android.app.Notification;
+import android.os.PowerManager;
import android.os.SystemClock;
import android.service.notification.StatusBarNotification;
import android.util.Log;
@@ -70,7 +71,8 @@
}
mCentralSurfacesOptional.ifPresent(centralSurfaces -> centralSurfaces.wakeUpIfDozing(
- SystemClock.uptimeMillis(), v, "NOTIFICATION_CLICK"));
+ SystemClock.uptimeMillis(), v, "NOTIFICATION_CLICK",
+ PowerManager.WAKE_REASON_GESTURE));
final ExpandableNotificationRow row = (ExpandableNotificationRow) v;
final NotificationEntry entry = row.getEntry();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/fsi/FsiChromeViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/fsi/FsiChromeViewBinder.kt
new file mode 100644
index 0000000..1a3927b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/fsi/FsiChromeViewBinder.kt
@@ -0,0 +1,99 @@
+package com.android.systemui.statusbar.notification.fsi
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.WindowManager
+import com.android.systemui.CoreStartable
+import com.android.systemui.R
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.statusbar.notification.fsi.FsiDebug.Companion.log
+import com.android.systemui.statusbar.phone.CentralSurfaces
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import java.util.concurrent.Executor
+import javax.inject.Inject
+
+@SysUISingleton
+class FsiChromeViewBinder
+@Inject
+constructor(
+ val context: Context,
+ val windowManager: WindowManager,
+ val viewModelFactory: FsiChromeViewModelFactory,
+ val layoutInflater: LayoutInflater,
+ val centralSurfaces: CentralSurfaces,
+ @Main val mainExecutor: Executor,
+ @Application val scope: CoroutineScope,
+) : CoreStartable {
+
+ companion object {
+ private const val classTag = "FsiChromeViewBinder"
+ }
+
+ private val fsiChromeView =
+ layoutInflater.inflate(R.layout.fsi_chrome_view, null /* root */, false /* attachToRoot */)
+ as FsiChromeView
+
+ var addedToWindowManager = false
+ var cornerRadius: Int = context.resources.getDimensionPixelSize(
+ R.dimen.notification_corner_radius)
+
+ override fun start() {
+ val methodTag = "start"
+ log("$classTag $methodTag ")
+
+ scope.launch {
+ log("$classTag $methodTag launch ")
+ viewModelFactory.viewModelFlow.collect { vm -> updateForViewModel(vm) }
+ }
+ }
+
+ private fun updateForViewModel(vm: FsiChromeViewModel?) {
+ val methodTag = "updateForViewModel"
+
+ if (vm == null) {
+ log("$classTag $methodTag viewModel is null, removing from window manager")
+
+ if (addedToWindowManager) {
+ windowManager.removeView(fsiChromeView)
+ addedToWindowManager = false
+ }
+ return
+ }
+
+ bindViewModel(vm, windowManager)
+
+ if (addedToWindowManager) {
+ log("$classTag $methodTag already addedToWindowManager")
+ } else {
+ windowManager.addView(fsiChromeView, FsiTaskViewConfig.getWmLayoutParams("PackageName"))
+ addedToWindowManager = true
+ }
+ }
+
+ private fun bindViewModel(
+ vm: FsiChromeViewModel,
+ windowManager: WindowManager,
+ ) {
+ log("$classTag bindViewModel")
+
+ fsiChromeView.appIconImageView.setImageDrawable(vm.appIcon)
+ fsiChromeView.appNameTextView.text = vm.appName
+
+ fsiChromeView.dismissButton.setOnClickListener { vm.onDismiss() }
+ fsiChromeView.fullscreenButton.setOnClickListener { vm.onFullscreen() }
+
+ vm.taskView.cornerRadius = cornerRadius.toFloat()
+ vm.taskView.startActivity(
+ vm.fsi,
+ FsiTaskViewConfig.getFillInIntent(),
+ FsiTaskViewConfig.getActivityOptions(context, windowManager),
+ FsiTaskViewConfig.getLaunchBounds(windowManager)
+ )
+
+ log("$classTag bindViewModel started taskview activity")
+ fsiChromeView.addView(vm.taskView)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/fsi/FsiTaskViewConfig.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/fsi/FsiTaskViewConfig.kt
new file mode 100644
index 0000000..034ab56
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/fsi/FsiTaskViewConfig.kt
@@ -0,0 +1,75 @@
+package com.android.systemui.statusbar.notification.fsi
+
+import android.app.ActivityOptions
+import android.content.Context
+import android.content.Intent
+import android.graphics.PixelFormat
+import android.graphics.Rect
+import android.os.Binder
+import android.view.ViewGroup
+import android.view.WindowManager
+
+/**
+ * Config for adding the FsiChromeView window to WindowManager and starting the FSI activity.
+ */
+class FsiTaskViewConfig {
+
+ companion object {
+
+ private const val classTag = "FsiTaskViewConfig"
+
+ fun getWmLayoutParams(packageName: String): WindowManager.LayoutParams {
+ val params: WindowManager.LayoutParams?
+ params =
+ WindowManager.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
+ WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or
+ WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED or
+ WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER,
+ PixelFormat.TRANSLUCENT
+ )
+ params.setTrustedOverlay()
+ params.fitInsetsTypes = 0
+ params.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
+ params.token = Binder()
+ params.packageName = packageName
+ params.layoutInDisplayCutoutMode =
+ WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
+ params.privateFlags =
+ params.privateFlags or WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS
+ return params
+ }
+
+ fun getFillInIntent(): Intent {
+ val fillInIntent = Intent()
+ fillInIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT)
+ fillInIntent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
+ // FLAG_ACTIVITY_NEW_TASK is auto-applied because
+ // we're starting the FSI activity from a non-Activity context
+ return fillInIntent
+ }
+
+ fun getLaunchBounds(windowManager: WindowManager): Rect {
+ // TODO(b/243421660) check this works for non-resizeable activity
+ return Rect()
+ }
+
+ fun getActivityOptions(context: Context, windowManager: WindowManager): ActivityOptions {
+ // Custom options so there is no activity transition animation
+ val options =
+ ActivityOptions.makeCustomAnimation(context, 0 /* enterResId */, 0 /* exitResId */)
+
+ options.taskAlwaysOnTop = true
+
+ options.pendingIntentLaunchFlags =
+ Intent.FLAG_ACTIVITY_NEW_DOCUMENT or
+ Intent.FLAG_ACTIVITY_MULTIPLE_TASK or
+ Intent.FLAG_ACTIVITY_NEW_TASK
+
+ options.launchBounds = getLaunchBounds(windowManager)
+ return options
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 4bcc0b6..c2c38a7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -916,6 +916,11 @@
return mView.getTranslationX();
}
+ /** Set view y-translation */
+ public void setTranslationY(float translationY) {
+ mView.setTranslationY(translationY);
+ }
+
public int indexOfChild(View view) {
return mView.indexOfChild(view);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
index d240d5a..9a8c5d7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
@@ -420,7 +420,7 @@
Runnable wakeUp = ()-> {
if (!wasDeviceInteractive || mUpdateMonitor.isDreaming()) {
mLogger.i("bio wakelock: Authenticated, waking up...");
- mPowerManager.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_GESTURE,
+ mPowerManager.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_BIOMETRIC,
"android.policy:BIOMETRIC");
}
Trace.beginSection("release wake-and-unlock");
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
index c7c6441..cf2f7742 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
@@ -25,6 +25,7 @@
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
+import android.os.PowerManager;
import android.os.UserHandle;
import android.service.notification.StatusBarNotification;
import android.view.KeyEvent;
@@ -205,7 +206,10 @@
@Override
Lifecycle getLifecycle();
- void wakeUpIfDozing(long time, View where, String why);
+ /**
+ * Wakes up the device if the device was dozing.
+ */
+ void wakeUpIfDozing(long time, View where, String why, @PowerManager.WakeReason int wakeReason);
NotificationShadeWindowView getNotificationShadeWindowView();
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 c1ed10c..22ebcab 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -887,8 +887,6 @@
mKeyguardIndicationController.init();
mColorExtractor.addOnColorsChangedListener(mOnColorsChangedListener);
- mStatusBarStateController.addCallback(mStateListener,
- SysuiStatusBarStateController.RANK_STATUS_BAR);
mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
@@ -1507,10 +1505,11 @@
* @param why the reason for the wake up
*/
@Override
- public void wakeUpIfDozing(long time, View where, String why) {
+ public void wakeUpIfDozing(long time, View where, String why,
+ @PowerManager.WakeReason int wakeReason) {
if (mDozing && mScreenOffAnimationController.allowWakeUpIfDozing()) {
mPowerManager.wakeUp(
- time, PowerManager.WAKE_REASON_GESTURE, "com.android.systemui:" + why);
+ time, wakeReason, "com.android.systemui:" + why);
mWakeUpComingFromTouch = true;
mFalsingCollector.onScreenOnFromTouch();
}
@@ -1587,6 +1586,8 @@
protected void startKeyguard() {
Trace.beginSection("CentralSurfaces#startKeyguard");
+ mStatusBarStateController.addCallback(mStateListener,
+ SysuiStatusBarStateController.RANK_STATUS_BAR);
mBiometricUnlockController = mBiometricUnlockControllerLazy.get();
mBiometricUnlockController.addBiometricModeListener(
new BiometricUnlockController.BiometricModeListener() {
@@ -3364,7 +3365,8 @@
mStatusBarHideIconsForBouncerManager.setBouncerShowingAndTriggerUpdate(bouncerShowing);
mCommandQueue.recomputeDisableFlags(mDisplayId, true /* animate */);
if (mBouncerShowing) {
- wakeUpIfDozing(SystemClock.uptimeMillis(), null, "BOUNCER_VISIBLE");
+ wakeUpIfDozing(SystemClock.uptimeMillis(), null, "BOUNCER_VISIBLE",
+ PowerManager.WAKE_REASON_GESTURE);
}
updateScrimController();
if (!mBouncerShowing) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java
index c7be219..c72eb05 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java
@@ -42,6 +42,8 @@
import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.WifiIconState;
import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView;
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel;
+import com.android.systemui.statusbar.pipeline.wifi.ui.view.ModernStatusBarWifiView;
+import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel;
import java.util.ArrayList;
import java.util.List;
@@ -56,14 +58,17 @@
private final int mIconSize;
private StatusBarWifiView mWifiView;
+ private ModernStatusBarWifiView mModernWifiView;
private boolean mDemoMode;
private int mColor;
private final MobileIconsViewModel mMobileIconsViewModel;
+ private final StatusBarLocation mLocation;
public DemoStatusIcons(
LinearLayout statusIcons,
MobileIconsViewModel mobileIconsViewModel,
+ StatusBarLocation location,
int iconSize
) {
super(statusIcons.getContext());
@@ -71,6 +76,7 @@
mIconSize = iconSize;
mColor = DarkIconDispatcher.DEFAULT_ICON_TINT;
mMobileIconsViewModel = mobileIconsViewModel;
+ mLocation = location;
if (statusIcons instanceof StatusIconContainer) {
setShouldRestrictIcons(((StatusIconContainer) statusIcons).isRestrictingIcons());
@@ -233,14 +239,14 @@
public void addDemoWifiView(WifiIconState state) {
Log.d(TAG, "addDemoWifiView: ");
- // TODO(b/238425913): Migrate this view to {@code ModernStatusBarWifiView}.
StatusBarWifiView view = StatusBarWifiView.fromContext(mContext, state.slot);
int viewIndex = getChildCount();
// If we have mobile views, put wifi before them
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
- if (child instanceof StatusBarMobileView) {
+ if (child instanceof StatusBarMobileView
+ || child instanceof ModernStatusBarMobileView) {
viewIndex = i;
break;
}
@@ -287,7 +293,7 @@
ModernStatusBarMobileView view = ModernStatusBarMobileView.constructAndBind(
mobileContext,
"mobile",
- mMobileIconsViewModel.viewModelForSub(subId)
+ mMobileIconsViewModel.viewModelForSub(subId, mLocation)
);
// mobile always goes at the end
@@ -296,6 +302,30 @@
}
/**
+ * Add a {@link ModernStatusBarWifiView}
+ */
+ public void addModernWifiView(LocationBasedWifiViewModel viewModel) {
+ Log.d(TAG, "addModernDemoWifiView: ");
+ ModernStatusBarWifiView view = ModernStatusBarWifiView
+ .constructAndBind(mContext, "wifi", viewModel);
+
+ int viewIndex = getChildCount();
+ // If we have mobile views, put wifi before them
+ for (int i = 0; i < getChildCount(); i++) {
+ View child = getChildAt(i);
+ if (child instanceof StatusBarMobileView
+ || child instanceof ModernStatusBarMobileView) {
+ viewIndex = i;
+ break;
+ }
+ }
+
+ mModernWifiView = view;
+ mModernWifiView.setStaticDrawableColor(mColor);
+ addView(view, viewIndex, createLayoutParams());
+ }
+
+ /**
* Apply an update to a mobile icon view for the given {@link MobileIconState}. For
* compatibility with {@link MobileContextProvider}, we have to recreate the view every time we
* update it, since the context (and thus the {@link Configuration}) may have changed
@@ -317,8 +347,14 @@
public void onRemoveIcon(StatusIconDisplayable view) {
if (view.getSlot().equals("wifi")) {
- removeView(mWifiView);
- mWifiView = null;
+ if (view instanceof StatusBarWifiView) {
+ removeView(mWifiView);
+ mWifiView = null;
+ } else if (view instanceof ModernStatusBarWifiView) {
+ Log.d(TAG, "onRemoveIcon: removing modern wifi view");
+ removeView(mModernWifiView);
+ mModernWifiView = null;
+ }
} else if (view instanceof StatusBarMobileView) {
StatusBarMobileView mobileView = matchingMobileView(view);
if (mobileView != null) {
@@ -371,8 +407,14 @@
if (mWifiView != null) {
mWifiView.onDarkChanged(areas, darkIntensity, tint);
}
+ if (mModernWifiView != null) {
+ mModernWifiView.onDarkChanged(areas, darkIntensity, tint);
+ }
for (StatusBarMobileView view : mMobileViews) {
view.onDarkChanged(areas, darkIntensity, tint);
}
+ for (ModernStatusBarMobileView view : mModernMobileViews) {
+ view.onDarkChanged(areas, darkIntensity, tint);
+ }
}
}
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 2ce1163..e4227dc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt
@@ -30,6 +30,7 @@
import com.android.systemui.keyguard.ui.binder.KeyguardBottomAreaViewBinder.bind
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel
import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.statusbar.VibratorHelper
/**
* Renders the bottom area of the lock-screen. Concerned primarily with the quick affordance UI
@@ -65,12 +66,14 @@
falsingManager: FalsingManager? = null,
lockIconViewController: LockIconViewController? = null,
messageDisplayer: MessageDisplayer? = null,
+ vibratorHelper: VibratorHelper? = null,
) {
binding =
bind(
this,
viewModel,
falsingManager,
+ vibratorHelper,
) {
messageDisplayer?.display(it)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index d500f99..ee8b861 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -232,7 +232,6 @@
private boolean mExpansionAffectsAlpha = true;
private boolean mAnimateChange;
private boolean mUpdatePending;
- private boolean mTracking;
private long mAnimationDuration = -1;
private long mAnimationDelay;
private Animator.AnimatorListener mAnimatorListener;
@@ -526,7 +525,6 @@
}
public void onTrackingStarted() {
- mTracking = true;
mDarkenWhileDragging = !mKeyguardStateController.canDismissLockScreen();
if (!mKeyguardUnlockAnimationController.isPlayingCannedUnlockAnimation()) {
mAnimatingPanelExpansionOnUnlock = false;
@@ -534,7 +532,6 @@
}
public void onExpandingFinished() {
- mTracking = false;
setUnocclusionAnimationRunning(false);
}
@@ -1450,8 +1447,6 @@
pw.print(" expansionProgress=");
pw.println(mTransitionToLockScreenFullShadeNotificationsProgress);
- pw.print(" mTracking=");
- pw.println(mTracking);
pw.print(" mDefaultScrimAlpha=");
pw.println(mDefaultScrimAlpha);
pw.print(" mPanelExpansionFraction=");
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
index df3ab49..1a14a036 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
@@ -359,6 +359,7 @@
// Whether or not these icons show up in dumpsys
protected boolean mShouldLog = false;
private StatusBarIconController mController;
+ private final StatusBarLocation mLocation;
// Enables SystemUI demo mode to take effect in this group
protected boolean mDemoable = true;
@@ -381,11 +382,12 @@
mContext = group.getContext();
mIconSize = mContext.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.status_bar_icon_size);
+ mLocation = location;
if (statusBarPipelineFlags.runNewMobileIconsBackend()) {
// This starts the flow for the new pipeline, and will notify us of changes if
// {@link StatusBarPipelineFlags#useNewMobileIcons} is also true.
- mMobileIconsViewModel = mobileUiAdapter.createMobileIconsViewModel();
+ mMobileIconsViewModel = mobileUiAdapter.getMobileIconsViewModel();
MobileIconsBinder.bind(mGroup, mMobileIconsViewModel);
} else {
mMobileIconsViewModel = null;
@@ -394,7 +396,7 @@
if (statusBarPipelineFlags.runNewWifiIconBackend()) {
// This starts the flow for the new pipeline, and will notify us of changes if
// {@link StatusBarPipelineFlags#useNewWifiIcon} is also true.
- mWifiViewModel = wifiUiAdapter.bindGroup(mGroup, location);
+ mWifiViewModel = wifiUiAdapter.bindGroup(mGroup, mLocation);
} else {
mWifiViewModel = null;
}
@@ -495,6 +497,11 @@
ModernStatusBarWifiView view = onCreateModernStatusBarWifiView(slot);
mGroup.addView(view, index, onCreateLayoutParams());
+
+ if (mIsInDemoMode) {
+ mDemoStatusIcons.addModernWifiView(mWifiViewModel);
+ }
+
return view;
}
@@ -569,7 +576,7 @@
.constructAndBind(
mobileContext,
slot,
- mMobileIconsViewModel.viewModelForSub(subId)
+ mMobileIconsViewModel.viewModelForSub(subId, mLocation)
);
}
@@ -686,6 +693,9 @@
mIsInDemoMode = true;
if (mDemoStatusIcons == null) {
mDemoStatusIcons = createDemoStatusIcons();
+ if (mStatusBarPipelineFlags.useNewWifiIcon()) {
+ mDemoStatusIcons.addModernWifiView(mWifiViewModel);
+ }
}
mDemoStatusIcons.onDemoModeStarted();
}
@@ -705,7 +715,12 @@
}
protected DemoStatusIcons createDemoStatusIcons() {
- return new DemoStatusIcons((LinearLayout) mGroup, mMobileIconsViewModel, mIconSize);
+ return new DemoStatusIcons(
+ (LinearLayout) mGroup,
+ mMobileIconsViewModel,
+ mLocation,
+ mIconSize
+ );
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index 3e3ce77..61ddf8c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -137,7 +137,7 @@
private final PrimaryBouncerInteractor mPrimaryBouncerInteractor;
private final AlternateBouncerInteractor mAlternateBouncerInteractor;
private final BouncerView mPrimaryBouncerView;
- private final Lazy<com.android.systemui.shade.ShadeController> mShadeController;
+ private final Lazy<ShadeController> mShadeController;
// Local cache of expansion events, to avoid duplicates
private float mFraction = -1f;
@@ -255,6 +255,7 @@
final Set<KeyguardViewManagerCallback> mCallbacks = new HashSet<>();
private boolean mIsModernBouncerEnabled;
private boolean mIsModernAlternateBouncerEnabled;
+ private boolean mIsUnoccludeTransitionFlagEnabled;
private OnDismissAction mAfterKeyguardGoneAction;
private Runnable mKeyguardGoneCancelAction;
@@ -335,6 +336,7 @@
mIsModernBouncerEnabled = featureFlags.isEnabled(Flags.MODERN_BOUNCER);
mIsModernAlternateBouncerEnabled = featureFlags.isEnabled(Flags.MODERN_ALTERNATE_BOUNCER);
mAlternateBouncerInteractor = alternateBouncerInteractor;
+ mIsUnoccludeTransitionFlagEnabled = featureFlags.isEnabled(Flags.UNOCCLUSION_TRANSITION);
}
@Override
@@ -891,8 +893,10 @@
// by a FLAG_DISMISS_KEYGUARD_ACTIVITY.
reset(isOccluding /* hideBouncerWhenShowing*/);
}
- if (animate && !isOccluded && isShowing && !primaryBouncerIsShowing()) {
- mCentralSurfaces.animateKeyguardUnoccluding();
+ if (!mIsUnoccludeTransitionFlagEnabled) {
+ if (animate && !isOccluded && isShowing && !primaryBouncerIsShowing()) {
+ mCentralSurfaces.animateKeyguardUnoccluding();
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
index a1e0c50..da1c361 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
@@ -20,6 +20,7 @@
import android.app.KeyguardManager;
import android.content.Context;
+import android.os.PowerManager;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemClock;
@@ -270,7 +271,8 @@
boolean nowExpanded) {
mHeadsUpManager.setExpanded(clickedEntry, nowExpanded);
mCentralSurfaces.wakeUpIfDozing(
- SystemClock.uptimeMillis(), clickedView, "NOTIFICATION_CLICK");
+ SystemClock.uptimeMillis(), clickedView, "NOTIFICATION_CLICK",
+ PowerManager.WAKE_REASON_GESTURE);
if (nowExpanded) {
if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD) {
mShadeTransitionController.goToLockedShade(clickedEntry.getRow());
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
index c350c78..0d01715 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
@@ -36,7 +36,7 @@
import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl
import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
-import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositorySwitcher
import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor
import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl
import dagger.Binds
@@ -56,7 +56,7 @@
@Binds
abstract fun connectivityRepository(impl: ConnectivityRepositoryImpl): ConnectivityRepository
- @Binds abstract fun wifiRepository(impl: WifiRepositoryImpl): WifiRepository
+ @Binds abstract fun wifiRepository(impl: WifiRepositorySwitcher): WifiRepository
@Binds
abstract fun wifiInteractor(impl: WifiInteractorImpl): WifiInteractor
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt
index a6b04e4..1aa954f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt
@@ -17,7 +17,6 @@
package com.android.systemui.statusbar.pipeline.mobile.data.model
import android.annotation.IntRange
-import android.telephony.Annotation.DataActivityType
import android.telephony.CellSignalStrength
import android.telephony.TelephonyCallback.CarrierNetworkListener
import android.telephony.TelephonyCallback.DataActivityListener
@@ -27,7 +26,10 @@
import android.telephony.TelephonyCallback.SignalStrengthsListener
import android.telephony.TelephonyDisplayInfo
import android.telephony.TelephonyManager
+import com.android.systemui.log.table.Diffable
+import com.android.systemui.log.table.TableRowLogger
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Disconnected
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
/**
* Data class containing all of the relevant information for a particular line of service, known as
@@ -39,31 +41,112 @@
* threading complex system objects through the pipeline.
*/
data class MobileConnectionModel(
- /** From [ServiceStateListener.onServiceStateChanged] */
+ /** Fields below are from [ServiceStateListener.onServiceStateChanged] */
val isEmergencyOnly: Boolean = false,
val isRoaming: Boolean = false,
+ /**
+ * See [android.telephony.ServiceState.getOperatorAlphaShort], this value is defined as the
+ * current registered operator name in short alphanumeric format. In some cases this name might
+ * be preferred over other methods of calculating the network name
+ */
+ val operatorAlphaShort: String? = null,
- /** From [SignalStrengthsListener.onSignalStrengthsChanged] */
+ /** Fields below from [SignalStrengthsListener.onSignalStrengthsChanged] */
val isGsm: Boolean = false,
@IntRange(from = 0, to = 4)
val cdmaLevel: Int = CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN,
@IntRange(from = 0, to = 4)
val primaryLevel: Int = CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN,
- /** Mapped from [DataConnectionStateListener.onDataConnectionStateChanged] */
+ /** Fields below from [DataConnectionStateListener.onDataConnectionStateChanged] */
val dataConnectionState: DataConnectionState = Disconnected,
- /** From [DataActivityListener.onDataActivity]. See [TelephonyManager] for the values */
- @DataActivityType val dataActivityDirection: Int? = null,
+ /**
+ * Fields below from [DataActivityListener.onDataActivity]. See [TelephonyManager] for the
+ * values
+ */
+ val dataActivityDirection: DataActivityModel =
+ DataActivityModel(
+ hasActivityIn = false,
+ hasActivityOut = false,
+ ),
- /** From [CarrierNetworkListener.onCarrierNetworkChange] */
+ /** Fields below from [CarrierNetworkListener.onCarrierNetworkChange] */
val carrierNetworkChangeActive: Boolean = false,
+ /** Fields below from [DisplayInfoListener.onDisplayInfoChanged]. */
+
/**
- * From [DisplayInfoListener.onDisplayInfoChanged].
- *
* [resolvedNetworkType] is the [TelephonyDisplayInfo.getOverrideNetworkType] if it exists or
* [TelephonyDisplayInfo.getNetworkType]. This is used to look up the proper network type icon
*/
val resolvedNetworkType: ResolvedNetworkType = ResolvedNetworkType.UnknownNetworkType,
-)
+) : Diffable<MobileConnectionModel> {
+ override fun logDiffs(prevVal: MobileConnectionModel, row: TableRowLogger) {
+ if (prevVal.dataConnectionState != dataConnectionState) {
+ row.logChange(COL_CONNECTION_STATE, dataConnectionState.toString())
+ }
+
+ if (prevVal.isEmergencyOnly != isEmergencyOnly) {
+ row.logChange(COL_EMERGENCY, isEmergencyOnly)
+ }
+
+ if (prevVal.isRoaming != isRoaming) {
+ row.logChange(COL_ROAMING, isRoaming)
+ }
+
+ if (prevVal.operatorAlphaShort != operatorAlphaShort) {
+ row.logChange(COL_OPERATOR, operatorAlphaShort)
+ }
+
+ if (prevVal.isGsm != isGsm) {
+ row.logChange(COL_IS_GSM, isGsm)
+ }
+
+ if (prevVal.cdmaLevel != cdmaLevel) {
+ row.logChange(COL_CDMA_LEVEL, cdmaLevel)
+ }
+
+ if (prevVal.primaryLevel != primaryLevel) {
+ row.logChange(COL_PRIMARY_LEVEL, primaryLevel)
+ }
+
+ if (prevVal.dataActivityDirection != dataActivityDirection) {
+ row.logChange(COL_ACTIVITY_DIRECTION, dataActivityDirection.toString())
+ }
+
+ if (prevVal.carrierNetworkChangeActive != carrierNetworkChangeActive) {
+ row.logChange(COL_CARRIER_NETWORK_CHANGE, carrierNetworkChangeActive)
+ }
+
+ if (prevVal.resolvedNetworkType != resolvedNetworkType) {
+ row.logChange(COL_RESOLVED_NETWORK_TYPE, resolvedNetworkType.toString())
+ }
+ }
+
+ override fun logFull(row: TableRowLogger) {
+ row.logChange(COL_CONNECTION_STATE, dataConnectionState.toString())
+ row.logChange(COL_EMERGENCY, isEmergencyOnly)
+ row.logChange(COL_ROAMING, isRoaming)
+ row.logChange(COL_OPERATOR, operatorAlphaShort)
+ row.logChange(COL_IS_GSM, isGsm)
+ row.logChange(COL_CDMA_LEVEL, cdmaLevel)
+ row.logChange(COL_PRIMARY_LEVEL, primaryLevel)
+ row.logChange(COL_ACTIVITY_DIRECTION, dataActivityDirection.toString())
+ row.logChange(COL_CARRIER_NETWORK_CHANGE, carrierNetworkChangeActive)
+ row.logChange(COL_RESOLVED_NETWORK_TYPE, resolvedNetworkType.toString())
+ }
+
+ companion object {
+ const val COL_EMERGENCY = "EmergencyOnly"
+ const val COL_ROAMING = "Roaming"
+ const val COL_OPERATOR = "OperatorName"
+ const val COL_IS_GSM = "IsGsm"
+ const val COL_CDMA_LEVEL = "CdmaLevel"
+ const val COL_PRIMARY_LEVEL = "PrimaryLevel"
+ const val COL_CONNECTION_STATE = "ConnectionState"
+ const val COL_ACTIVITY_DIRECTION = "DataActivity"
+ const val COL_CARRIER_NETWORK_CHANGE = "CarrierNetworkChangeActive"
+ const val COL_RESOLVED_NETWORK_TYPE = "NetworkType"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/NetworkNameModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/NetworkNameModel.kt
new file mode 100644
index 0000000..c50d82a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/NetworkNameModel.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.model
+
+import android.content.Intent
+import android.telephony.TelephonyManager.EXTRA_DATA_SPN
+import android.telephony.TelephonyManager.EXTRA_PLMN
+import android.telephony.TelephonyManager.EXTRA_SHOW_PLMN
+import android.telephony.TelephonyManager.EXTRA_SHOW_SPN
+import com.android.systemui.log.table.Diffable
+import com.android.systemui.log.table.TableRowLogger
+
+/**
+ * Encapsulates the data needed to show a network name for a mobile network. The data is parsed from
+ * the intent sent by [android.telephony.TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED].
+ */
+sealed interface NetworkNameModel : Diffable<NetworkNameModel> {
+ val name: String
+
+ /** The default name is read from [com.android.internal.R.string.lockscreen_carrier_default] */
+ data class Default(override val name: String) : NetworkNameModel {
+ override fun logDiffs(prevVal: NetworkNameModel, row: TableRowLogger) {
+ if (prevVal !is Default || prevVal.name != name) {
+ row.logChange(COL_NETWORK_NAME, "Default($name)")
+ }
+ }
+
+ override fun logFull(row: TableRowLogger) {
+ row.logChange(COL_NETWORK_NAME, "Default($name)")
+ }
+ }
+
+ /**
+ * This name has been derived from telephony intents. see
+ * [android.telephony.TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED]
+ */
+ data class Derived(override val name: String) : NetworkNameModel {
+ override fun logDiffs(prevVal: NetworkNameModel, row: TableRowLogger) {
+ if (prevVal !is Derived || prevVal.name != name) {
+ row.logChange(COL_NETWORK_NAME, "Derived($name)")
+ }
+ }
+
+ override fun logFull(row: TableRowLogger) {
+ row.logChange(COL_NETWORK_NAME, "Derived($name)")
+ }
+ }
+
+ companion object {
+ const val COL_NETWORK_NAME = "networkName"
+ }
+}
+
+fun Intent.toNetworkNameModel(separator: String): NetworkNameModel? {
+ val showSpn = getBooleanExtra(EXTRA_SHOW_SPN, false)
+ val spn = getStringExtra(EXTRA_DATA_SPN)
+ val showPlmn = getBooleanExtra(EXTRA_SHOW_PLMN, false)
+ val plmn = getStringExtra(EXTRA_PLMN)
+
+ val str = StringBuilder()
+ val strData = StringBuilder()
+ if (showPlmn && plmn != null) {
+ str.append(plmn)
+ strData.append(plmn)
+ }
+ if (showSpn && spn != null) {
+ if (str.isNotEmpty()) {
+ str.append(separator)
+ }
+ str.append(spn)
+ }
+
+ return if (str.isNotEmpty()) NetworkNameModel.Derived(str.toString()) else null
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
index fc59f6e..40e9ba1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
@@ -20,7 +20,9 @@
import android.telephony.SubscriptionManager
import android.telephony.TelephonyCallback
import android.telephony.TelephonyManager
+import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
@@ -38,6 +40,13 @@
interface MobileConnectionRepository {
/** The subscriptionId that this connection represents */
val subId: Int
+
+ /**
+ * The table log buffer created for this connection. Will have the name "MobileConnectionLog
+ * [subId]"
+ */
+ val tableLogBuffer: TableLogBuffer
+
/**
* A flow that aggregates all necessary callbacks from [TelephonyCallback] into a single
* listener + model.
@@ -58,4 +67,7 @@
* True if the Enhanced Roaming Indicator (ERI) display number is not [TelephonyManager.ERI_OFF]
*/
val cdmaRoaming: StateFlow<Boolean>
+
+ /** The service provider name for this network connection, or the default name */
+ val networkName: StateFlow<NetworkNameModel>
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt
index 98b47e4..b252de8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt
@@ -18,14 +18,18 @@
import android.content.Context
import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
+import android.telephony.TelephonyManager.DATA_ACTIVITY_NONE
import android.util.Log
import com.android.settingslib.SignalIcon
import com.android.settingslib.mobile.MobileMappings
import com.android.settingslib.mobile.TelephonyIcons
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.TableLogBufferFactory
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
@@ -34,6 +38,7 @@
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.Mobile
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled
+import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -57,6 +62,7 @@
private val dataSource: DemoModeMobileConnectionDataSource,
@Application private val scope: CoroutineScope,
context: Context,
+ private val logFactory: TableLogBufferFactory,
) : MobileConnectionsRepository {
private var demoCommandJob: Job? = null
@@ -146,7 +152,16 @@
override fun getRepoForSubId(subId: Int): DemoMobileConnectionRepository {
return connectionRepoCache[subId]
- ?: DemoMobileConnectionRepository(subId).also { connectionRepoCache[subId] = it }
+ ?: createDemoMobileConnectionRepo(subId).also { connectionRepoCache[subId] = it }
+ }
+
+ private fun createDemoMobileConnectionRepo(subId: Int): DemoMobileConnectionRepository {
+ val tableLogBuffer = logFactory.create("DemoMobileConnectionLog [$subId]", 100)
+
+ return DemoMobileConnectionRepository(
+ subId,
+ tableLogBuffer,
+ )
}
override val globalMobileDataSettingChangedEvent = MutableStateFlow(Unit)
@@ -185,6 +200,7 @@
// This is always true here, because we split out disabled states at the data-source level
connection.dataEnabled.value = true
connection.isDefaultDataSubscription.value = state.dataType != null
+ connection.networkName.value = NetworkNameModel.Derived(state.name)
connection.cdmaRoaming.value = state.roaming
connection.connectionInfo.value = state.toMobileConnectionModel()
@@ -236,7 +252,7 @@
primaryLevel = level ?: 0,
dataConnectionState =
DataConnectionState.Connected, // TODO(b/261029387): not yet supported
- dataActivityDirection = activity,
+ dataActivityDirection = (activity ?: DATA_ACTIVITY_NONE).toMobileDataActivityModel(),
carrierNetworkChangeActive = carrierNetworkChange,
resolvedNetworkType = dataType.toResolvedNetworkType()
)
@@ -256,7 +272,10 @@
}
}
-class DemoMobileConnectionRepository(override val subId: Int) : MobileConnectionRepository {
+class DemoMobileConnectionRepository(
+ override val subId: Int,
+ override val tableLogBuffer: TableLogBuffer,
+) : MobileConnectionRepository {
override val connectionInfo = MutableStateFlow(MobileConnectionModel())
override val dataEnabled = MutableStateFlow(true)
@@ -264,4 +283,6 @@
override val isDefaultDataSubscription = MutableStateFlow(true)
override val cdmaRoaming = MutableStateFlow(false)
+
+ override val networkName = MutableStateFlow(NetworkNameModel.Derived("demo network"))
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt
index 2cdbc19..d4ddb85 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt
@@ -24,10 +24,8 @@
import android.telephony.TelephonyManager.DATA_ACTIVITY_OUT
import com.android.settingslib.SignalIcon.MobileIconGroup
import com.android.settingslib.mobile.TelephonyIcons
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.demomode.DemoMode
import com.android.systemui.demomode.DemoMode.COMMAND_NETWORK
import com.android.systemui.demomode.DemoModeController
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
@@ -35,8 +33,6 @@
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
@@ -52,27 +48,7 @@
demoModeController: DemoModeController,
@Application scope: CoroutineScope,
) {
- private val demoCommandStream: Flow<Bundle> = conflatedCallbackFlow {
- val callback =
- object : DemoMode {
- override fun demoCommands(): List<String> = listOf(COMMAND_NETWORK)
-
- override fun dispatchDemoCommand(command: String, args: Bundle) {
- trySend(args)
- }
-
- override fun onDemoModeFinished() {
- // Handled elsewhere
- }
-
- override fun onDemoModeStarted() {
- // Handled elsewhere
- }
- }
-
- demoModeController.addCallback(callback)
- awaitClose { demoModeController.removeCallback(callback) }
- }
+ private val demoCommandStream = demoModeController.demoFlowForCommand(COMMAND_NETWORK)
// If the args contains "mobile", then all of the args are relevant. It's just the way demo mode
// commands work and it's a little silly
@@ -99,6 +75,7 @@
val activity = getString("activity")?.toActivity()
val carrierNetworkChange = getString("carriernetworkchange") == "show"
val roaming = getString("roam") == "show"
+ val name = getString("networkname") ?: "demo mode"
return Mobile(
level = level,
@@ -109,6 +86,7 @@
activity = activity,
carrierNetworkChange = carrierNetworkChange,
roaming = roaming,
+ name = name,
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/model/FakeNetworkEventModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/model/FakeNetworkEventModel.kt
index b8543ec..8b03f71 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/model/FakeNetworkEventModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/model/FakeNetworkEventModel.kt
@@ -35,6 +35,7 @@
@DataActivityType val activity: Int?,
val carrierNetworkChange: Boolean,
val roaming: Boolean,
+ val name: String,
) : FakeNetworkEventModel
data class MobileDisabled(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
index 295e0dc..0b9e158 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
@@ -17,30 +17,39 @@
package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod
import android.content.Context
+import android.content.IntentFilter
import android.database.ContentObserver
import android.provider.Settings.Global
import android.telephony.CellSignalStrength
import android.telephony.CellSignalStrengthCdma
import android.telephony.ServiceState
import android.telephony.SignalStrength
+import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
import android.telephony.TelephonyCallback
import android.telephony.TelephonyDisplayInfo
import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE
import android.telephony.TelephonyManager
import android.telephony.TelephonyManager.ERI_OFF
+import android.telephony.TelephonyManager.EXTRA_SUBSCRIPTION_ID
import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
+import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.TableLogBufferFactory
+import com.android.systemui.log.table.logDiffsForTable
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.OverrideNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.UnknownNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.model.toDataConnectionType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.toNetworkNameModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
+import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel
import com.android.systemui.util.settings.GlobalSettings
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
@@ -52,6 +61,7 @@
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
@@ -62,13 +72,17 @@
class MobileConnectionRepositoryImpl(
private val context: Context,
override val subId: Int,
+ defaultNetworkName: NetworkNameModel,
+ networkNameSeparator: String,
private val telephonyManager: TelephonyManager,
private val globalSettings: GlobalSettings,
+ broadcastDispatcher: BroadcastDispatcher,
defaultDataSubId: StateFlow<Int>,
globalMobileDataSettingChangedEvent: Flow<Unit>,
mobileMappingsProxy: MobileMappingsProxy,
bgDispatcher: CoroutineDispatcher,
logger: ConnectivityPipelineLogger,
+ mobileLogger: TableLogBuffer,
scope: CoroutineScope,
) : MobileConnectionRepository {
init {
@@ -82,10 +96,11 @@
private val telephonyCallbackEvent = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
+ override val tableLogBuffer: TableLogBuffer = mobileLogger
+
override val connectionInfo: StateFlow<MobileConnectionModel> = run {
var state = MobileConnectionModel()
conflatedCallbackFlow {
- // TODO (b/240569788): log all of these into the connectivity logger
val callback =
object :
TelephonyCallback(),
@@ -96,15 +111,18 @@
TelephonyCallback.CarrierNetworkListener,
TelephonyCallback.DisplayInfoListener {
override fun onServiceStateChanged(serviceState: ServiceState) {
+ logger.logOnServiceStateChanged(serviceState, subId)
state =
state.copy(
isEmergencyOnly = serviceState.isEmergencyOnly,
isRoaming = serviceState.roaming,
+ operatorAlphaShort = serviceState.operatorAlphaShort,
)
trySend(state)
}
override fun onSignalStrengthsChanged(signalStrength: SignalStrength) {
+ logger.logOnSignalStrengthsChanged(signalStrength, subId)
val cdmaLevel =
signalStrength
.getCellSignalStrengths(CellSignalStrengthCdma::class.java)
@@ -131,17 +149,23 @@
dataState: Int,
networkType: Int
) {
+ logger.logOnDataConnectionStateChanged(dataState, networkType, subId)
state =
state.copy(dataConnectionState = dataState.toDataConnectionType())
trySend(state)
}
override fun onDataActivity(direction: Int) {
- state = state.copy(dataActivityDirection = direction)
+ logger.logOnDataActivity(direction, subId)
+ state =
+ state.copy(
+ dataActivityDirection = direction.toMobileDataActivityModel()
+ )
trySend(state)
}
override fun onCarrierNetworkChange(active: Boolean) {
+ logger.logOnCarrierNetworkChange(active, subId)
state = state.copy(carrierNetworkChangeActive = active)
trySend(state)
}
@@ -149,6 +173,7 @@
override fun onDisplayInfoChanged(
telephonyDisplayInfo: TelephonyDisplayInfo
) {
+ logger.logOnDisplayInfoChanged(telephonyDisplayInfo, subId)
val networkType =
if (telephonyDisplayInfo.networkType == NETWORK_TYPE_UNKNOWN) {
@@ -179,7 +204,11 @@
awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
}
.onEach { telephonyCallbackEvent.tryEmit(Unit) }
- .logOutputChange(logger, "MobileSubscriptionModel")
+ .logDiffsForTable(
+ mobileLogger,
+ columnPrefix = "MobileConnection ($subId)",
+ initialValue = state,
+ )
.stateIn(scope, SharingStarted.WhileSubscribed(), state)
}
@@ -218,46 +247,97 @@
.mapLatest { telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber != ERI_OFF }
.stateIn(scope, SharingStarted.WhileSubscribed(), false)
- override val dataEnabled: StateFlow<Boolean> =
+ override val networkName: StateFlow<NetworkNameModel> =
+ broadcastDispatcher
+ .broadcastFlow(IntentFilter(TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED)) {
+ intent,
+ _ ->
+ if (intent.getIntExtra(EXTRA_SUBSCRIPTION_ID, INVALID_SUBSCRIPTION_ID) != subId) {
+ defaultNetworkName
+ } else {
+ intent.toNetworkNameModel(networkNameSeparator) ?: defaultNetworkName
+ }
+ }
+ .distinctUntilChanged()
+ .logDiffsForTable(
+ mobileLogger,
+ columnPrefix = "",
+ initialValue = defaultNetworkName,
+ )
+ .stateIn(scope, SharingStarted.WhileSubscribed(), defaultNetworkName)
+
+ override val dataEnabled: StateFlow<Boolean> = run {
+ val initial = dataConnectionAllowed()
telephonyPollingEvent
.mapLatest { dataConnectionAllowed() }
- .stateIn(scope, SharingStarted.WhileSubscribed(), dataConnectionAllowed())
+ .distinctUntilChanged()
+ .logDiffsForTable(
+ mobileLogger,
+ columnPrefix = "",
+ columnName = "dataEnabled",
+ initialValue = initial,
+ )
+ .stateIn(scope, SharingStarted.WhileSubscribed(), initial)
+ }
private fun dataConnectionAllowed(): Boolean = telephonyManager.isDataConnectionAllowed
- override val isDefaultDataSubscription: StateFlow<Boolean> =
+ override val isDefaultDataSubscription: StateFlow<Boolean> = run {
+ val initialValue = defaultDataSubId.value == subId
defaultDataSubId
.mapLatest { it == subId }
- .stateIn(scope, SharingStarted.WhileSubscribed(), defaultDataSubId.value == subId)
+ .distinctUntilChanged()
+ .logDiffsForTable(
+ mobileLogger,
+ columnPrefix = "",
+ columnName = "isDefaultDataSub",
+ initialValue = initialValue,
+ )
+ .stateIn(scope, SharingStarted.WhileSubscribed(), initialValue)
+ }
class Factory
@Inject
constructor(
+ private val broadcastDispatcher: BroadcastDispatcher,
private val context: Context,
private val telephonyManager: TelephonyManager,
private val logger: ConnectivityPipelineLogger,
private val globalSettings: GlobalSettings,
private val mobileMappingsProxy: MobileMappingsProxy,
+ private val logFactory: TableLogBufferFactory,
@Background private val bgDispatcher: CoroutineDispatcher,
@Application private val scope: CoroutineScope,
) {
fun build(
subId: Int,
+ defaultNetworkName: NetworkNameModel,
+ networkNameSeparator: String,
defaultDataSubId: StateFlow<Int>,
globalMobileDataSettingChangedEvent: Flow<Unit>,
): MobileConnectionRepository {
+ val mobileLogger = logFactory.create(tableBufferLogName(subId), 100)
+
return MobileConnectionRepositoryImpl(
context,
subId,
+ defaultNetworkName,
+ networkNameSeparator,
telephonyManager.createForSubscriptionId(subId),
globalSettings,
+ broadcastDispatcher,
defaultDataSubId,
globalMobileDataSettingChangedEvent,
mobileMappingsProxy,
bgDispatcher,
logger,
+ mobileLogger,
scope,
)
}
}
+
+ companion object {
+ fun tableBufferLogName(subId: Int): String = "MobileConnectionLog [$subId]"
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
index 483df47..d407abe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
@@ -38,17 +38,20 @@
import com.android.internal.telephony.PhoneConstants
import com.android.settingslib.SignalIcon.MobileIconGroup
import com.android.settingslib.mobile.MobileMappings.Config
+import com.android.systemui.R
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange
import com.android.systemui.util.settings.GlobalSettings
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
@@ -88,6 +91,14 @@
) : MobileConnectionsRepository {
private var subIdRepositoryCache: MutableMap<Int, MobileConnectionRepository> = mutableMapOf()
+ private val defaultNetworkName =
+ NetworkNameModel.Default(
+ context.getString(com.android.internal.R.string.lockscreen_carrier_default)
+ )
+
+ private val networkNameSeparator: String =
+ context.getString(R.string.status_bar_network_name_separator)
+
/**
* State flow that emits the set of mobile data subscriptions, each represented by its own
* [SubscriptionInfo]. We probably only need the [SubscriptionInfo.getSubscriptionId] of each
@@ -110,6 +121,7 @@
awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) }
}
.mapLatest { fetchSubscriptionsList().map { it.toSubscriptionModel() } }
+ .logInputChange(logger, "onSubscriptionsChanged")
.onEach { infos -> dropUnusedReposFromCache(infos) }
.stateIn(scope, started = SharingStarted.WhileSubscribed(), listOf())
@@ -126,6 +138,8 @@
telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
}
+ .distinctUntilChanged()
+ .logInputChange(logger, "onActiveDataSubscriptionIdChanged")
.stateIn(scope, started = SharingStarted.WhileSubscribed(), INVALID_SUBSCRIPTION_ID)
private val defaultDataSubIdChangeEvent: MutableSharedFlow<Unit> =
@@ -139,6 +153,7 @@
intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY, INVALID_SUBSCRIPTION_ID)
}
.distinctUntilChanged()
+ .logInputChange(logger, "ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED")
.onEach { defaultDataSubIdChangeEvent.tryEmit(Unit) }
.stateIn(
scope,
@@ -147,13 +162,15 @@
)
private val carrierConfigChangedEvent =
- broadcastDispatcher.broadcastFlow(
- IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)
- )
+ broadcastDispatcher
+ .broadcastFlow(IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED))
+ .logInputChange(logger, "ACTION_CARRIER_CONFIG_CHANGED")
override val defaultDataSubRatConfig: StateFlow<Config> =
merge(defaultDataSubIdChangeEvent, carrierConfigChangedEvent)
.mapLatest { Config.readConfig(context) }
+ .distinctUntilChanged()
+ .logInputChange(logger, "defaultDataSubRatConfig")
.stateIn(
scope,
SharingStarted.WhileSubscribed(),
@@ -161,10 +178,16 @@
)
override val defaultMobileIconMapping: Flow<Map<String, MobileIconGroup>> =
- defaultDataSubRatConfig.map { mobileMappingsProxy.mapIconSets(it) }
+ defaultDataSubRatConfig
+ .map { mobileMappingsProxy.mapIconSets(it) }
+ .distinctUntilChanged()
+ .logInputChange(logger, "defaultMobileIconMapping")
override val defaultMobileIconGroup: Flow<MobileIconGroup> =
- defaultDataSubRatConfig.map { mobileMappingsProxy.getDefaultIcons(it) }
+ defaultDataSubRatConfig
+ .map { mobileMappingsProxy.getDefaultIcons(it) }
+ .distinctUntilChanged()
+ .logInputChange(logger, "defaultMobileIconGroup")
override fun getRepoForSubId(subId: Int): MobileConnectionRepository {
if (!isValidSubId(subId)) {
@@ -181,22 +204,24 @@
* In single-SIM devices, the [MOBILE_DATA] setting is phone-wide. For multi-SIM, the individual
* connection repositories also observe the URI for [MOBILE_DATA] + subId.
*/
- override val globalMobileDataSettingChangedEvent: Flow<Unit> = conflatedCallbackFlow {
- val observer =
- object : ContentObserver(null) {
- override fun onChange(selfChange: Boolean) {
- trySend(Unit)
- }
+ override val globalMobileDataSettingChangedEvent: Flow<Unit> =
+ conflatedCallbackFlow {
+ val observer =
+ object : ContentObserver(null) {
+ override fun onChange(selfChange: Boolean) {
+ trySend(Unit)
+ }
+ }
+
+ globalSettings.registerContentObserver(
+ globalSettings.getUriFor(MOBILE_DATA),
+ true,
+ observer
+ )
+
+ awaitClose { context.contentResolver.unregisterContentObserver(observer) }
}
-
- globalSettings.registerContentObserver(
- globalSettings.getUriFor(MOBILE_DATA),
- true,
- observer
- )
-
- awaitClose { context.contentResolver.unregisterContentObserver(observer) }
- }
+ .logInputChange(logger, "globalMobileDataSettingChangedEvent")
@SuppressLint("MissingPermission")
override val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> =
@@ -226,6 +251,8 @@
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
}
+ .distinctUntilChanged()
+ .logInputChange(logger, "defaultMobileNetworkConnectivity")
.stateIn(scope, SharingStarted.WhileSubscribed(), MobileConnectivityModel())
private fun isValidSubId(subId: Int): Boolean {
@@ -243,6 +270,8 @@
private fun createRepositoryForSubId(subId: Int): MobileConnectionRepository {
return mobileConnectionRepositoryFactory.build(
subId,
+ defaultNetworkName,
+ networkNameSeparator,
defaultDataSubId,
globalMobileDataSettingChangedEvent,
)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
index 15b70f9..e6686dc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
@@ -19,11 +19,15 @@
import android.telephony.CarrierConfigManager
import com.android.settingslib.SignalIcon.MobileIconGroup
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Connected
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
import com.android.systemui.util.CarrierConfigTracker
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@@ -32,6 +36,12 @@
import kotlinx.coroutines.flow.stateIn
interface MobileIconInteractor {
+ /** The table log created for this connection */
+ val tableLogBuffer: TableLogBuffer
+
+ /** The current mobile data activity */
+ val activity: Flow<DataActivityModel>
+
/** Only true if mobile is the default transport but is not validated, otherwise false */
val isDefaultConnectionFailed: StateFlow<Boolean>
@@ -51,6 +61,15 @@
/** Observable for RAT type (network type) indicator */
val networkTypeIconGroup: StateFlow<MobileIconGroup>
+ /**
+ * Provider name for this network connection. The name can be one of 3 values:
+ * 1. The default network name, if one is configured
+ * 2. A derived name based off of the intent [ACTION_SERVICE_PROVIDERS_UPDATED]
+ * 3. Or, in the case where the repository sends us the default network name, we check for an
+ * override in [connectionInfo.operatorAlphaShort], a value that is derived from [ServiceState]
+ */
+ val networkName: StateFlow<NetworkNameModel>
+
/** True if this line of service is emergency-only */
val isEmergencyOnly: StateFlow<Boolean>
@@ -82,10 +101,30 @@
) : MobileIconInteractor {
private val connectionInfo = connectionRepository.connectionInfo
+ override val tableLogBuffer: TableLogBuffer = connectionRepository.tableLogBuffer
+
+ override val activity = connectionInfo.mapLatest { it.dataActivityDirection }
+
override val isDataEnabled: StateFlow<Boolean> = connectionRepository.dataEnabled
override val isDefaultDataEnabled = defaultSubscriptionHasDataEnabled
+ override val networkName =
+ combine(connectionInfo, connectionRepository.networkName) { connection, networkName ->
+ if (
+ networkName is NetworkNameModel.Default && connection.operatorAlphaShort != null
+ ) {
+ NetworkNameModel.Derived(connection.operatorAlphaShort)
+ } else {
+ networkName
+ }
+ }
+ .stateIn(
+ scope,
+ SharingStarted.WhileSubscribed(),
+ connectionRepository.networkName.value
+ )
+
/** Observable for the current RAT indicator icon ([MobileIconGroup]) */
override val networkTypeIconGroup: StateFlow<MobileIconGroup> =
combine(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt
index 62fa723..829a5ca 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt
@@ -20,7 +20,6 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.statusbar.phone.StatusBarIconController
-import com.android.systemui.statusbar.phone.StatusBarIconController.IconManager
import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel
@@ -70,6 +69,9 @@
private val mobileSubIdsState: StateFlow<List<Int>> =
mobileSubIds.stateIn(scope, SharingStarted.WhileSubscribed(), listOf())
+ /** In order to keep the logs tame, we will reuse the same top-level mobile icons view model */
+ val mobileIconsViewModel = iconsViewModelFactory.create(mobileSubIdsState)
+
override fun start() {
// Only notify the icon controller if we want to *render* the new icons.
// Note that this flow may still run if
@@ -81,12 +83,4 @@
}
}
}
-
- /**
- * Create a MobileIconsViewModel for a given [IconManager], and bind it to to the manager's
- * lifecycle. This will start collecting on [mobileSubIdsState] and link our new pipeline with
- * the old view system.
- */
- fun createMobileIconsViewModel(): MobileIconsViewModel =
- iconsViewModelFactory.create(mobileSubIdsState)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
index 4455801..ab442b5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
@@ -17,6 +17,7 @@
package com.android.systemui.statusbar.pipeline.mobile.ui.binder
import android.content.res.ColorStateList
+import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
@@ -29,7 +30,7 @@
import com.android.systemui.R
import com.android.systemui.common.ui.binder.IconViewBinder
import com.android.systemui.lifecycle.repeatWhenAttached
-import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
@@ -38,8 +39,11 @@
@JvmStatic
fun bind(
view: ViewGroup,
- viewModel: MobileIconViewModel,
+ viewModel: LocationBasedMobileViewModel,
) {
+ val activityContainer = view.requireViewById<View>(R.id.inout_container)
+ val activityIn = view.requireViewById<ImageView>(R.id.mobile_in)
+ val activityOut = view.requireViewById<ImageView>(R.id.mobile_out)
val networkTypeView = view.requireViewById<ImageView>(R.id.mobile_type)
val iconView = view.requireViewById<ImageView>(R.id.mobile_signal)
val mobileDrawable = SignalDrawable(view.context).also { iconView.setImageDrawable(it) }
@@ -74,6 +78,15 @@
}
}
+ // Set the activity indicators
+ launch { viewModel.activityInVisible.collect { activityIn.isVisible = it } }
+
+ launch { viewModel.activityOutVisible.collect { activityOut.isVisible = it } }
+
+ launch {
+ viewModel.activityContainerVisible.collect { activityContainer.isVisible = it }
+ }
+
// Set the tint
launch {
viewModel.tint.collect { tint ->
@@ -81,6 +94,8 @@
iconView.imageTintList = tintList
networkTypeView.imageTintList = tintList
roamingView.imageTintList = tintList
+ activityIn.imageTintList = tintList
+ activityOut.imageTintList = tintList
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt
index 0ab7bcd..e86fee2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt
@@ -24,7 +24,7 @@
import com.android.systemui.statusbar.BaseStatusBarFrameLayout
import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON
import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconBinder
-import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel
import java.util.ArrayList
class ModernStatusBarMobileView(
@@ -71,7 +71,7 @@
fun constructAndBind(
context: Context,
slot: String,
- viewModel: MobileIconViewModel,
+ viewModel: LocationBasedMobileViewModel,
): ModernStatusBarMobileView {
return (LayoutInflater.from(context)
.inflate(R.layout.status_bar_mobile_signal_group_new, null)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt
new file mode 100644
index 0000000..b0dc41f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel
+
+import android.graphics.Color
+import com.android.systemui.statusbar.phone.StatusBarLocation
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
+
+/**
+ * A view model for an individual mobile icon that embeds the notion of a [StatusBarLocation]. This
+ * allows the mobile icon to change some view parameters at different locations
+ *
+ * @param commonImpl for convenience, this class wraps a base interface that can provides all of the
+ * common implementations between locations. See [MobileIconViewModel]
+ */
+abstract class LocationBasedMobileViewModel(
+ val commonImpl: MobileIconViewModelCommon,
+ val logger: ConnectivityPipelineLogger,
+) : MobileIconViewModelCommon by commonImpl {
+ abstract val tint: Flow<Int>
+
+ companion object {
+ fun viewModelForLocation(
+ commonImpl: MobileIconViewModelCommon,
+ logger: ConnectivityPipelineLogger,
+ loc: StatusBarLocation,
+ ): LocationBasedMobileViewModel =
+ when (loc) {
+ StatusBarLocation.HOME -> HomeMobileIconViewModel(commonImpl, logger)
+ StatusBarLocation.KEYGUARD -> KeyguardMobileIconViewModel(commonImpl, logger)
+ StatusBarLocation.QS -> QsMobileIconViewModel(commonImpl, logger)
+ }
+ }
+}
+
+class HomeMobileIconViewModel(
+ commonImpl: MobileIconViewModelCommon,
+ logger: ConnectivityPipelineLogger,
+) : MobileIconViewModelCommon, LocationBasedMobileViewModel(commonImpl, logger) {
+ override val tint: Flow<Int> =
+ flowOf(Color.CYAN)
+ .distinctUntilChanged()
+ .logOutputChange(logger, "HOME tint(${commonImpl.subscriptionId})")
+}
+
+class QsMobileIconViewModel(
+ commonImpl: MobileIconViewModelCommon,
+ logger: ConnectivityPipelineLogger,
+) : MobileIconViewModelCommon, LocationBasedMobileViewModel(commonImpl, logger) {
+ override val tint: Flow<Int> =
+ flowOf(Color.GREEN)
+ .distinctUntilChanged()
+ .logOutputChange(logger, "QS tint(${commonImpl.subscriptionId})")
+}
+
+class KeyguardMobileIconViewModel(
+ commonImpl: MobileIconViewModelCommon,
+ logger: ConnectivityPipelineLogger,
+) : MobileIconViewModelCommon, LocationBasedMobileViewModel(commonImpl, logger) {
+ override val tint: Flow<Int> =
+ flowOf(Color.MAGENTA)
+ .distinctUntilChanged()
+ .logOutputChange(logger, "KEYGUARD tint(${commonImpl.subscriptionId})")
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
index f4d6111..2d6ac4e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
@@ -16,20 +16,40 @@
package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel
-import android.graphics.Color
import com.android.settingslib.graph.SignalDrawable
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.log.table.logDiffsForTable
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractor
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+
+/** Common interface for all of the location-based mobile icon view models. */
+interface MobileIconViewModelCommon {
+ val subscriptionId: Int
+ /** An int consumable by [SignalDrawable] for display */
+ val iconId: Flow<Int>
+ val roaming: Flow<Boolean>
+ /** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */
+ val networkTypeIcon: Flow<Icon?>
+ val activityInVisible: Flow<Boolean>
+ val activityOutVisible: Flow<Boolean>
+ val activityContainerVisible: Flow<Boolean>
+}
/**
* View model for the state of a single mobile icon. Each [MobileIconViewModel] will keep watch over
@@ -37,24 +57,29 @@
* subscription's information.
*
* There will be exactly one [MobileIconViewModel] per filtered subscription offered from
- * [MobileIconsInteractor.filteredSubscriptions]
+ * [MobileIconsInteractor.filteredSubscriptions].
*
- * TODO: figure out where carrier merged and VCN models go (probably here?)
+ * For the sake of keeping log spam in check, every flow funding the [MobileIconViewModelCommon]
+ * interface is implemented as a [StateFlow]. This ensures that each location-based mobile icon view
+ * model gets the exact same information, as well as allows us to log that unified state only once
+ * per icon.
*/
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@OptIn(ExperimentalCoroutinesApi::class)
class MobileIconViewModel
constructor(
- val subscriptionId: Int,
+ override val subscriptionId: Int,
iconInteractor: MobileIconInteractor,
logger: ConnectivityPipelineLogger,
-) {
+ constants: ConnectivityConstants,
+ scope: CoroutineScope,
+) : MobileIconViewModelCommon {
/** Whether or not to show the error state of [SignalDrawable] */
private val showExclamationMark: Flow<Boolean> =
iconInteractor.isDefaultDataEnabled.mapLatest { !it }
- /** An int consumable by [SignalDrawable] for display */
- val iconId: Flow<Int> =
+ override val iconId: Flow<Int> = run {
+ val initial = SignalDrawable.getEmptyState(iconInteractor.numberOfLevels.value)
combine(iconInteractor.level, iconInteractor.numberOfLevels, showExclamationMark) {
level,
numberOfLevels,
@@ -62,32 +87,97 @@
SignalDrawable.getState(level, numberOfLevels, showExclamationMark)
}
.distinctUntilChanged()
- .logOutputChange(logger, "iconId($subscriptionId)")
+ .logDiffsForTable(
+ iconInteractor.tableLogBuffer,
+ columnPrefix = "",
+ columnName = "iconId",
+ initialValue = initial,
+ )
+ .stateIn(scope, SharingStarted.WhileSubscribed(), initial)
+ }
- /** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */
- val networkTypeIcon: Flow<Icon?> =
+ override val networkTypeIcon: Flow<Icon?> =
combine(
- iconInteractor.networkTypeIconGroup,
- iconInteractor.isDataConnected,
- iconInteractor.isDataEnabled,
- iconInteractor.isDefaultConnectionFailed,
- iconInteractor.alwaysShowDataRatIcon,
- ) { networkTypeIconGroup, dataConnected, dataEnabled, failedConnection, alwaysShow ->
- val desc =
- if (networkTypeIconGroup.dataContentDescription != 0)
- ContentDescription.Resource(networkTypeIconGroup.dataContentDescription)
- else null
- val icon = Icon.Resource(networkTypeIconGroup.dataType, desc)
- return@combine when {
- alwaysShow -> icon
- !dataConnected -> null
- !dataEnabled -> null
- failedConnection -> null
- else -> icon
+ iconInteractor.networkTypeIconGroup,
+ iconInteractor.isDataConnected,
+ iconInteractor.isDataEnabled,
+ iconInteractor.isDefaultConnectionFailed,
+ iconInteractor.alwaysShowDataRatIcon,
+ ) { networkTypeIconGroup, dataConnected, dataEnabled, failedConnection, alwaysShow ->
+ val desc =
+ if (networkTypeIconGroup.dataContentDescription != 0)
+ ContentDescription.Resource(networkTypeIconGroup.dataContentDescription)
+ else null
+ val icon = Icon.Resource(networkTypeIconGroup.dataType, desc)
+ return@combine when {
+ alwaysShow -> icon
+ !dataConnected -> null
+ !dataEnabled -> null
+ failedConnection -> null
+ else -> icon
+ }
}
+ .distinctUntilChanged()
+ .onEach {
+ // This is done as an onEach side effect since Icon is not Diffable (yet)
+ iconInteractor.tableLogBuffer.logChange(
+ prefix = "",
+ columnName = "networkTypeIcon",
+ value = it.toString(),
+ )
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), null)
+
+ override val roaming: StateFlow<Boolean> =
+ iconInteractor.isRoaming
+ .logDiffsForTable(
+ iconInteractor.tableLogBuffer,
+ columnPrefix = "",
+ columnName = "roaming",
+ initialValue = false,
+ )
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ private val activity: Flow<DataActivityModel?> =
+ if (!constants.shouldShowActivityConfig) {
+ flowOf(null)
+ } else {
+ iconInteractor.activity
}
- val roaming: Flow<Boolean> = iconInteractor.isRoaming
+ override val activityInVisible: Flow<Boolean> =
+ activity
+ .map { it?.hasActivityIn ?: false }
+ .distinctUntilChanged()
+ .logDiffsForTable(
+ iconInteractor.tableLogBuffer,
+ columnPrefix = "",
+ columnName = "activityInVisible",
+ initialValue = false,
+ )
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
- val tint: Flow<Int> = flowOf(Color.CYAN)
+ override val activityOutVisible: Flow<Boolean> =
+ activity
+ .map { it?.hasActivityOut ?: false }
+ .distinctUntilChanged()
+ .logDiffsForTable(
+ iconInteractor.tableLogBuffer,
+ columnPrefix = "",
+ columnName = "activityOutVisible",
+ initialValue = false,
+ )
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ override val activityContainerVisible: Flow<Boolean> =
+ activity
+ .map { it != null && (it.hasActivityIn || it.hasActivityOut) }
+ .distinctUntilChanged()
+ .logDiffsForTable(
+ iconInteractor.tableLogBuffer,
+ columnPrefix = "",
+ columnName = "activityContainerVisible",
+ initialValue = false,
+ )
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt
index 2349cb7..b9318b1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt
@@ -14,16 +14,19 @@
* limitations under the License.
*/
-@file:OptIn(InternalCoroutinesApi::class)
-
package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.statusbar.phone.StatusBarLocation
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import javax.inject.Inject
-import kotlinx.coroutines.InternalCoroutinesApi
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
/**
* View model for describing the system's current mobile cellular connections. The result is a list
@@ -36,26 +39,50 @@
val subscriptionIdsFlow: StateFlow<List<Int>>,
private val interactor: MobileIconsInteractor,
private val logger: ConnectivityPipelineLogger,
+ private val constants: ConnectivityConstants,
+ @Application private val scope: CoroutineScope,
) {
- /** TODO: do we need to cache these? */
- fun viewModelForSub(subId: Int): MobileIconViewModel =
- MobileIconViewModel(
- subId,
- interactor.createMobileConnectionInteractorForSubId(subId),
- logger
- )
+ @VisibleForTesting val mobileIconSubIdCache = mutableMapOf<Int, MobileIconViewModel>()
+
+ init {
+ scope.launch { subscriptionIdsFlow.collect { removeInvalidModelsFromCache(it) } }
+ }
+
+ fun viewModelForSub(subId: Int, location: StatusBarLocation): LocationBasedMobileViewModel {
+ val common =
+ mobileIconSubIdCache[subId]
+ ?: MobileIconViewModel(
+ subId,
+ interactor.createMobileConnectionInteractorForSubId(subId),
+ logger,
+ constants,
+ scope,
+ )
+ .also { mobileIconSubIdCache[subId] = it }
+
+ return LocationBasedMobileViewModel.viewModelForLocation(common, logger, location)
+ }
+
+ private fun removeInvalidModelsFromCache(subIds: List<Int>) {
+ val subIdsToRemove = mobileIconSubIdCache.keys.filter { !subIds.contains(it) }
+ subIdsToRemove.forEach { mobileIconSubIdCache.remove(it) }
+ }
class Factory
@Inject
constructor(
private val interactor: MobileIconsInteractor,
private val logger: ConnectivityPipelineLogger,
+ private val constants: ConnectivityConstants,
+ @Application private val scope: CoroutineScope,
) {
fun create(subscriptionIdsFlow: StateFlow<List<Int>>): MobileIconsViewModel {
return MobileIconsViewModel(
subscriptionIdsFlow,
interactor,
logger,
+ constants,
+ scope,
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityConstants.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityConstants.kt
index 6efb10f..0c9b86c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityConstants.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityConstants.kt
@@ -16,8 +16,10 @@
package com.android.systemui.statusbar.pipeline.shared
+import android.content.Context
import android.telephony.TelephonyManager
import com.android.systemui.Dumpable
+import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dump.DumpManager
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.SB_LOGGING_TAG
@@ -32,15 +34,25 @@
@SysUISingleton
class ConnectivityConstants
@Inject
-constructor(dumpManager: DumpManager, telephonyManager: TelephonyManager) : Dumpable {
+constructor(
+ context: Context,
+ dumpManager: DumpManager,
+ telephonyManager: TelephonyManager,
+) : Dumpable {
init {
- dumpManager.registerDumpable("${SB_LOGGING_TAG}Constants", this)
+ dumpManager.registerNormalDumpable("${SB_LOGGING_TAG}Constants", this)
}
/** True if this device has the capability for data connections and false otherwise. */
val hasDataCapabilities = telephonyManager.isDataCapable
+ /** True if we should show the activityIn/activityOut icons and false otherwise */
+ val shouldShowActivityConfig = context.resources.getBoolean(R.bool.config_showActivity)
+
override fun dump(pw: PrintWriter, args: Array<out String>) {
- pw.apply { println("hasDataCapabilities=$hasDataCapabilities") }
+ pw.apply {
+ println("hasDataCapabilities=$hasDataCapabilities")
+ println("shouldShowActivityConfig=$shouldShowActivityConfig")
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
index d3cf32f..d3ff357 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
@@ -18,8 +18,11 @@
import android.net.Network
import android.net.NetworkCapabilities
-import com.android.systemui.log.dagger.StatusBarConnectivityLog
+import android.telephony.ServiceState
+import android.telephony.SignalStrength
+import android.telephony.TelephonyDisplayInfo
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.dagger.StatusBarConnectivityLog
import com.android.systemui.plugins.log.LogBuffer
import com.android.systemui.plugins.log.LogLevel
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.toString
@@ -28,7 +31,9 @@
import kotlinx.coroutines.flow.onEach
@SysUISingleton
-class ConnectivityPipelineLogger @Inject constructor(
+class ConnectivityPipelineLogger
+@Inject
+constructor(
@StatusBarConnectivityLog private val buffer: LogBuffer,
) {
/**
@@ -37,34 +42,23 @@
* Use this method for inputs that don't have any extra information besides their callback name.
*/
fun logInputChange(callbackName: String) {
+ buffer.log(SB_LOGGING_TAG, LogLevel.INFO, { str1 = callbackName }, { "Input: $str1" })
+ }
+
+ /** Logs a change in one of the **raw inputs** to the connectivity pipeline. */
+ fun logInputChange(callbackName: String, changeInfo: String?) {
buffer.log(
SB_LOGGING_TAG,
LogLevel.INFO,
- { str1 = callbackName },
- { "Input: $str1" }
+ {
+ str1 = callbackName
+ str2 = changeInfo
+ },
+ { "Input: $str1: $str2" }
)
}
- /**
- * Logs a change in one of the **raw inputs** to the connectivity pipeline.
- */
- fun logInputChange(callbackName: String, changeInfo: String?) {
- buffer.log(
- SB_LOGGING_TAG,
- LogLevel.INFO,
- {
- str1 = callbackName
- str2 = changeInfo
- },
- {
- "Input: $str1: $str2"
- }
- )
- }
-
- /**
- * Logs a **data transformation** that we performed within the connectivity pipeline.
- */
+ /** Logs a **data transformation** that we performed within the connectivity pipeline. */
fun logTransformation(transformationName: String, oldValue: Any?, newValue: Any?) {
if (oldValue == newValue) {
buffer.log(
@@ -74,9 +68,7 @@
str1 = transformationName
str2 = oldValue.toString()
},
- {
- "Transform: $str1: $str2 (transformation didn't change it)"
- }
+ { "Transform: $str1: $str2 (transformation didn't change it)" }
)
} else {
buffer.log(
@@ -87,27 +79,21 @@
str2 = oldValue.toString()
str3 = newValue.toString()
},
- {
- "Transform: $str1: $str2 -> $str3"
- }
+ { "Transform: $str1: $str2 -> $str3" }
)
}
}
- /**
- * Logs a change in one of the **outputs** to the connectivity pipeline.
- */
+ /** Logs a change in one of the **outputs** to the connectivity pipeline. */
fun logOutputChange(outputParamName: String, changeInfo: String) {
buffer.log(
- SB_LOGGING_TAG,
- LogLevel.INFO,
- {
- str1 = outputParamName
- str2 = changeInfo
- },
- {
- "Output: $str1: $str2"
- }
+ SB_LOGGING_TAG,
+ LogLevel.INFO,
+ {
+ str1 = outputParamName
+ str2 = changeInfo
+ },
+ { "Output: $str1: $str2" }
)
}
@@ -119,9 +105,7 @@
int1 = network.getNetId()
str1 = networkCapabilities.toString()
},
- {
- "onCapabilitiesChanged: net=$int1 capabilities=$str1"
- }
+ { "onCapabilitiesChanged: net=$int1 capabilities=$str1" }
)
}
@@ -129,21 +113,93 @@
buffer.log(
SB_LOGGING_TAG,
LogLevel.INFO,
+ { int1 = network.getNetId() },
+ { "onLost: net=$int1" }
+ )
+ }
+
+ fun logOnServiceStateChanged(serviceState: ServiceState, subId: Int) {
+ buffer.log(
+ SB_LOGGING_TAG,
+ LogLevel.INFO,
{
- int1 = network.getNetId()
+ int1 = subId
+ bool1 = serviceState.isEmergencyOnly
+ bool2 = serviceState.roaming
+ str1 = serviceState.operatorAlphaShort
},
{
- "onLost: net=$int1"
+ "onServiceStateChanged: subId=$int1 emergencyOnly=$bool1 roaming=$bool2" +
+ " operator=$str1"
}
)
}
+ fun logOnSignalStrengthsChanged(signalStrength: SignalStrength, subId: Int) {
+ buffer.log(
+ SB_LOGGING_TAG,
+ LogLevel.INFO,
+ {
+ int1 = subId
+ str1 = signalStrength.toString()
+ },
+ { "onSignalStrengthsChanged: subId=$int1 strengths=$str1" }
+ )
+ }
+
+ fun logOnDataConnectionStateChanged(dataState: Int, networkType: Int, subId: Int) {
+ buffer.log(
+ SB_LOGGING_TAG,
+ LogLevel.INFO,
+ {
+ int1 = subId
+ int2 = dataState
+ str1 = networkType.toString()
+ },
+ { "onDataConnectionStateChanged: subId=$int1 dataState=$int2 networkType=$str1" },
+ )
+ }
+
+ fun logOnDataActivity(direction: Int, subId: Int) {
+ buffer.log(
+ SB_LOGGING_TAG,
+ LogLevel.INFO,
+ {
+ int1 = subId
+ int2 = direction
+ },
+ { "onDataActivity: subId=$int1 direction=$int2" },
+ )
+ }
+
+ fun logOnCarrierNetworkChange(active: Boolean, subId: Int) {
+ buffer.log(
+ SB_LOGGING_TAG,
+ LogLevel.INFO,
+ {
+ int1 = subId
+ bool1 = active
+ },
+ { "onCarrierNetworkChange: subId=$int1 active=$bool1" },
+ )
+ }
+
+ fun logOnDisplayInfoChanged(displayInfo: TelephonyDisplayInfo, subId: Int) {
+ buffer.log(
+ SB_LOGGING_TAG,
+ LogLevel.INFO,
+ {
+ int1 = subId
+ str1 = displayInfo.toString()
+ },
+ { "onDisplayInfoChanged: subId=$int1 displayInfo=$str1" },
+ )
+ }
+
companion object {
const val SB_LOGGING_TAG = "SbConnectivity"
- /**
- * Log a change in one of the **inputs** to the connectivity pipeline.
- */
+ /** Log a change in one of the **inputs** to the connectivity pipeline. */
fun Flow<Unit>.logInputChange(
logger: ConnectivityPipelineLogger,
inputParamName: String,
@@ -155,26 +211,26 @@
* Log a change in one of the **inputs** to the connectivity pipeline.
*
* @param prettyPrint an optional function to transform the value into a readable string.
- * [toString] is used if no custom function is provided.
+ * [toString] is used if no custom function is provided.
*/
fun <T> Flow<T>.logInputChange(
logger: ConnectivityPipelineLogger,
inputParamName: String,
prettyPrint: (T) -> String = { it.toString() }
): Flow<T> {
- return this.onEach {logger.logInputChange(inputParamName, prettyPrint(it)) }
+ return this.onEach { logger.logInputChange(inputParamName, prettyPrint(it)) }
}
/**
* Log a change in one of the **outputs** to the connectivity pipeline.
*
* @param prettyPrint an optional function to transform the value into a readable string.
- * [toString] is used if no custom function is provided.
+ * [toString] is used if no custom function is provided.
*/
fun <T> Flow<T>.logOutputChange(
- logger: ConnectivityPipelineLogger,
- outputParamName: String,
- prettyPrint: (T) -> String = { it.toString() }
+ logger: ConnectivityPipelineLogger,
+ outputParamName: String,
+ prettyPrint: (T) -> String = { it.toString() }
): Flow<T> {
return this.onEach { logger.logOutputChange(outputParamName, prettyPrint(it)) }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/model/DataActivityModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/model/DataActivityModel.kt
index 9b41567..05d0714 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/model/DataActivityModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/model/DataActivityModel.kt
@@ -16,6 +16,9 @@
package com.android.systemui.statusbar.pipeline.shared.data.model
+import android.net.wifi.WifiManager
+import android.telephony.Annotation
+import android.telephony.TelephonyManager
import com.android.systemui.log.table.Diffable
import com.android.systemui.log.table.TableRowLogger
@@ -44,3 +47,25 @@
const val ACTIVITY_PREFIX = "dataActivity"
private const val COL_ACTIVITY_IN = "in"
private const val COL_ACTIVITY_OUT = "out"
+
+fun @receiver:Annotation.DataActivityType Int.toMobileDataActivityModel(): DataActivityModel =
+ when (this) {
+ TelephonyManager.DATA_ACTIVITY_IN ->
+ DataActivityModel(hasActivityIn = true, hasActivityOut = false)
+ TelephonyManager.DATA_ACTIVITY_OUT ->
+ DataActivityModel(hasActivityIn = false, hasActivityOut = true)
+ TelephonyManager.DATA_ACTIVITY_INOUT ->
+ DataActivityModel(hasActivityIn = true, hasActivityOut = true)
+ else -> DataActivityModel(hasActivityIn = false, hasActivityOut = false)
+ }
+
+fun Int.toWifiDataActivityModel(): DataActivityModel =
+ when (this) {
+ WifiManager.TrafficStateCallback.DATA_ACTIVITY_IN ->
+ DataActivityModel(hasActivityIn = true, hasActivityOut = false)
+ WifiManager.TrafficStateCallback.DATA_ACTIVITY_OUT ->
+ DataActivityModel(hasActivityIn = false, hasActivityOut = true)
+ WifiManager.TrafficStateCallback.DATA_ACTIVITY_INOUT ->
+ DataActivityModel(hasActivityIn = true, hasActivityOut = true)
+ else -> DataActivityModel(hasActivityIn = false, hasActivityOut = false)
+ }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
index 8144198..5ccd6f4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
@@ -43,6 +43,7 @@
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.SB_LOGGING_TAG
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange
import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
+import com.android.systemui.statusbar.pipeline.shared.data.model.toWifiDataActivityModel
import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
import java.util.concurrent.Executor
import javax.inject.Inject
@@ -237,7 +238,7 @@
conflatedCallbackFlow {
val callback = TrafficStateCallback { state ->
logger.logInputChange("onTrafficStateChange", prettyPrintActivity(state))
- trySend(trafficStateToDataActivityModel(state))
+ trySend(state.toWifiDataActivityModel())
}
wifiManager.registerTrafficStateCallback(mainExecutor, callback)
awaitClose { wifiManager.unregisterTrafficStateCallback(callback) }
@@ -266,15 +267,6 @@
// NetworkCallback inside [wifiNetwork] for our wifi network information.
val WIFI_NETWORK_DEFAULT = WifiNetworkModel.Inactive
- private fun trafficStateToDataActivityModel(state: Int): DataActivityModel {
- return DataActivityModel(
- hasActivityIn = state == TrafficStateCallback.DATA_ACTIVITY_IN ||
- state == TrafficStateCallback.DATA_ACTIVITY_INOUT,
- hasActivityOut = state == TrafficStateCallback.DATA_ACTIVITY_OUT ||
- state == TrafficStateCallback.DATA_ACTIVITY_INOUT,
- )
- }
-
private fun networkCapabilitiesToWifiInfo(
networkCapabilities: NetworkCapabilities
): WifiInfo? {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcher.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcher.kt
new file mode 100644
index 0000000..73bcdfd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcher.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.wifi.data.repository
+
+import android.os.Bundle
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.demomode.DemoMode
+import com.android.systemui.demomode.DemoModeController
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoWifiRepository
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * Provides the [WifiRepository] interface either through the [DemoWifiRepository] implementation,
+ * or the [WifiRepositoryImpl]'s prod implementation, based on the current demo mode value. In this
+ * way, downstream clients can all consist of real implementations and not care about which
+ * repository is responsible for the data. Graphically:
+ *
+ * ```
+ * RealRepository
+ * │
+ * ├──►RepositorySwitcher──►RealInteractor──►RealViewModel
+ * │
+ * DemoRepository
+ * ```
+ *
+ * When demo mode turns on, every flow will [flatMapLatest] to the current provider's version of
+ * that flow.
+ */
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+class WifiRepositorySwitcher
+@Inject
+constructor(
+ private val realImpl: WifiRepositoryImpl,
+ private val demoImpl: DemoWifiRepository,
+ private val demoModeController: DemoModeController,
+ @Application scope: CoroutineScope,
+) : WifiRepository {
+ private val isDemoMode =
+ conflatedCallbackFlow {
+ val callback =
+ object : DemoMode {
+ override fun dispatchDemoCommand(command: String?, args: Bundle?) {
+ // Don't care
+ }
+
+ override fun onDemoModeStarted() {
+ demoImpl.startProcessingCommands()
+ trySend(true)
+ }
+
+ override fun onDemoModeFinished() {
+ demoImpl.stopProcessingCommands()
+ trySend(false)
+ }
+ }
+
+ demoModeController.addCallback(callback)
+ awaitClose { demoModeController.removeCallback(callback) }
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), demoModeController.isInDemoMode)
+
+ @VisibleForTesting
+ val activeRepo =
+ isDemoMode
+ .mapLatest { isDemoMode ->
+ if (isDemoMode) {
+ demoImpl
+ } else {
+ realImpl
+ }
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), realImpl)
+
+ override val isWifiEnabled: StateFlow<Boolean> =
+ activeRepo
+ .flatMapLatest { it.isWifiEnabled }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), realImpl.isWifiEnabled.value)
+
+ override val isWifiDefault: StateFlow<Boolean> =
+ activeRepo
+ .flatMapLatest { it.isWifiDefault }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), realImpl.isWifiDefault.value)
+
+ override val wifiNetwork: StateFlow<WifiNetworkModel> =
+ activeRepo
+ .flatMapLatest { it.wifiNetwork }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), realImpl.wifiNetwork.value)
+
+ override val wifiActivity: StateFlow<DataActivityModel> =
+ activeRepo
+ .flatMapLatest { it.wifiActivity }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), realImpl.wifiActivity.value)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoModeWifiDataSource.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoModeWifiDataSource.kt
new file mode 100644
index 0000000..c588945
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoModeWifiDataSource.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.wifi.data.repository.demo
+
+import android.net.wifi.WifiManager
+import android.os.Bundle
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.demomode.DemoMode.COMMAND_NETWORK
+import com.android.systemui.demomode.DemoModeController
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.shareIn
+
+/** Data source to map between demo mode commands and inputs into [DemoWifiRepository]'s flows */
+@SysUISingleton
+class DemoModeWifiDataSource
+@Inject
+constructor(
+ demoModeController: DemoModeController,
+ @Application scope: CoroutineScope,
+) {
+ private val demoCommandStream = demoModeController.demoFlowForCommand(COMMAND_NETWORK)
+ private val _wifiCommands = demoCommandStream.map { args -> args.toWifiEvent() }
+ val wifiEvents = _wifiCommands.shareIn(scope, SharingStarted.WhileSubscribed())
+
+ private fun Bundle.toWifiEvent(): FakeWifiEventModel? {
+ val wifi = getString("wifi") ?: return null
+ return if (wifi == "show") {
+ activeWifiEvent()
+ } else {
+ FakeWifiEventModel.WifiDisabled
+ }
+ }
+
+ private fun Bundle.activeWifiEvent(): FakeWifiEventModel.Wifi {
+ val level = getString("level")?.toInt()
+ val activity = getString("activity")?.toActivity()
+ val ssid = getString("ssid")
+ val validated = getString("fully").toBoolean()
+
+ return FakeWifiEventModel.Wifi(
+ level = level,
+ activity = activity,
+ ssid = ssid,
+ validated = validated,
+ )
+ }
+
+ private fun String.toActivity(): Int =
+ when (this) {
+ "inout" -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_INOUT
+ "in" -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_IN
+ "out" -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_OUT
+ else -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_NONE
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt
new file mode 100644
index 0000000..7890074
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.wifi.data.repository.demo
+
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
+import com.android.systemui.statusbar.pipeline.shared.data.model.toWifiDataActivityModel
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.launch
+
+/** Demo-able wifi repository to support SystemUI demo mode commands. */
+class DemoWifiRepository
+@Inject
+constructor(
+ private val dataSource: DemoModeWifiDataSource,
+ @Application private val scope: CoroutineScope,
+) : WifiRepository {
+ private var demoCommandJob: Job? = null
+
+ private val _isWifiEnabled = MutableStateFlow(false)
+ override val isWifiEnabled: StateFlow<Boolean> = _isWifiEnabled
+
+ private val _isWifiDefault = MutableStateFlow(false)
+ override val isWifiDefault: StateFlow<Boolean> = _isWifiDefault
+
+ private val _wifiNetwork = MutableStateFlow<WifiNetworkModel>(WifiNetworkModel.Inactive)
+ override val wifiNetwork: StateFlow<WifiNetworkModel> = _wifiNetwork
+
+ private val _wifiActivity =
+ MutableStateFlow(DataActivityModel(hasActivityIn = false, hasActivityOut = false))
+ override val wifiActivity: StateFlow<DataActivityModel> = _wifiActivity
+
+ fun startProcessingCommands() {
+ demoCommandJob =
+ scope.launch {
+ dataSource.wifiEvents.filterNotNull().collect { event -> processEvent(event) }
+ }
+ }
+
+ fun stopProcessingCommands() {
+ demoCommandJob?.cancel()
+ }
+
+ private fun processEvent(event: FakeWifiEventModel) =
+ when (event) {
+ is FakeWifiEventModel.Wifi -> processEnabledWifiState(event)
+ is FakeWifiEventModel.WifiDisabled -> processDisabledWifiState()
+ }
+
+ private fun processDisabledWifiState() {
+ _isWifiEnabled.value = false
+ _isWifiDefault.value = false
+ _wifiActivity.value = DataActivityModel(hasActivityIn = false, hasActivityOut = false)
+ _wifiNetwork.value = WifiNetworkModel.Inactive
+ }
+
+ private fun processEnabledWifiState(event: FakeWifiEventModel.Wifi) {
+ _isWifiEnabled.value = true
+ _isWifiDefault.value = true
+ _wifiActivity.value =
+ event.activity?.toWifiDataActivityModel()
+ ?: DataActivityModel(hasActivityIn = false, hasActivityOut = false)
+ _wifiNetwork.value = event.toWifiNetworkModel()
+ }
+
+ private fun FakeWifiEventModel.Wifi.toWifiNetworkModel(): WifiNetworkModel =
+ WifiNetworkModel.Active(
+ networkId = DEMO_NET_ID,
+ isValidated = validated ?: true,
+ level = level,
+ ssid = ssid,
+
+ // These fields below aren't supported in demo mode, since they aren't needed to satisfy
+ // the interface.
+ isPasspointAccessPoint = false,
+ isOnlineSignUpForPasspointAccessPoint = false,
+ passpointProviderFriendlyName = null,
+ )
+
+ companion object {
+ private const val DEMO_NET_ID = 1234
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/model/FakeWifiEventModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/model/FakeWifiEventModel.kt
new file mode 100644
index 0000000..2353fb8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/model/FakeWifiEventModel.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model
+
+/**
+ * Model for demo wifi commands, ported from [NetworkControllerImpl]
+ *
+ * Nullable fields represent optional command line arguments
+ */
+sealed interface FakeWifiEventModel {
+ data class Wifi(
+ val level: Int?,
+ val activity: Int?,
+ val ssid: String?,
+ val validated: Boolean?,
+ ) : FakeWifiEventModel
+
+ object WifiDisabled : FakeWifiEventModel
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt
index 3c0eb91..4f7fe28 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt
@@ -38,16 +38,12 @@
dumpManager.registerDumpable("${SB_LOGGING_TAG}WifiConstants", this)
}
- /** True if we should show the activityIn/activityOut icons and false otherwise. */
- val shouldShowActivityConfig = context.resources.getBoolean(R.bool.config_showActivity)
-
/** True if we should always show the wifi icon when wifi is enabled and false otherwise. */
val alwaysShowIconIfEnabled =
context.resources.getBoolean(R.bool.config_showWifiIndicatorWhenEnabled)
override fun dump(pw: PrintWriter, args: Array<out String>) {
pw.apply {
- println("shouldShowActivityConfig=$shouldShowActivityConfig")
println("alwaysShowIconIfEnabled=$alwaysShowIconIfEnabled")
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
index 07a7595..ab464cc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
@@ -148,7 +148,7 @@
/** The wifi activity status. Null if we shouldn't display the activity status. */
private val activity: Flow<DataActivityModel?> =
- if (!wifiConstants.shouldShowActivityConfig) {
+ if (!connectivityConstants.shouldShowActivityConfig) {
flowOf(null)
} else {
combine(interactor.activity, interactor.ssid) { activity, ssid ->
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
index d8a8c5d..c9ed0cb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
@@ -47,6 +47,7 @@
import android.view.View;
import android.view.ViewAnimationUtils;
import android.view.ViewGroup;
+import android.view.ViewGroupOverlay;
import android.view.ViewRootImpl;
import android.view.WindowInsets;
import android.view.WindowInsetsAnimation;
@@ -57,7 +58,6 @@
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
-import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
@@ -133,6 +133,7 @@
private RevealParams mRevealParams;
private Rect mContentBackgroundBounds;
private boolean mIsFocusAnimationFlagActive;
+ private boolean mIsAnimatingAppearance = false;
// TODO(b/193539698): move these to a Controller
private RemoteInputController mController;
@@ -142,10 +143,6 @@
private boolean mSending;
private NotificationViewWrapper mWrapper;
- private Integer mDefocusTargetHeight = null;
- private boolean mIsAnimatingAppearance = false;
-
-
// TODO(b/193539698): remove this; views shouldn't have access to their controller, and places
// that need the controller shouldn't have access to the view
private RemoteInputViewController mViewController;
@@ -423,18 +420,6 @@
return mIsAnimatingAppearance;
}
- /**
- * View will ensure to use at most the provided defocusTargetHeight, when defocusing animated.
- * This is to ensure that the parent can resize itself to the targetHeight while the defocus
- * animation of the RemoteInputView is running.
- *
- * @param defocusTargetHeight The target height the parent will resize itself to. If null, the
- * RemoteInputView will not resize itself.
- */
- public void setDefocusTargetHeight(Integer defocusTargetHeight) {
- mDefocusTargetHeight = defocusTargetHeight;
- }
-
@VisibleForTesting
void onDefocus(boolean animate, boolean logClose) {
mController.removeRemoteInput(mEntry, mToken);
@@ -443,35 +428,28 @@
// During removal, we get reattached and lose focus. Not hiding in that
// case to prevent flicker.
if (!mRemoved) {
- if (animate && mIsFocusAnimationFlagActive) {
- Animator animator = getDefocusAnimator();
+ ViewGroup parent = (ViewGroup) getParent();
+ if (animate && parent != null && mIsFocusAnimationFlagActive) {
- // When defocusing, the notification needs to shrink. Therefore, we need to free
- // up the space that is needed for the RemoteInputView. This is done by setting
- // a negative top margin of the height difference of the RemoteInputView and its
- // sibling (the actions_container_layout containing the Reply button)
- if (mDefocusTargetHeight != null && mDefocusTargetHeight < getHeight()
- && mDefocusTargetHeight >= 0
- && getLayoutParams() instanceof FrameLayout.LayoutParams) {
- int heightToShrink = getHeight() - mDefocusTargetHeight;
- FrameLayout.LayoutParams layoutParams =
- (FrameLayout.LayoutParams) getLayoutParams();
- layoutParams.topMargin = -heightToShrink;
- setLayoutParams(layoutParams);
- ((ViewGroup) getParent().getParent()).setClipChildren(false);
- }
+ ViewGroup grandParent = (ViewGroup) parent.getParent();
+ ViewGroupOverlay overlay = parent.getOverlay();
+
+ // After adding this RemoteInputView to the overlay of the parent (and thus removing
+ // it from the parent itself), the parent will shrink in height. This causes the
+ // overlay to be moved. To correct the position of the overlay we need to offset it.
+ int overlayOffsetY = getMaxSiblingHeight() - getHeight();
+ overlay.add(this);
+ if (grandParent != null) grandParent.setClipChildren(false);
+
+ Animator animator = getDefocusAnimator(overlayOffsetY);
+ View self = this;
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
- //reset top margin after the animation
- if (getLayoutParams() instanceof FrameLayout.LayoutParams) {
- FrameLayout.LayoutParams layoutParams =
- (FrameLayout.LayoutParams) getLayoutParams();
- layoutParams.topMargin = 0;
- setLayoutParams(layoutParams);
- ((ViewGroup) getParent().getParent()).setClipChildren(true);
- }
+ overlay.remove(self);
+ parent.addView(self);
+ if (grandParent != null) grandParent.setClipChildren(true);
setVisibility(GONE);
if (mWrapper != null) {
mWrapper.setRemoteInputVisible(false);
@@ -609,7 +587,7 @@
}
/**
- * Sets whether the feature flag for the updated inline reply animation is active or not.
+ * Sets whether the feature flag for the revised inline reply animation is active or not.
* @param active
*/
public void setIsFocusAnimationFlagActive(boolean active) {
@@ -846,6 +824,23 @@
}
}
+ /**
+ * @return max sibling height (0 in case of no siblings)
+ */
+ public int getMaxSiblingHeight() {
+ ViewGroup parentView = (ViewGroup) getParent();
+ int maxHeight = 0;
+ if (parentView == null) return 0;
+ for (int i = 0; i < parentView.getChildCount(); i++) {
+ View siblingView = parentView.getChildAt(i);
+ if (siblingView != this) maxHeight = Math.max(maxHeight, siblingView.getHeight());
+ }
+ return maxHeight;
+ }
+
+ /**
+ * Creates an animator for the focus animation.
+ */
private Animator getFocusAnimator(View crossFadeView) {
final Animator alphaAnimator = ObjectAnimator.ofFloat(this, View.ALPHA, 0f, 1f);
alphaAnimator.setStartDelay(FOCUS_ANIMATION_FADE_IN_DELAY);
@@ -854,7 +849,7 @@
ValueAnimator scaleAnimator = ValueAnimator.ofFloat(FOCUS_ANIMATION_MIN_SCALE, 1f);
scaleAnimator.addUpdateListener(valueAnimator -> {
- setFocusAnimationScaleY((float) scaleAnimator.getAnimatedValue());
+ setFocusAnimationScaleY((float) scaleAnimator.getAnimatedValue(), 0);
});
scaleAnimator.setDuration(FOCUS_ANIMATION_TOTAL_DURATION);
scaleAnimator.setInterpolator(InterpolatorsAndroidX.FAST_OUT_SLOW_IN);
@@ -875,21 +870,26 @@
return animatorSet;
}
- private Animator getDefocusAnimator() {
+ /**
+ * Creates an animator for the defocus animation.
+ *
+ * @param offsetY The RemoteInputView will be offset by offsetY during the animation
+ */
+ private Animator getDefocusAnimator(int offsetY) {
final Animator alphaAnimator = ObjectAnimator.ofFloat(this, View.ALPHA, 1f, 0f);
alphaAnimator.setDuration(FOCUS_ANIMATION_CROSSFADE_DURATION);
alphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR);
ValueAnimator scaleAnimator = ValueAnimator.ofFloat(1f, FOCUS_ANIMATION_MIN_SCALE);
scaleAnimator.addUpdateListener(valueAnimator -> {
- setFocusAnimationScaleY((float) scaleAnimator.getAnimatedValue());
+ setFocusAnimationScaleY((float) scaleAnimator.getAnimatedValue(), offsetY);
});
scaleAnimator.setDuration(FOCUS_ANIMATION_TOTAL_DURATION);
scaleAnimator.setInterpolator(InterpolatorsAndroidX.FAST_OUT_SLOW_IN);
scaleAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation, boolean isReverse) {
- setFocusAnimationScaleY(1f);
+ setFocusAnimationScaleY(1f /* scaleY */, 0 /* verticalOffset */);
}
});
@@ -901,15 +901,21 @@
/**
* Sets affected view properties for a vertical scale animation
*
- * @param scaleY desired vertical view scale
+ * @param scaleY desired vertical view scale
+ * @param verticalOffset vertical offset to apply to the RemoteInputView during the animation
*/
- private void setFocusAnimationScaleY(float scaleY) {
+ private void setFocusAnimationScaleY(float scaleY, int verticalOffset) {
int verticalBoundOffset = (int) ((1f - scaleY) * 0.5f * mContentView.getHeight());
- mContentBackgroundBounds = new Rect(0, verticalBoundOffset, mContentView.getWidth(),
+ Rect contentBackgroundBounds = new Rect(0, verticalBoundOffset, mContentView.getWidth(),
mContentView.getHeight() - verticalBoundOffset);
- mContentBackground.setBounds(mContentBackgroundBounds);
+ mContentBackground.setBounds(contentBackgroundBounds);
mContentView.setBackground(mContentBackground);
- setTranslationY(verticalBoundOffset);
+ if (scaleY == 1f) {
+ mContentBackgroundBounds = null;
+ } else {
+ mContentBackgroundBounds = contentBackgroundBounds;
+ }
+ setTranslationY(verticalBoundOffset + verticalOffset);
}
/** Handler for button click on send action in IME. */
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
index 532fbaa..ad48e21 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
@@ -331,7 +331,7 @@
return
}
- removeViewFromWindow(displayInfo)
+ removeViewFromWindow(displayInfo, removalReason)
// Prune anything that's already timed out before determining if we should re-display a
// different chipbar.
@@ -358,14 +358,14 @@
removeViewFromWindow(displayInfo)
}
- private fun removeViewFromWindow(displayInfo: DisplayInfo) {
+ private fun removeViewFromWindow(displayInfo: DisplayInfo, removalReason: String? = null) {
val view = displayInfo.view
if (view == null) {
logger.logViewRemovalIgnored(displayInfo.info.id, "View is null")
return
}
displayInfo.view = null // Need other places??
- animateViewOut(view) {
+ animateViewOut(view, removalReason) {
windowManager.removeView(view)
displayInfo.wakeLock?.release(displayInfo.info.wakeReason)
}
@@ -428,7 +428,11 @@
*
* @param onAnimationEnd an action that *must* be run once the animation finishes successfully.
*/
- internal open fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
+ internal open fun animateViewOut(
+ view: ViewGroup,
+ removalReason: String? = null,
+ onAnimationEnd: Runnable
+ ) {
onAnimationEnd.run()
}
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 fd2c705..52980c3 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
@@ -211,7 +211,7 @@
)
}
- override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
+ override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) {
val innerView = view.getInnerView()
innerView.accessibilityLiveRegion = ACCESSIBILITY_LIVE_REGION_NONE
ViewHierarchyAnimator.animateRemoval(
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/DemoModeFragment.java b/packages/SystemUI/src/com/android/systemui/tuner/DemoModeFragment.java
index 1f44434..2464886 100644
--- a/packages/SystemUI/src/com/android/systemui/tuner/DemoModeFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/tuner/DemoModeFragment.java
@@ -33,6 +33,7 @@
import com.android.systemui.demomode.DemoMode;
import com.android.systemui.demomode.DemoModeAvailabilityTracker;
import com.android.systemui.demomode.DemoModeController;
+import com.android.systemui.util.settings.GlobalSettings;
public class DemoModeFragment extends PreferenceFragment implements OnPreferenceChangeListener {
@@ -54,13 +55,15 @@
private SwitchPreference mOnSwitch;
private DemoModeController mDemoModeController;
+ private GlobalSettings mGlobalSettings;
private Tracker mDemoModeTracker;
// We are the only ones who ever call this constructor, so don't worry about the warning
@SuppressLint("ValidFragment")
- public DemoModeFragment(DemoModeController demoModeController) {
+ public DemoModeFragment(DemoModeController demoModeController, GlobalSettings globalSettings) {
super();
mDemoModeController = demoModeController;
+ mGlobalSettings = globalSettings;
}
@@ -80,7 +83,7 @@
screen.addPreference(mOnSwitch);
setPreferenceScreen(screen);
- mDemoModeTracker = new Tracker(context);
+ mDemoModeTracker = new Tracker(context, mGlobalSettings);
mDemoModeTracker.startTracking();
updateDemoModeEnabled();
updateDemoModeOn();
@@ -202,8 +205,8 @@
}
private class Tracker extends DemoModeAvailabilityTracker {
- Tracker(Context context) {
- super(context);
+ Tracker(Context context, GlobalSettings globalSettings) {
+ super(context, globalSettings);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerActivity.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerActivity.java
index 3231aec..32ecb67 100644
--- a/packages/SystemUI/src/com/android/systemui/tuner/TunerActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerActivity.java
@@ -33,6 +33,7 @@
import com.android.systemui.R;
import com.android.systemui.demomode.DemoModeController;
import com.android.systemui.fragments.FragmentService;
+import com.android.systemui.util.settings.GlobalSettings;
import javax.inject.Inject;
@@ -44,12 +45,18 @@
private final DemoModeController mDemoModeController;
private final TunerService mTunerService;
+ private final GlobalSettings mGlobalSettings;
@Inject
- TunerActivity(DemoModeController demoModeController, TunerService tunerService) {
+ TunerActivity(
+ DemoModeController demoModeController,
+ TunerService tunerService,
+ GlobalSettings globalSettings
+ ) {
super();
mDemoModeController = demoModeController;
mTunerService = tunerService;
+ mGlobalSettings = globalSettings;
}
protected void onCreate(Bundle savedInstanceState) {
@@ -69,7 +76,7 @@
boolean showDemoMode = action != null && action.equals(
"com.android.settings.action.DEMO_MODE");
final PreferenceFragment fragment = showDemoMode
- ? new DemoModeFragment(mDemoModeController)
+ ? new DemoModeFragment(mDemoModeController, mGlobalSettings)
: new TunerFragment(mTunerService);
getFragmentManager().beginTransaction().replace(R.id.content_frame,
fragment, TAG_TUNER).commit();
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
index 9cca950..523cf68 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
@@ -159,18 +159,24 @@
ensureOverlayRemoved()
val newRoot = SurfaceControlViewHost(context, context.display!!, wwm)
- val newView =
- LightRevealScrim(context, null).apply {
- revealEffect = createLightRevealEffect()
- isScrimOpaqueChangedListener = Consumer {}
- revealAmount =
- when (reason) {
- FOLD -> TRANSPARENT
- UNFOLD -> BLACK
- }
- }
-
val params = getLayoutParams()
+ val newView =
+ LightRevealScrim(
+ context,
+ attrs = null,
+ initialWidth = params.width,
+ initialHeight = params.height
+ )
+ .apply {
+ revealEffect = createLightRevealEffect()
+ isScrimOpaqueChangedListener = Consumer {}
+ revealAmount =
+ when (reason) {
+ FOLD -> TRANSPARENT
+ UNFOLD -> BLACK
+ }
+ }
+
newRoot.setView(newView, params)
if (onOverlayReady != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java b/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java
index b56c403..cd21a45 100644
--- a/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java
@@ -32,6 +32,7 @@
import androidx.annotation.Nullable;
import com.android.settingslib.users.EditUserInfoController;
+import com.android.settingslib.users.GrantAdminDialogController;
import com.android.systemui.R;
import com.android.systemui.plugins.ActivityStarter;
@@ -60,9 +61,10 @@
private final IActivityManager mActivityManager;
private final ActivityStarter mActivityStarter;
+ private Dialog mGrantAdminDialog;
private Dialog mSetupUserDialog;
private final OnBackInvokedCallback mBackCallback = this::onBackInvoked;
-
+ private Boolean mGrantAdminRights;
@Inject
public CreateUserActivity(UserCreator userCreator,
EditUserInfoController editUserInfoController, IActivityManager activityManager,
@@ -78,14 +80,17 @@
super.onCreate(savedInstanceState);
setShowWhenLocked(true);
setContentView(R.layout.activity_create_new_user);
-
if (savedInstanceState != null) {
mEditUserInfoController.onRestoreInstanceState(savedInstanceState);
}
- mSetupUserDialog = createDialog();
- mSetupUserDialog.show();
-
+ if (mUserCreator.isHeadlessSystemUserMode()) {
+ mGrantAdminDialog = buildGrantAdminDialog();
+ mGrantAdminDialog.show();
+ } else {
+ mSetupUserDialog = createDialog();
+ mSetupUserDialog.show();
+ }
getOnBackInvokedDispatcher().registerOnBackInvokedCallback(
OnBackInvokedDispatcher.PRIORITY_DEFAULT,
mBackCallback);
@@ -124,6 +129,22 @@
);
}
+ private Dialog buildGrantAdminDialog() {
+ return new GrantAdminDialogController().createDialog(
+ this,
+ (grantAdminRights) -> {
+ mGrantAdminDialog.dismiss();
+ mGrantAdminRights = grantAdminRights;
+ mSetupUserDialog = createDialog();
+ mSetupUserDialog.show();
+ },
+ () -> {
+ mGrantAdminRights = false;
+ finish();
+ }
+ );
+ }
+
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
@@ -139,6 +160,9 @@
if (mSetupUserDialog != null) {
mSetupUserDialog.dismiss();
}
+ if (mGrantAdminDialog != null) {
+ mGrantAdminDialog.dismiss();
+ }
finish();
}
@@ -150,13 +174,15 @@
private void addUserNow(String userName, Drawable userIcon) {
mSetupUserDialog.dismiss();
-
userName = (userName == null || userName.trim().isEmpty())
? getString(com.android.settingslib.R.string.user_new_user_name)
: userName;
mUserCreator.createUser(userName, userIcon,
userInfo -> {
+ if (mGrantAdminRights) {
+ mUserCreator.setUserAdmin(userInfo.id);
+ }
switchToUser(userInfo.id);
finishIfNeeded();
}, () -> {
diff --git a/packages/SystemUI/src/com/android/systemui/user/UserCreator.kt b/packages/SystemUI/src/com/android/systemui/user/UserCreator.kt
index dcbbe74..277f670 100644
--- a/packages/SystemUI/src/com/android/systemui/user/UserCreator.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/UserCreator.kt
@@ -32,7 +32,9 @@
* A class to do the user creation process. It shows a progress dialog, and manages the user
* creation
*/
-class UserCreator @Inject constructor(
+class UserCreator
+@Inject
+constructor(
private val context: Context,
private val userManager: UserManager,
@Main private val mainExecutor: Executor,
@@ -42,14 +44,14 @@
* Shows a progress dialog then starts the user creation process on the main thread.
*
* @param successCallback is called when the user creation is successful.
- * @param errorCallback is called when userManager.createUser returns null.
- * (Exceptions are not handled by this class)
+ * @param errorCallback is called when userManager.createUser returns null. (Exceptions are not
+ * handled by this class)
*/
fun createUser(
userName: String?,
userIcon: Drawable?,
successCallback: Consumer<UserInfo?>,
- errorCallback: Runnable
+ errorCallback: Runnable
) {
val userCreationProgressDialog: Dialog = UserCreatingDialog(context)
userCreationProgressDialog.show()
@@ -71,11 +73,21 @@
newUserIcon = UserIcons.getDefaultUserIcon(res, user.id, false)
}
userManager.setUserIcon(
- user.id, UserIcons.convertToBitmapAtUserIconSize(res, newUserIcon))
+ user.id,
+ UserIcons.convertToBitmapAtUserIconSize(res, newUserIcon)
+ )
}
userCreationProgressDialog.dismiss()
successCallback.accept(user)
}
}
}
+
+ fun setUserAdmin(userId: Int) {
+ userManager.setUserAdmin(userId)
+ }
+
+ fun isHeadlessSystemUserMode(): Boolean {
+ return UserManager.isHeadlessSystemUserMode()
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
index 74295f0..45cb11e 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
@@ -56,6 +56,7 @@
import com.android.systemui.user.shared.model.UserActionModel
import com.android.systemui.user.shared.model.UserModel
import com.android.systemui.user.utils.MultiUserActionsEvent
+import com.android.systemui.user.utils.MultiUserActionsEventHelper
import com.android.systemui.util.kotlin.pairwise
import java.io.PrintWriter
import javax.inject.Inject
@@ -372,6 +373,9 @@
if (LegacyUserDataHelper.isUser(record)) {
// It's safe to use checkNotNull around record.info because isUser only returns true
// if record.info is not null.
+ uiEventLogger.log(
+ MultiUserActionsEventHelper.userSwitchMetric(checkNotNull(record.info))
+ )
selectUser(checkNotNull(record.info).id, dialogShower)
} else {
executeAction(LegacyUserDataHelper.toUserActionModel(record), dialogShower)
diff --git a/packages/SystemUI/src/com/android/systemui/user/utils/MultiUserActionsEvent.kt b/packages/SystemUI/src/com/android/systemui/user/utils/MultiUserActionsEvent.kt
index bfedac9..ec79b6d 100644
--- a/packages/SystemUI/src/com/android/systemui/user/utils/MultiUserActionsEvent.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/utils/MultiUserActionsEvent.kt
@@ -23,7 +23,13 @@
@UiEvent(doc = "Add User tap from User Switcher.") CREATE_USER_FROM_USER_SWITCHER(1257),
@UiEvent(doc = "Add Guest tap from User Switcher.") CREATE_GUEST_FROM_USER_SWITCHER(1258),
@UiEvent(doc = "Add Restricted User tap from User Switcher.")
- CREATE_RESTRICTED_USER_FROM_USER_SWITCHER(1259);
+ CREATE_RESTRICTED_USER_FROM_USER_SWITCHER(1259),
+ @UiEvent(doc = "Switch to User tap from User Switcher.")
+ SWITCH_TO_USER_FROM_USER_SWITCHER(1266),
+ @UiEvent(doc = "Switch to Guest User tap from User Switcher.")
+ SWITCH_TO_GUEST_FROM_USER_SWITCHER(1267),
+ @UiEvent(doc = "Switch to Restricted User tap from User Switcher.")
+ SWITCH_TO_RESTRICTED_USER_FROM_USER_SWITCHER(1268);
override fun getId(): Int {
return value
diff --git a/packages/SystemUI/src/com/android/systemui/user/utils/MultiUserActionsEventHelper.kt b/packages/SystemUI/src/com/android/systemui/user/utils/MultiUserActionsEventHelper.kt
new file mode 100644
index 0000000..d12a788
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/utils/MultiUserActionsEventHelper.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.user.utils
+
+import android.content.pm.UserInfo
+
+class MultiUserActionsEventHelper {
+ /** Return MultiUserActionsEvent based on UserInfo. */
+ companion object {
+ fun userSwitchMetric(userInfo: UserInfo): MultiUserActionsEvent {
+ if (userInfo.isGuest) return MultiUserActionsEvent.SWITCH_TO_GUEST_FROM_USER_SWITCHER
+ if (userInfo.isRestricted)
+ return MultiUserActionsEvent.SWITCH_TO_RESTRICTED_USER_FROM_USER_SWITCHER
+ return MultiUserActionsEvent.SWITCH_TO_USER_FROM_USER_SWITCHER
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt
index 9653985..d6b3b22 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt
@@ -21,6 +21,8 @@
import androidx.lifecycle.repeatOnLifecycle
import com.android.systemui.lifecycle.repeatWhenAttached
import java.util.function.Consumer
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
@@ -34,7 +36,10 @@
view: View,
flow: Flow<T>,
consumer: Consumer<T>,
+ coroutineContext: CoroutineContext = EmptyCoroutineContext,
state: Lifecycle.State = Lifecycle.State.CREATED,
) {
- view.repeatWhenAttached { repeatOnLifecycle(state) { flow.collect { consumer.accept(it) } } }
+ view.repeatWhenAttached(coroutineContext) {
+ repeatOnLifecycle(state) { flow.collect { consumer.accept(it) } }
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
index c94c97c..be4bbdf 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
@@ -25,6 +25,7 @@
import android.testing.AndroidTestingRunner;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.plugins.ClockAnimations;
import com.android.systemui.statusbar.phone.DozeParameters;
import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
@@ -59,6 +60,8 @@
@Mock
DozeParameters mDozeParameters;
@Mock
+ FeatureFlags mFeatureFlags;
+ @Mock
ScreenOffAnimationController mScreenOffAnimationController;
@Captor
private ArgumentCaptor<KeyguardUpdateMonitorCallback> mKeyguardUpdateMonitorCallbackCaptor;
@@ -77,6 +80,7 @@
mKeyguardUpdateMonitor,
mConfigurationController,
mDozeParameters,
+ mFeatureFlags,
mScreenOffAnimationController);
}
@@ -96,9 +100,9 @@
public void setTranslationYExcludingMedia_forwardsCallToView() {
float translationY = 123f;
- mController.setTranslationYExcludingMedia(translationY);
+ mController.setTranslationY(translationY, /* excludeMedia= */true);
- verify(mKeyguardStatusView).setChildrenTranslationYExcludingMediaView(translationY);
+ verify(mKeyguardStatusView).setChildrenTranslationY(translationY, /* excludeMedia= */true);
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt
index ce44f4d..508aea5 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt
@@ -37,19 +37,23 @@
fun setChildrenTranslationYExcludingMediaView_mediaViewIsNotTranslated() {
val translationY = 1234f
- keyguardStatusView.setChildrenTranslationYExcludingMediaView(translationY)
+ keyguardStatusView.setChildrenTranslationY(translationY, /* excludeMedia= */true)
assertThat(mediaView.translationY).isEqualTo(0)
- }
-
- @Test
- fun setChildrenTranslationYExcludingMediaView_childrenAreTranslated() {
- val translationY = 1234f
-
- keyguardStatusView.setChildrenTranslationYExcludingMediaView(translationY)
childrenExcludingMedia.forEach {
assertThat(it.translationY).isEqualTo(translationY)
}
}
-}
\ No newline at end of file
+
+ @Test
+ fun setChildrenTranslationYIncludeMediaView() {
+ val translationY = 1234f
+
+ keyguardStatusView.setChildrenTranslationY(translationY, /* excludeMedia= */false)
+
+ statusViewContainer.children.forEach {
+ assertThat(it.translationY).isEqualTo(translationY)
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUnfoldTransitionTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUnfoldTransitionTest.kt
index 6c1f008..bb03f28 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUnfoldTransitionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUnfoldTransitionTest.kt
@@ -22,9 +22,13 @@
import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.StatusBarState.KEYGUARD
+import com.android.systemui.statusbar.StatusBarState.SHADE
import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider
import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
@@ -33,7 +37,6 @@
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
/**
@@ -50,7 +53,9 @@
@Mock private lateinit var parent: ViewGroup
- private lateinit var keyguardUnfoldTransition: KeyguardUnfoldTransition
+ @Mock private lateinit var statusBarStateController: StatusBarStateController
+
+ private lateinit var underTest: KeyguardUnfoldTransition
private lateinit var progressListener: TransitionProgressListener
private var xTranslationMax = 0f
@@ -61,10 +66,10 @@
xTranslationMax =
context.resources.getDimensionPixelSize(R.dimen.keyguard_unfold_translation_x).toFloat()
- keyguardUnfoldTransition = KeyguardUnfoldTransition(context, progressProvider)
+ underTest = KeyguardUnfoldTransition(context, statusBarStateController, progressProvider)
- keyguardUnfoldTransition.setup(parent)
- keyguardUnfoldTransition.statusViewCentered = false
+ underTest.setup(parent)
+ underTest.statusViewCentered = false
verify(progressProvider).addCallback(capture(progressListenerCaptor))
progressListener = progressListenerCaptor.value
@@ -72,10 +77,11 @@
@Test
fun onTransition_centeredViewDoesNotMove() {
- keyguardUnfoldTransition.statusViewCentered = true
+ whenever(statusBarStateController.getState()).thenReturn(KEYGUARD)
+ underTest.statusViewCentered = true
val view = View(context)
- `when`(parent.findViewById<View>(R.id.lockscreen_clock_view_large)).thenReturn(view)
+ whenever(parent.findViewById<View>(R.id.lockscreen_clock_view_large)).thenReturn(view)
progressListener.onTransitionStarted()
assertThat(view.translationX).isZero()
@@ -89,4 +95,44 @@
progressListener.onTransitionFinished()
assertThat(view.translationX).isZero()
}
+
+ @Test
+ fun whenInShadeState_viewDoesNotMove() {
+ whenever(statusBarStateController.getState()).thenReturn(SHADE)
+
+ val view = View(context)
+ whenever(parent.findViewById<View>(R.id.lockscreen_clock_view_large)).thenReturn(view)
+
+ progressListener.onTransitionStarted()
+ assertThat(view.translationX).isZero()
+
+ progressListener.onTransitionProgress(0f)
+ assertThat(view.translationX).isZero()
+
+ progressListener.onTransitionProgress(0.5f)
+ assertThat(view.translationX).isZero()
+
+ progressListener.onTransitionFinished()
+ assertThat(view.translationX).isZero()
+ }
+
+ @Test
+ fun whenInKeyguardState_viewDoesMove() {
+ whenever(statusBarStateController.getState()).thenReturn(KEYGUARD)
+
+ val view = View(context)
+ whenever(parent.findViewById<View>(R.id.lockscreen_clock_view_large)).thenReturn(view)
+
+ progressListener.onTransitionStarted()
+ assertThat(view.translationX).isZero()
+
+ progressListener.onTransitionProgress(0f)
+ assertThat(view.translationX).isEqualTo(xTranslationMax)
+
+ progressListener.onTransitionProgress(0.5f)
+ assertThat(view.translationX).isEqualTo(0.5f * xTranslationMax)
+
+ progressListener.onTransitionFinished()
+ assertThat(view.translationX).isZero()
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
index 898f370..b4696e4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
@@ -35,8 +35,6 @@
import android.view.WindowManager
import android.widget.ScrollView
import androidx.test.filters.SmallTest
-import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn
-import com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn
import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.widget.LockPatternUtils
import com.android.systemui.R
@@ -81,6 +79,8 @@
@Mock
lateinit var wakefulnessLifecycle: WakefulnessLifecycle
@Mock
+ lateinit var panelInteractionDetector: AuthDialogPanelInteractionDetector
+ @Mock
lateinit var windowToken: IBinder
@Mock
lateinit var interactionJankMonitor: InteractionJankMonitor
@@ -170,26 +170,6 @@
}
@Test
- fun testDismissesOnFocusLoss() {
- val container = initializeFingerprintContainer()
- waitForIdleSync()
-
- val requestID = authContainer?.requestId ?: 0L
-
- verify(callback).onDialogAnimatedIn(requestID)
-
- container.onWindowFocusChanged(false)
- waitForIdleSync()
-
- verify(callback).onDismissed(
- eq(AuthDialogCallback.DISMISSED_USER_CANCELED),
- eq<ByteArray?>(null), /* credentialAttestation */
- eq(requestID)
- )
- assertThat(container.parent).isNull()
- }
-
- @Test
fun testFocusLossAfterRotating() {
val container = initializeFingerprintContainer()
waitForIdleSync()
@@ -209,35 +189,6 @@
}
@Test
- fun testDismissesOnFocusLoss_hidesKeyboardWhenVisible() {
- val container = initializeFingerprintContainer(
- authenticators = BiometricManager.Authenticators.DEVICE_CREDENTIAL
- )
- waitForIdleSync()
-
- val requestID = authContainer?.requestId ?: 0L
-
- // Simulate keyboard was shown on the credential view
- val windowInsetsController = container.windowInsetsController
- spyOn(windowInsetsController)
- spyOn(container.rootWindowInsets)
- doReturn(true).`when`(container.rootWindowInsets).isVisible(WindowInsets.Type.ime())
-
- container.onWindowFocusChanged(false)
- waitForIdleSync()
-
- // Expect hiding IME request will be invoked when dismissing the view
- verify(windowInsetsController)?.hide(WindowInsets.Type.ime())
-
- verify(callback).onDismissed(
- eq(AuthDialogCallback.DISMISSED_USER_CANCELED),
- eq<ByteArray?>(null), /* credentialAttestation */
- eq(requestID)
- )
- assertThat(container.parent).isNull()
- }
-
- @Test
fun testActionAuthenticated_sendsDismissedAuthenticated() {
val container = initializeFingerprintContainer()
container.mBiometricCallback.onAction(
@@ -519,6 +470,7 @@
fingerprintProps,
faceProps,
wakefulnessLifecycle,
+ panelInteractionDetector,
userManager,
lockPatternUtils,
interactionJankMonitor,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
index 67b293f..5afe49e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
@@ -152,6 +152,8 @@
@Mock
private WakefulnessLifecycle mWakefulnessLifecycle;
@Mock
+ private AuthDialogPanelInteractionDetector mPanelInteractionDetector;
+ @Mock
private UserManager mUserManager;
@Mock
private LockPatternUtils mLockPatternUtils;
@@ -953,9 +955,10 @@
super(context, mExecution, mCommandQueue, mActivityTaskManager, mWindowManager,
mFingerprintManager, mFaceManager, () -> mUdfpsController,
() -> mSideFpsController, mDisplayManager, mWakefulnessLifecycle,
- mUserManager, mLockPatternUtils, mUdfpsLogger, mLogContextInteractor,
- () -> mBiometricPromptCredentialInteractor, () -> mCredentialViewModel,
- mInteractionJankMonitor, mHandler, mBackgroundExecutor, mVibratorHelper);
+ mPanelInteractionDetector, mUserManager, mLockPatternUtils, mUdfpsLogger,
+ mLogContextInteractor, () -> mBiometricPromptCredentialInteractor,
+ () -> mCredentialViewModel, mInteractionJankMonitor, mHandler,
+ mBackgroundExecutor, mVibratorHelper);
}
@Override
@@ -963,7 +966,9 @@
boolean requireConfirmation, int userId, int[] sensorIds,
String opPackageName, boolean skipIntro, long operationId, long requestId,
@BiometricManager.BiometricMultiSensorMode int multiSensorConfig,
- WakefulnessLifecycle wakefulnessLifecycle, UserManager userManager,
+ WakefulnessLifecycle wakefulnessLifecycle,
+ AuthDialogPanelInteractionDetector panelInteractionDetector,
+ UserManager userManager,
LockPatternUtils lockPatternUtils) {
mLastBiometricPromptInfo = promptInfo;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java
index e7e6918..bdd496e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java
@@ -18,6 +18,8 @@
import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CLIPBOARD_OVERLAY_ENABLED;
+import static com.google.android.setupcompat.util.WizardManagerHelper.SETTINGS_SECURE_USER_SETUP_COMPLETE;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -32,6 +34,7 @@
import android.content.ClipboardManager;
import android.os.PersistableBundle;
import android.provider.DeviceConfig;
+import android.provider.Settings;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
@@ -66,6 +69,8 @@
@Mock
private ClipboardOverlayController mOverlayController;
@Mock
+ private ClipboardToast mClipboardToast;
+ @Mock
private UiEventLogger mUiEventLogger;
@Mock
private FeatureFlags mFeatureFlags;
@@ -84,6 +89,8 @@
@Spy
private Provider<ClipboardOverlayController> mOverlayControllerProvider;
+ private ClipboardListener mClipboardListener;
+
@Before
public void setup() {
@@ -93,7 +100,8 @@
when(mClipboardOverlayControllerLegacyFactory.create(any()))
.thenReturn(mOverlayControllerLegacy);
when(mClipboardManager.hasPrimaryClip()).thenReturn(true);
-
+ Settings.Secure.putInt(
+ mContext.getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 1);
mSampleClipData = new ClipData("Test", new String[]{"text/plain"},
new ClipData.Item("Test Item"));
@@ -101,16 +109,17 @@
when(mClipboardManager.getPrimaryClipSource()).thenReturn(mSampleSource);
mDeviceConfigProxy = new DeviceConfigProxyFake();
+
+ mClipboardListener = new ClipboardListener(getContext(), mDeviceConfigProxy,
+ mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory,
+ mClipboardToast, mClipboardManager, mUiEventLogger, mFeatureFlags);
}
@Test
public void test_disabled() {
mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED,
"false", false);
- ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy,
- mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory,
- mClipboardManager, mUiEventLogger, mFeatureFlags);
- listener.start();
+ mClipboardListener.start();
verifyZeroInteractions(mClipboardManager);
verifyZeroInteractions(mUiEventLogger);
}
@@ -119,10 +128,7 @@
public void test_enabled() {
mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED,
"true", false);
- ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy,
- mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory,
- mClipboardManager, mUiEventLogger, mFeatureFlags);
- listener.start();
+ mClipboardListener.start();
verify(mClipboardManager).addPrimaryClipChangedListener(any());
verifyZeroInteractions(mUiEventLogger);
}
@@ -133,11 +139,8 @@
mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED,
"true", false);
- ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy,
- mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory,
- mClipboardManager, mUiEventLogger, mFeatureFlags);
- listener.start();
- listener.onPrimaryClipChanged();
+ mClipboardListener.start();
+ mClipboardListener.onPrimaryClipChanged();
verify(mClipboardOverlayControllerLegacyFactory).create(any());
@@ -152,14 +155,14 @@
// Should clear the overlay controller
mRunnableCaptor.getValue().run();
- listener.onPrimaryClipChanged();
+ mClipboardListener.onPrimaryClipChanged();
verify(mClipboardOverlayControllerLegacyFactory, times(2)).create(any());
// Not calling the runnable here, just change the clip again and verify that the overlay is
// NOT recreated.
- listener.onPrimaryClipChanged();
+ mClipboardListener.onPrimaryClipChanged();
verify(mClipboardOverlayControllerLegacyFactory, times(2)).create(any());
verifyZeroInteractions(mOverlayControllerProvider);
@@ -171,11 +174,8 @@
mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED,
"true", false);
- ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy,
- mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory,
- mClipboardManager, mUiEventLogger, mFeatureFlags);
- listener.start();
- listener.onPrimaryClipChanged();
+ mClipboardListener.start();
+ mClipboardListener.onPrimaryClipChanged();
verify(mOverlayControllerProvider).get();
@@ -190,14 +190,14 @@
// Should clear the overlay controller
mRunnableCaptor.getValue().run();
- listener.onPrimaryClipChanged();
+ mClipboardListener.onPrimaryClipChanged();
verify(mOverlayControllerProvider, times(2)).get();
// Not calling the runnable here, just change the clip again and verify that the overlay is
// NOT recreated.
- listener.onPrimaryClipChanged();
+ mClipboardListener.onPrimaryClipChanged();
verify(mOverlayControllerProvider, times(2)).get();
verifyZeroInteractions(mClipboardOverlayControllerLegacyFactory);
@@ -233,13 +233,10 @@
public void test_logging_enterAndReenter() {
when(mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR)).thenReturn(false);
- ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy,
- mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory,
- mClipboardManager, mUiEventLogger, mFeatureFlags);
- listener.start();
+ mClipboardListener.start();
- listener.onPrimaryClipChanged();
- listener.onPrimaryClipChanged();
+ mClipboardListener.onPrimaryClipChanged();
+ mClipboardListener.onPrimaryClipChanged();
verify(mUiEventLogger, times(1)).log(
ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ENTERED, 0, mSampleSource);
@@ -251,17 +248,29 @@
public void test_logging_enterAndReenter_new() {
when(mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR)).thenReturn(true);
- ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy,
- mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory,
- mClipboardManager, mUiEventLogger, mFeatureFlags);
- listener.start();
+ mClipboardListener.start();
- listener.onPrimaryClipChanged();
- listener.onPrimaryClipChanged();
+ mClipboardListener.onPrimaryClipChanged();
+ mClipboardListener.onPrimaryClipChanged();
verify(mUiEventLogger, times(1)).log(
ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ENTERED, 0, mSampleSource);
verify(mUiEventLogger, times(1)).log(
ClipboardOverlayEvent.CLIPBOARD_OVERLAY_UPDATED, 0, mSampleSource);
}
+
+ @Test
+ public void test_userSetupIncomplete_showsToast() {
+ Settings.Secure.putInt(
+ mContext.getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0);
+
+ mClipboardListener.start();
+ mClipboardListener.onPrimaryClipChanged();
+
+ verify(mUiEventLogger, times(1)).log(
+ ClipboardOverlayEvent.CLIPBOARD_TOAST_SHOWN, 0, mSampleSource);
+ verify(mClipboardToast, times(1)).showCopiedToast();
+ verifyZeroInteractions(mOverlayControllerProvider);
+ verifyZeroInteractions(mClipboardOverlayControllerLegacyFactory);
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/demomode/DemoModeControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/demomode/DemoModeControllerTest.kt
new file mode 100644
index 0000000..87c66b5
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/demomode/DemoModeControllerTest.kt
@@ -0,0 +1,104 @@
+/*
+ * 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.demomode
+
+import android.content.Intent
+import android.os.Bundle
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.demomode.DemoMode.ACTION_DEMO
+import com.android.systemui.demomode.DemoMode.COMMAND_STATUS
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.util.settings.FakeSettings
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+@SmallTest
+class DemoModeControllerTest : SysuiTestCase() {
+ private lateinit var underTest: DemoModeController
+
+ @Mock private lateinit var dumpManager: DumpManager
+
+ private val globalSettings = FakeSettings()
+
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+
+ @Before
+ fun setUp() {
+ allowTestableLooperAsMainThread()
+
+ MockitoAnnotations.initMocks(this)
+
+ globalSettings.putInt(DemoModeController.DEMO_MODE_ALLOWED, 1)
+ globalSettings.putInt(DemoModeController.DEMO_MODE_ON, 1)
+
+ underTest =
+ DemoModeController(
+ context = context,
+ dumpManager = dumpManager,
+ globalSettings = globalSettings,
+ broadcastDispatcher = fakeBroadcastDispatcher
+ )
+
+ underTest.initialize()
+ }
+
+ @Test
+ fun `demo command flow - returns args`() =
+ testScope.runTest {
+ var latest: Bundle? = null
+ val flow = underTest.demoFlowForCommand(TEST_COMMAND)
+ val job = launch { flow.collect { latest = it } }
+
+ sendDemoCommand(args = mapOf("key1" to "val1"))
+ assertThat(latest!!.getString("key1")).isEqualTo("val1")
+
+ sendDemoCommand(args = mapOf("key2" to "val2"))
+ assertThat(latest!!.getString("key2")).isEqualTo("val2")
+
+ job.cancel()
+ }
+
+ private fun sendDemoCommand(command: String? = TEST_COMMAND, args: Map<String, String>) {
+ val intent = Intent(ACTION_DEMO)
+ intent.putExtra("command", command)
+ args.forEach { arg -> intent.putExtra(arg.key, arg.value) }
+
+ fakeBroadcastDispatcher.registeredReceivers.forEach { it.onReceive(context, intent) }
+ }
+
+ companion object {
+ // Use a valid command until we properly fake out everything
+ const val TEST_COMMAND = COMMAND_STATUS
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java
index 07d7e79..cefba62 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java
@@ -37,6 +37,7 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.content.res.Resources;
import android.database.ContentObserver;
import android.hardware.Sensor;
import android.hardware.display.AmbientDisplayConfiguration;
@@ -78,7 +79,8 @@
@RunWithLooper
@SmallTest
public class DozeSensorsTest extends SysuiTestCase {
-
+ @Mock
+ private Resources mResources;
@Mock
private AsyncSensorManager mSensorManager;
@Mock
@@ -428,7 +430,7 @@
@Test
public void testGesturesAllInitiallyRespectSettings() {
- DozeSensors dozeSensors = new DozeSensors(mSensorManager, mDozeParameters,
+ DozeSensors dozeSensors = new DozeSensors(mResources, mSensorManager, mDozeParameters,
mAmbientDisplayConfiguration, mWakeLock, mCallback, mProxCallback, mDozeLog,
mProximitySensor, mFakeSettings, mAuthController,
mDevicePostureController, mUserTracker);
@@ -438,9 +440,47 @@
}
}
+ @Test
+ public void liftToWake_defaultSetting_configDefaultFalse() {
+ // WHEN the default lift to wake gesture setting is false
+ when(mResources.getBoolean(
+ com.android.internal.R.bool.config_dozePickupGestureEnabled)).thenReturn(false);
+
+ DozeSensors dozeSensors = new DozeSensors(mResources, mSensorManager, mDozeParameters,
+ mAmbientDisplayConfiguration, mWakeLock, mCallback, mProxCallback, mDozeLog,
+ mProximitySensor, mFakeSettings, mAuthController,
+ mDevicePostureController, mUserTracker);
+
+ for (TriggerSensor sensor : dozeSensors.mTriggerSensors) {
+ // THEN lift to wake's TriggerSensor enabledBySettings is false
+ if (sensor.mPulseReason == DozeLog.REASON_SENSOR_PICKUP) {
+ assertFalse(sensor.enabledBySetting());
+ }
+ }
+ }
+
+ @Test
+ public void liftToWake_defaultSetting_configDefaultTrue() {
+ // WHEN the default lift to wake gesture setting is true
+ when(mResources.getBoolean(
+ com.android.internal.R.bool.config_dozePickupGestureEnabled)).thenReturn(true);
+
+ DozeSensors dozeSensors = new DozeSensors(mResources, mSensorManager, mDozeParameters,
+ mAmbientDisplayConfiguration, mWakeLock, mCallback, mProxCallback, mDozeLog,
+ mProximitySensor, mFakeSettings, mAuthController,
+ mDevicePostureController, mUserTracker);
+
+ for (TriggerSensor sensor : dozeSensors.mTriggerSensors) {
+ // THEN lift to wake's TriggerSensor enabledBySettings is true
+ if (sensor.mPulseReason == DozeLog.REASON_SENSOR_PICKUP) {
+ assertTrue(sensor.enabledBySetting());
+ }
+ }
+ }
+
private class TestableDozeSensors extends DozeSensors {
TestableDozeSensors() {
- super(mSensorManager, mDozeParameters,
+ super(mResources, mSensorManager, mDozeParameters,
mAmbientDisplayConfiguration, mWakeLock, mCallback, mProxCallback, mDozeLog,
mProximitySensor, mFakeSettings, mAuthController,
mDevicePostureController, mUserTracker);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamCallbackControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamCallbackControllerTest.kt
deleted file mode 100644
index 003efbf..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamCallbackControllerTest.kt
+++ /dev/null
@@ -1,63 +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.systemui.dreams
-
-import android.testing.AndroidTestingRunner
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.Mockito.never
-import org.mockito.Mockito.reset
-import org.mockito.Mockito.times
-import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-class DreamCallbackControllerTest : SysuiTestCase() {
-
- @Mock private lateinit var callback: DreamCallbackController.DreamCallback
-
- private lateinit var underTest: DreamCallbackController
-
- @Before
- fun setUp() {
- MockitoAnnotations.initMocks(this)
- underTest = DreamCallbackController()
- }
-
- @Test
- fun testOnWakeUpInvokesCallback() {
- underTest.addCallback(callback)
- underTest.onWakeUp()
- verify(callback).onWakeUp()
-
- // Adding twice should not invoke twice
- reset(callback)
- underTest.addCallback(callback)
- underTest.onWakeUp()
- verify(callback, times(1)).onWakeUp()
-
- // After remove, no call to callback
- reset(callback)
- underTest.removeCallback(callback)
- underTest.onWakeUp()
- verify(callback, never()).onWakeUp()
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayCallbackControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayCallbackControllerTest.kt
new file mode 100644
index 0000000..9f534ef
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayCallbackControllerTest.kt
@@ -0,0 +1,91 @@
+/*
+ * 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 android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class DreamOverlayCallbackControllerTest : SysuiTestCase() {
+
+ @Mock private lateinit var callback: DreamOverlayCallbackController.Callback
+
+ private lateinit var underTest: DreamOverlayCallbackController
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ underTest = DreamOverlayCallbackController()
+ }
+
+ @Test
+ fun onWakeUpInvokesCallback() {
+ underTest.onStartDream()
+ assertThat(underTest.isDreaming).isEqualTo(true)
+
+ underTest.addCallback(callback)
+ underTest.onWakeUp()
+ verify(callback).onWakeUp()
+ assertThat(underTest.isDreaming).isEqualTo(false)
+
+ // Adding twice should not invoke twice
+ reset(callback)
+ underTest.addCallback(callback)
+ underTest.onWakeUp()
+ verify(callback, times(1)).onWakeUp()
+
+ // After remove, no call to callback
+ reset(callback)
+ underTest.removeCallback(callback)
+ underTest.onWakeUp()
+ verify(callback, never()).onWakeUp()
+ }
+
+ @Test
+ fun onStartDreamInvokesCallback() {
+ underTest.addCallback(callback)
+
+ assertThat(underTest.isDreaming).isEqualTo(false)
+
+ underTest.onStartDream()
+ verify(callback).onStartDream()
+ assertThat(underTest.isDreaming).isEqualTo(true)
+
+ // Adding twice should not invoke twice
+ reset(callback)
+ underTest.addCallback(callback)
+ underTest.onStartDream()
+ verify(callback, times(1)).onStartDream()
+
+ // After remove, no call to callback
+ reset(callback)
+ underTest.removeCallback(callback)
+ underTest.onStartDream()
+ verify(callback, never()).onStartDream()
+ }
+}
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 52663ce..8f97026 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
@@ -140,7 +140,7 @@
UiEventLogger mUiEventLogger;
@Mock
- DreamCallbackController mDreamCallbackController;
+ DreamOverlayCallbackController mDreamOverlayCallbackController;
@Captor
ArgumentCaptor<View> mViewCaptor;
@@ -186,7 +186,7 @@
mUiEventLogger,
mTouchInsetManager,
LOW_LIGHT_COMPONENT,
- mDreamCallbackController);
+ mDreamOverlayCallbackController);
}
@Test
@@ -398,7 +398,7 @@
mService.onWakeUp(callback);
mMainExecutor.runAllReady();
verify(mDreamOverlayContainerViewController).wakeUp(callback, mMainExecutor);
- verify(mDreamCallbackController).onWakeUp();
+ verify(mDreamOverlayCallbackController).onWakeUp();
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
index 563d44d..be712f6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
@@ -28,8 +28,7 @@
import com.android.systemui.doze.DozeMachine
import com.android.systemui.doze.DozeTransitionCallback
import com.android.systemui.doze.DozeTransitionListener
-import com.android.systemui.dreams.DreamCallbackController
-import com.android.systemui.dreams.DreamCallbackController.DreamCallback
+import com.android.systemui.dreams.DreamOverlayCallbackController
import com.android.systemui.keyguard.WakefulnessLifecycle
import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
import com.android.systemui.keyguard.shared.model.BiometricUnlockSource
@@ -68,7 +67,7 @@
@Mock private lateinit var dozeTransitionListener: DozeTransitionListener
@Mock private lateinit var authController: AuthController
@Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
- @Mock private lateinit var dreamCallbackController: DreamCallbackController
+ @Mock private lateinit var dreamOverlayCallbackController: DreamOverlayCallbackController
private lateinit var underTest: KeyguardRepositoryImpl
@@ -86,7 +85,7 @@
keyguardUpdateMonitor,
dozeTransitionListener,
authController,
- dreamCallbackController,
+ dreamOverlayCallbackController,
)
}
@@ -171,6 +170,29 @@
}
@Test
+ fun isKeyguardOccluded() =
+ runTest(UnconfinedTestDispatcher()) {
+ whenever(keyguardStateController.isOccluded).thenReturn(false)
+ var latest: Boolean? = null
+ val job = underTest.isKeyguardOccluded.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isFalse()
+
+ val captor = argumentCaptor<KeyguardStateController.Callback>()
+ verify(keyguardStateController).addCallback(captor.capture())
+
+ whenever(keyguardStateController.isOccluded).thenReturn(true)
+ captor.value.onKeyguardShowingChanged()
+ assertThat(latest).isTrue()
+
+ whenever(keyguardStateController.isOccluded).thenReturn(false)
+ captor.value.onKeyguardShowingChanged()
+ assertThat(latest).isFalse()
+
+ job.cancel()
+ }
+
+ @Test
fun isDozing() =
runTest(UnconfinedTestDispatcher()) {
var latest: Boolean? = null
@@ -343,19 +365,22 @@
}
@Test
- fun isDreamingFromDreamCallbackController() =
+ fun isDreamingFromDreamOverlayCallbackController() =
runTest(UnconfinedTestDispatcher()) {
- whenever(keyguardUpdateMonitor.isDreaming()).thenReturn(true)
+ whenever(dreamOverlayCallbackController.isDreaming).thenReturn(false)
var latest: Boolean? = null
- val job = underTest.isDreaming.onEach { latest = it }.launchIn(this)
+ val job = underTest.isDreamingWithOverlay.onEach { latest = it }.launchIn(this)
- assertThat(latest).isTrue()
+ assertThat(latest).isFalse()
val listener =
- withArgCaptor<DreamCallbackController.DreamCallback> {
- verify(dreamCallbackController).addCallback(capture())
+ withArgCaptor<DreamOverlayCallbackController.Callback> {
+ verify(dreamOverlayCallbackController).addCallback(capture())
}
+ listener.onStartDream()
+ assertThat(latest).isTrue()
+
listener.onWakeUp()
assertThat(latest).isFalse()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
index a6cf840..d2b7838 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
@@ -23,6 +23,8 @@
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepositoryImpl
+import com.android.systemui.keyguard.shared.model.DozeStateModel
+import com.android.systemui.keyguard.shared.model.DozeTransitionModel
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionInfo
import com.android.systemui.keyguard.shared.model.WakeSleepReason
@@ -42,6 +44,7 @@
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.Mock
+import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@@ -64,8 +67,8 @@
// Used to verify transition requests for test output
@Mock private lateinit var mockTransitionRepository: KeyguardTransitionRepository
- private lateinit var lockscreenBouncerTransitionInteractor:
- LockscreenBouncerTransitionInteractor
+ private lateinit var fromLockscreenTransitionInteractor: FromLockscreenTransitionInteractor
+ private lateinit var fromDreamingTransitionInteractor: FromDreamingTransitionInteractor
@Before
fun setUp() {
@@ -79,25 +82,82 @@
transitionRepository = KeyguardTransitionRepositoryImpl()
runner = KeyguardTransitionRunner(transitionRepository)
- lockscreenBouncerTransitionInteractor =
- LockscreenBouncerTransitionInteractor(
+ fromLockscreenTransitionInteractor =
+ FromLockscreenTransitionInteractor(
scope = testScope,
keyguardInteractor = KeyguardInteractor(keyguardRepository),
shadeRepository = shadeRepository,
keyguardTransitionRepository = mockTransitionRepository,
keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository),
)
- lockscreenBouncerTransitionInteractor.start()
+ fromLockscreenTransitionInteractor.start()
+
+ fromDreamingTransitionInteractor =
+ FromDreamingTransitionInteractor(
+ scope = testScope,
+ keyguardInteractor = KeyguardInteractor(keyguardRepository),
+ keyguardTransitionRepository = mockTransitionRepository,
+ keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository),
+ )
+ fromDreamingTransitionInteractor.start()
}
@Test
+ fun `DREAMING to LOCKSCREEN`() =
+ testScope.runTest {
+ // GIVEN a device is dreaming and occluded
+ keyguardRepository.setDreamingWithOverlay(true)
+ keyguardRepository.setKeyguardOccluded(true)
+ runCurrent()
+
+ // GIVEN a prior transition has run to DREAMING
+ runner.startTransition(
+ testScope,
+ TransitionInfo(
+ ownerName = "",
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.DREAMING,
+ animator =
+ ValueAnimator().apply {
+ duration = 10
+ interpolator = Interpolators.LINEAR
+ },
+ )
+ )
+ runCurrent()
+ reset(mockTransitionRepository)
+
+ // WHEN doze is complete
+ keyguardRepository.setDozeTransitionModel(
+ DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH)
+ )
+ // AND dreaming has stopped
+ keyguardRepository.setDreamingWithOverlay(false)
+ // AND occluded has stopped
+ keyguardRepository.setKeyguardOccluded(false)
+ runCurrent()
+
+ val info =
+ withArgCaptor<TransitionInfo> {
+ verify(mockTransitionRepository).startTransition(capture())
+ }
+ // THEN a transition to BOUNCER should occur
+ assertThat(info.ownerName).isEqualTo("FromDreamingTransitionInteractor")
+ assertThat(info.from).isEqualTo(KeyguardState.DREAMING)
+ assertThat(info.to).isEqualTo(KeyguardState.LOCKSCREEN)
+ assertThat(info.animator).isNotNull()
+
+ coroutineContext.cancelChildren()
+ }
+
+ @Test
fun `LOCKSCREEN to BOUNCER via bouncer showing call`() =
testScope.runTest {
// GIVEN a device that has at least woken up
keyguardRepository.setWakefulnessModel(startingToWake())
runCurrent()
- // GIVEN a transition has run to LOCKSCREEN
+ // GIVEN a prior transition has run to LOCKSCREEN
runner.startTransition(
testScope,
TransitionInfo(
@@ -122,7 +182,7 @@
verify(mockTransitionRepository).startTransition(capture())
}
// THEN a transition to BOUNCER should occur
- assertThat(info.ownerName).isEqualTo("LockscreenBouncerTransitionInteractor")
+ assertThat(info.ownerName).isEqualTo("FromLockscreenTransitionInteractor")
assertThat(info.from).isEqualTo(KeyguardState.LOCKSCREEN)
assertThat(info.to).isEqualTo(KeyguardState.BOUNCER)
assertThat(info.animator).isNotNull()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
index 81950f1..5571663 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
@@ -19,8 +19,9 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE
+import com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.DreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
+import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.AnimationParams
import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -28,6 +29,8 @@
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.DREAM_OVERLAY_ALPHA
import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.DREAM_OVERLAY_TRANSLATION_Y
+import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.LOCKSCREEN_ALPHA
+import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.LOCKSCREEN_TRANSLATION_Y
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -65,10 +68,9 @@
repository.sendTransitionStep(step(0.5f))
repository.sendTransitionStep(step(1f))
- // Only two values should be present, since the dream overlay runs for a small fraction
- // of
- // the overall animation time
- assertThat(values.size).isEqualTo(2)
+ // Only 3 values should be present, since the dream overlay runs for a small fraction
+ // of the overall animation time
+ assertThat(values.size).isEqualTo(3)
assertThat(values[0])
.isEqualTo(
EMPHASIZED_ACCELERATE.getInterpolation(
@@ -81,6 +83,12 @@
animValue(0.3f, DREAM_OVERLAY_TRANSLATION_Y)
) * pixels
)
+ assertThat(values[2])
+ .isEqualTo(
+ EMPHASIZED_ACCELERATE.getInterpolation(
+ animValue(0.5f, DREAM_OVERLAY_TRANSLATION_Y)
+ ) * pixels
+ )
job.cancel()
}
@@ -98,8 +106,7 @@
repository.sendTransitionStep(step(1f))
// Only two values should be present, since the dream overlay runs for a small fraction
- // of
- // the overall animation time
+ // of the overall animation time
assertThat(values.size).isEqualTo(2)
assertThat(values[0]).isEqualTo(1f - animValue(0f, DREAM_OVERLAY_ALPHA))
assertThat(values[1]).isEqualTo(1f - animValue(0.1f, DREAM_OVERLAY_ALPHA))
@@ -107,6 +114,77 @@
job.cancel()
}
+ @Test
+ fun lockscreenFadeIn() =
+ runTest(UnconfinedTestDispatcher()) {
+ val values = mutableListOf<Float>()
+
+ val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this)
+
+ repository.sendTransitionStep(step(0f))
+ repository.sendTransitionStep(step(0.1f))
+ // Should start running here...
+ repository.sendTransitionStep(step(0.2f))
+ repository.sendTransitionStep(step(0.3f))
+ // ...up to here
+ repository.sendTransitionStep(step(1f))
+
+ // Only two values should be present, since the dream overlay runs for a small fraction
+ // of the overall animation time
+ assertThat(values.size).isEqualTo(2)
+ assertThat(values[0]).isEqualTo(animValue(0.2f, LOCKSCREEN_ALPHA))
+ assertThat(values[1]).isEqualTo(animValue(0.3f, LOCKSCREEN_ALPHA))
+
+ job.cancel()
+ }
+
+ @Test
+ fun lockscreenTranslationY() =
+ runTest(UnconfinedTestDispatcher()) {
+ val values = mutableListOf<Float>()
+
+ val pixels = 100
+ val job =
+ underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
+
+ repository.sendTransitionStep(step(0f))
+ repository.sendTransitionStep(step(0.3f))
+ repository.sendTransitionStep(step(0.5f))
+ repository.sendTransitionStep(step(1f))
+
+ assertThat(values.size).isEqualTo(4)
+ assertThat(values[0])
+ .isEqualTo(
+ -pixels +
+ EMPHASIZED_DECELERATE.getInterpolation(
+ animValue(0f, LOCKSCREEN_TRANSLATION_Y)
+ ) * pixels
+ )
+ assertThat(values[1])
+ .isEqualTo(
+ -pixels +
+ EMPHASIZED_DECELERATE.getInterpolation(
+ animValue(0.3f, LOCKSCREEN_TRANSLATION_Y)
+ ) * pixels
+ )
+ assertThat(values[2])
+ .isEqualTo(
+ -pixels +
+ EMPHASIZED_DECELERATE.getInterpolation(
+ animValue(0.5f, LOCKSCREEN_TRANSLATION_Y)
+ ) * pixels
+ )
+ assertThat(values[3])
+ .isEqualTo(
+ -pixels +
+ EMPHASIZED_DECELERATE.getInterpolation(
+ animValue(1f, LOCKSCREEN_TRANSLATION_Y)
+ ) * pixels
+ )
+
+ job.cancel()
+ }
+
private fun animValue(stepValue: Float, params: AnimationParams): Float {
val totalDuration = TO_LOCKSCREEN_DURATION
val startValue = (params.startTime / totalDuration).toFloat()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt
new file mode 100644
index 0000000..98d292d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt
@@ -0,0 +1,144 @@
+/*
+ * 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.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.FromOccludedTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.AnimationParams
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel.Companion.LOCKSCREEN_ALPHA
+import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel.Companion.LOCKSCREEN_TRANSLATION_Y
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class OccludedToLockscreenTransitionViewModelTest : SysuiTestCase() {
+ private lateinit var underTest: OccludedToLockscreenTransitionViewModel
+ private lateinit var repository: FakeKeyguardTransitionRepository
+
+ @Before
+ fun setUp() {
+ repository = FakeKeyguardTransitionRepository()
+ val interactor = KeyguardTransitionInteractor(repository)
+ underTest = OccludedToLockscreenTransitionViewModel(interactor)
+ }
+
+ @Test
+ fun lockscreenFadeIn() =
+ runTest(UnconfinedTestDispatcher()) {
+ val values = mutableListOf<Float>()
+
+ val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this)
+
+ repository.sendTransitionStep(step(0f))
+ // Should start running here...
+ repository.sendTransitionStep(step(0.3f))
+ repository.sendTransitionStep(step(0.4f))
+ repository.sendTransitionStep(step(0.5f))
+ // ...up to here
+ repository.sendTransitionStep(step(0.6f))
+ repository.sendTransitionStep(step(1f))
+
+ // Only two values should be present, since the dream overlay runs for a small fraction
+ // of the overall animation time
+ assertThat(values.size).isEqualTo(3)
+ assertThat(values[0]).isEqualTo(animValue(0.3f, LOCKSCREEN_ALPHA))
+ assertThat(values[1]).isEqualTo(animValue(0.4f, LOCKSCREEN_ALPHA))
+ assertThat(values[2]).isEqualTo(animValue(0.5f, LOCKSCREEN_ALPHA))
+
+ job.cancel()
+ }
+
+ @Test
+ fun lockscreenTranslationY() =
+ runTest(UnconfinedTestDispatcher()) {
+ val values = mutableListOf<Float>()
+
+ val pixels = 100
+ val job =
+ underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
+
+ repository.sendTransitionStep(step(0f))
+ repository.sendTransitionStep(step(0.3f))
+ repository.sendTransitionStep(step(0.5f))
+ repository.sendTransitionStep(step(1f))
+
+ assertThat(values.size).isEqualTo(4)
+ assertThat(values[0])
+ .isEqualTo(
+ -pixels +
+ EMPHASIZED_DECELERATE.getInterpolation(
+ animValue(0f, LOCKSCREEN_TRANSLATION_Y)
+ ) * pixels
+ )
+ assertThat(values[1])
+ .isEqualTo(
+ -pixels +
+ EMPHASIZED_DECELERATE.getInterpolation(
+ animValue(0.3f, LOCKSCREEN_TRANSLATION_Y)
+ ) * pixels
+ )
+ assertThat(values[2])
+ .isEqualTo(
+ -pixels +
+ EMPHASIZED_DECELERATE.getInterpolation(
+ animValue(0.5f, LOCKSCREEN_TRANSLATION_Y)
+ ) * pixels
+ )
+ assertThat(values[3])
+ .isEqualTo(
+ -pixels +
+ EMPHASIZED_DECELERATE.getInterpolation(
+ animValue(1f, LOCKSCREEN_TRANSLATION_Y)
+ ) * pixels
+ )
+
+ job.cancel()
+ }
+
+ private fun animValue(stepValue: Float, params: AnimationParams): Float {
+ val totalDuration = TO_LOCKSCREEN_DURATION
+ val startValue = (params.startTime / totalDuration).toFloat()
+
+ val multiplier = (totalDuration / params.duration).toFloat()
+ return (stepValue - startValue) * multiplier
+ }
+
+ private fun step(value: Float): TransitionStep {
+ return TransitionStep(
+ from = KeyguardState.OCCLUDED,
+ to = KeyguardState.LOCKSCREEN,
+ value = value,
+ transitionState = TransitionState.RUNNING,
+ ownerName = "OccludedToLockscreenTransitionViewModelTest"
+ )
+ }
+}
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 07a3109..9c4e849 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
@@ -66,7 +66,7 @@
wakeLockBuilder,
systemClock,
) {
- override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
+ override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) {
// Just bypass the animation in tests
onAnimationEnd.run()
}
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 03ba3d3..cefc742 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
@@ -98,6 +98,7 @@
fun setUp() {
MockitoAnnotations.initMocks(this)
whenever(mediaTttFlags.isMediaTttEnabled()).thenReturn(true)
+ whenever(mediaTttFlags.isMediaTttReceiverSuccessRippleEnabled()).thenReturn(true)
fakeAppIconDrawable = context.getDrawable(R.drawable.ic_cake)!!
whenever(packageManager.getApplicationIcon(PACKAGE_NAME)).thenReturn(fakeAppIconDrawable)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java
index 17eb6e2..bbd62c7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java
@@ -147,8 +147,8 @@
setUserProfiles(0);
setShowUserVisibleJobs(true);
- UserVisibleJobSummary j1 = new UserVisibleJobSummary(0, 0, "pkg1", 0);
- UserVisibleJobSummary j2 = new UserVisibleJobSummary(1, 0, "pkg2", 1);
+ UserVisibleJobSummary j1 = new UserVisibleJobSummary(0, 0, "pkg1", null, 0);
+ UserVisibleJobSummary j2 = new UserVisibleJobSummary(1, 0, "pkg2", null, 1);
Assert.assertEquals(0, mFmc.getNumRunningPackages());
mIUserVisibleJobObserver.onUserVisibleJobStateChanged(j1, true);
Assert.assertEquals(1, mFmc.getNumRunningPackages());
@@ -167,8 +167,8 @@
Binder b1 = new Binder();
Binder b2 = new Binder();
- UserVisibleJobSummary j1 = new UserVisibleJobSummary(0, 0, "pkg1", 0);
- UserVisibleJobSummary j3 = new UserVisibleJobSummary(1, 0, "pkg3", 1);
+ UserVisibleJobSummary j1 = new UserVisibleJobSummary(0, 0, "pkg1", null, 0);
+ UserVisibleJobSummary j3 = new UserVisibleJobSummary(1, 0, "pkg3", null, 1);
Assert.assertEquals(0, mFmc.getNumRunningPackages());
mIForegroundServiceObserver.onForegroundStateChanged(b1, "pkg1", 0, true);
Assert.assertEquals(1, mFmc.getNumRunningPackages());
@@ -359,8 +359,8 @@
// pkg1 has only job
// pkg2 has both job and fgs
// pkg3 has only fgs
- UserVisibleJobSummary j1 = new UserVisibleJobSummary(0, 0, "pkg1", 0);
- UserVisibleJobSummary j2 = new UserVisibleJobSummary(1, 0, "pkg2", 1);
+ UserVisibleJobSummary j1 = new UserVisibleJobSummary(0, 0, "pkg1", null, 0);
+ UserVisibleJobSummary j2 = new UserVisibleJobSummary(1, 0, "pkg2", null, 1);
Binder b2 = new Binder();
Binder b3 = new Binder();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationControllerTest.kt
new file mode 100644
index 0000000..db6fc13
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationControllerTest.kt
@@ -0,0 +1,141 @@
+/*
+ * 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.shade
+
+import android.testing.AndroidTestingRunner
+import android.view.View
+import android.view.ViewGroup
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.StatusBarState.KEYGUARD
+import com.android.systemui.statusbar.StatusBarState.SHADE
+import com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+/**
+ * Translates items away/towards the hinge when the device is opened/closed. This is controlled by
+ * the set of ids, which also dictact which direction to move and when, via a filter fn.
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class NotificationPanelUnfoldAnimationControllerTest : SysuiTestCase() {
+
+ @Mock private lateinit var progressProvider: NaturalRotationUnfoldProgressProvider
+
+ @Captor private lateinit var progressListenerCaptor: ArgumentCaptor<TransitionProgressListener>
+
+ @Mock private lateinit var parent: ViewGroup
+
+ @Mock private lateinit var statusBarStateController: StatusBarStateController
+
+ private lateinit var underTest: NotificationPanelUnfoldAnimationController
+ private lateinit var progressListener: TransitionProgressListener
+ private var xTranslationMax = 0f
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+
+ xTranslationMax =
+ context.resources.getDimensionPixelSize(R.dimen.notification_side_paddings).toFloat()
+
+ underTest =
+ NotificationPanelUnfoldAnimationController(
+ context,
+ statusBarStateController,
+ progressProvider
+ )
+ underTest.setup(parent)
+
+ verify(progressProvider).addCallback(capture(progressListenerCaptor))
+ progressListener = progressListenerCaptor.value
+ }
+
+ @Test
+ fun whenInKeyguardState_viewDoesNotMove() {
+ whenever(statusBarStateController.getState()).thenReturn(KEYGUARD)
+
+ val view = View(context)
+ whenever(parent.findViewById<View>(R.id.quick_settings_panel)).thenReturn(view)
+
+ progressListener.onTransitionStarted()
+ assertThat(view.translationX).isZero()
+
+ progressListener.onTransitionProgress(0f)
+ assertThat(view.translationX).isZero()
+
+ progressListener.onTransitionProgress(0.5f)
+ assertThat(view.translationX).isZero()
+
+ progressListener.onTransitionFinished()
+ assertThat(view.translationX).isZero()
+ }
+
+ @Test
+ fun whenInShadeState_viewDoesMove() {
+ whenever(statusBarStateController.getState()).thenReturn(SHADE)
+
+ val view = View(context)
+ whenever(parent.findViewById<View>(R.id.quick_settings_panel)).thenReturn(view)
+
+ progressListener.onTransitionStarted()
+ assertThat(view.translationX).isZero()
+
+ progressListener.onTransitionProgress(0f)
+ assertThat(view.translationX).isEqualTo(xTranslationMax)
+
+ progressListener.onTransitionProgress(0.5f)
+ assertThat(view.translationX).isEqualTo(0.5f * xTranslationMax)
+
+ progressListener.onTransitionFinished()
+ assertThat(view.translationX).isZero()
+ }
+
+ @Test
+ fun whenInShadeLockedState_viewDoesMove() {
+ whenever(statusBarStateController.getState()).thenReturn(SHADE_LOCKED)
+
+ val view = View(context)
+ whenever(parent.findViewById<View>(R.id.quick_settings_panel)).thenReturn(view)
+
+ progressListener.onTransitionStarted()
+ assertThat(view.translationX).isZero()
+
+ progressListener.onTransitionProgress(0f)
+ assertThat(view.translationX).isEqualTo(xTranslationMax)
+
+ progressListener.onTransitionProgress(0.5f)
+ assertThat(view.translationX).isEqualTo(0.5f * xTranslationMax)
+
+ progressListener.onTransitionFinished()
+ assertThat(view.translationX).isZero()
+ }
+}
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 4843c76..53ab19c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
@@ -105,7 +105,10 @@
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel;
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel;
+import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel;
import com.android.systemui.media.controls.pipeline.MediaDataManager;
import com.android.systemui.media.controls.ui.KeyguardMediaController;
import com.android.systemui.media.controls.ui.MediaHierarchyManager;
@@ -187,6 +190,8 @@
import java.util.List;
import java.util.Optional;
+import kotlinx.coroutines.CoroutineDispatcher;
+
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
@@ -288,6 +293,10 @@
@Mock private KeyguardBottomAreaViewModel mKeyguardBottomAreaViewModel;
@Mock private KeyguardBottomAreaInteractor mKeyguardBottomAreaInteractor;
@Mock private AlternateBouncerInteractor mAlternateBouncerInteractor;
+ @Mock private DreamingToLockscreenTransitionViewModel mDreamingToLockscreenTransitionViewModel;
+ @Mock private OccludedToLockscreenTransitionViewModel mOccludedToLockscreenTransitionViewModel;
+ @Mock private KeyguardTransitionInteractor mKeyguardTransitionInteractor;
+ @Mock private CoroutineDispatcher mMainDispatcher;
@Mock private MotionEvent mDownMotionEvent;
@Captor
private ArgumentCaptor<NotificationStackScrollLayout.OnEmptySpaceClickListener>
@@ -505,6 +514,10 @@
mKeyguardBottomAreaViewModel,
mKeyguardBottomAreaInteractor,
mAlternateBouncerInteractor,
+ mDreamingToLockscreenTransitionViewModel,
+ mOccludedToLockscreenTransitionViewModel,
+ mMainDispatcher,
+ mKeyguardTransitionInteractor,
mDumpManager);
mNotificationPanelViewController.initDependencies(
mCentralSurfaces,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt
index 43c6942..3e769e9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt
@@ -21,6 +21,7 @@
import android.provider.Settings.Secure.DOZE_TAP_SCREEN_GESTURE
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper
+import android.os.PowerManager
import android.view.MotionEvent
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -36,9 +37,9 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.anyLong
-import org.mockito.ArgumentMatchers.anyObject
import org.mockito.ArgumentMatchers.anyString
import org.mockito.Mock
import org.mockito.Mockito.never
@@ -106,7 +107,8 @@
underTest.onSingleTapUp(upEv)
// THEN wake up device if dozing
- verify(centralSurfaces).wakeUpIfDozing(anyLong(), anyObject(), anyString())
+ verify(centralSurfaces).wakeUpIfDozing(
+ anyLong(), any(), anyString(), eq(PowerManager.WAKE_REASON_TAP))
}
@Test
@@ -125,7 +127,8 @@
underTest.onDoubleTapEvent(upEv)
// THEN wake up device if dozing
- verify(centralSurfaces).wakeUpIfDozing(anyLong(), anyObject(), anyString())
+ verify(centralSurfaces).wakeUpIfDozing(
+ anyLong(), any(), anyString(), eq(PowerManager.WAKE_REASON_TAP))
}
@Test
@@ -156,7 +159,8 @@
underTest.onSingleTapUp(upEv)
// THEN the device doesn't wake up
- verify(centralSurfaces, never()).wakeUpIfDozing(anyLong(), anyObject(), anyString())
+ verify(centralSurfaces, never()).wakeUpIfDozing(
+ anyLong(), any(), anyString(), anyInt())
}
@Test
@@ -203,7 +207,8 @@
underTest.onDoubleTapEvent(upEv)
// THEN the device doesn't wake up
- verify(centralSurfaces, never()).wakeUpIfDozing(anyLong(), anyObject(), anyString())
+ verify(centralSurfaces, never()).wakeUpIfDozing(
+ anyLong(), any(), anyString(), anyInt())
}
@Test
@@ -222,7 +227,8 @@
underTest.onSingleTapUp(upEv)
// THEN the device doesn't wake up
- verify(centralSurfaces, never()).wakeUpIfDozing(anyLong(), anyObject(), anyString())
+ verify(centralSurfaces, never()).wakeUpIfDozing(
+ anyLong(), any(), anyString(), anyInt())
}
@Test
@@ -241,7 +247,8 @@
underTest.onDoubleTapEvent(upEv)
// THEN the device doesn't wake up
- verify(centralSurfaces, never()).wakeUpIfDozing(anyLong(), anyObject(), anyString())
+ verify(centralSurfaces, never()).wakeUpIfDozing(
+ anyLong(), any(), anyString(), anyInt())
}
fun updateSettings() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LightRevealScrimTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LightRevealScrimTest.kt
index 97fe25d..d3befb4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LightRevealScrimTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LightRevealScrimTest.kt
@@ -20,6 +20,7 @@
import android.view.View
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
@@ -36,7 +37,7 @@
@Before
fun setUp() {
- scrim = LightRevealScrim(context, null)
+ scrim = LightRevealScrim(context, null, DEFAULT_WIDTH, DEFAULT_HEIGHT)
scrim.isScrimOpaqueChangedListener = Consumer { opaque ->
isOpaque = opaque
}
@@ -63,4 +64,25 @@
scrim.revealAmount = 0.5f
assertFalse("Scrim is opaque even though it's revealed", scrim.isScrimOpaque)
}
+
+ @Test
+ fun testBeforeOnMeasure_defaultDimensions() {
+ assertThat(scrim.viewWidth).isEqualTo(DEFAULT_WIDTH)
+ assertThat(scrim.viewHeight).isEqualTo(DEFAULT_HEIGHT)
+ }
+
+ @Test
+ fun testAfterOnMeasure_measuredDimensions() {
+ scrim.measure(/* widthMeasureSpec= */ exact(1), /* heightMeasureSpec= */ exact(2))
+
+ assertThat(scrim.viewWidth).isEqualTo(1)
+ assertThat(scrim.viewHeight).isEqualTo(2)
+ }
+
+ private fun exact(value: Int) = View.MeasureSpec.makeMeasureSpec(value, View.MeasureSpec.EXACTLY)
+
+ private companion object {
+ private const val DEFAULT_WIDTH = 42
+ private const val DEFAULT_HEIGHT = 24
+ }
}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/TargetSdkResolverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/TargetSdkResolverTest.kt
index 8275c0c..9b3626b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/TargetSdkResolverTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/TargetSdkResolverTest.kt
@@ -127,6 +127,6 @@
NotificationManager.IMPORTANCE_DEFAULT,
null, null,
null, null, null, true, 0, false, -1, false, null, null, false, false,
- false, null, 0, false)
+ false, null, 0, false, 0)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index 1ce460c..3a1f9b7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -31,6 +31,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
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.clearInvocations;
@@ -177,6 +178,8 @@
import com.android.wm.shell.bubbles.Bubbles;
import com.android.wm.shell.startingsurface.StartingSurface;
+import dagger.Lazy;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -189,8 +192,6 @@
import java.io.PrintWriter;
import java.util.Optional;
-import dagger.Lazy;
-
@SmallTest
@RunWith(AndroidTestingRunner.class)
@RunWithLooper(setAsMainLooper = true)
@@ -306,6 +307,7 @@
@Mock private ViewRootImpl mViewRootImpl;
@Mock private WindowOnBackInvokedDispatcher mOnBackInvokedDispatcher;
@Captor private ArgumentCaptor<OnBackInvokedCallback> mOnBackInvokedCallback;
+ @Mock IPowerManager mPowerManagerService;
private ShadeController mShadeController;
private final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
@@ -319,9 +321,8 @@
public void setup() throws Exception {
MockitoAnnotations.initMocks(this);
- IPowerManager powerManagerService = mock(IPowerManager.class);
IThermalService thermalService = mock(IThermalService.class);
- mPowerManager = new PowerManager(mContext, powerManagerService, thermalService,
+ mPowerManager = new PowerManager(mContext, mPowerManagerService, thermalService,
Handler.createAsync(Looper.myLooper()));
mNotificationInterruptStateProvider =
@@ -363,7 +364,7 @@
when(mStackScrollerController.getView()).thenReturn(mStackScroller);
when(mStackScroller.generateLayoutParams(any())).thenReturn(new LayoutParams(0, 0));
when(mNotificationPanelView.getLayoutParams()).thenReturn(new LayoutParams(0, 0));
- when(powerManagerService.isInteractive()).thenReturn(true);
+ when(mPowerManagerService.isInteractive()).thenReturn(true);
when(mStackScroller.getActivatedChild()).thenReturn(null);
doAnswer(invocation -> {
@@ -1190,6 +1191,34 @@
verify(mStatusBarStateController).setState(SHADE);
}
+ @Test
+ public void dozing_wakeUp() throws RemoteException {
+ // GIVEN can wakeup when dozing & is dozing
+ when(mScreenOffAnimationController.allowWakeUpIfDozing()).thenReturn(true);
+ setDozing(true);
+
+ // WHEN wakeup is requested
+ final int wakeReason = PowerManager.WAKE_REASON_TAP;
+ mCentralSurfaces.wakeUpIfDozing(0, null, "", wakeReason);
+
+ // THEN power manager receives wakeup
+ verify(mPowerManagerService).wakeUp(eq(0L), eq(wakeReason), anyString(), anyString());
+ }
+
+ @Test
+ public void notDozing_noWakeUp() throws RemoteException {
+ // GIVEN can wakeup when dozing and NOT dozing
+ when(mScreenOffAnimationController.allowWakeUpIfDozing()).thenReturn(true);
+ setDozing(false);
+
+ // WHEN wakeup is requested
+ final int wakeReason = PowerManager.WAKE_REASON_TAP;
+ mCentralSurfaces.wakeUpIfDozing(0, null, "", wakeReason);
+
+ // THEN power manager receives wakeup
+ verify(mPowerManagerService, never()).wakeUp(anyLong(), anyInt(), anyString(), anyString());
+ }
+
/**
* Configures the appropriate mocks and then calls {@link CentralSurfacesImpl#updateIsKeyguard}
* to reconfigure the keyguard to reflect the requested showing/occluded states.
@@ -1226,6 +1255,13 @@
states);
}
+ private void setDozing(boolean isDozing) {
+ ArgumentCaptor<StatusBarStateController.StateListener> callbackCaptor =
+ ArgumentCaptor.forClass(StatusBarStateController.StateListener.class);
+ verify(mStatusBarStateController).addCallback(callbackCaptor.capture(), anyInt());
+ callbackCaptor.getValue().onDozingChanged(isDozing);
+ }
+
public static class TestableNotificationInterruptStateProviderImpl extends
NotificationInterruptStateProviderImpl {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModelTest.kt
new file mode 100644
index 0000000..f822ba0
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModelTest.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.model
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.log.table.TableRowLogger
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_ACTIVITY_DIRECTION
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_CARRIER_NETWORK_CHANGE
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_CDMA_LEVEL
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_CONNECTION_STATE
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_EMERGENCY
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_IS_GSM
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_OPERATOR
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_PRIMARY_LEVEL
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_RESOLVED_NETWORK_TYPE
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_ROAMING
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+@SmallTest
+class MobileConnectionModelTest : SysuiTestCase() {
+
+ @Test
+ fun `log diff - initial log contains all columns`() {
+ val logger = TestLogger()
+ val connection = MobileConnectionModel()
+
+ connection.logFull(logger)
+
+ assertThat(logger.changes)
+ .contains(Pair(COL_EMERGENCY, connection.isEmergencyOnly.toString()))
+ assertThat(logger.changes).contains(Pair(COL_ROAMING, connection.isRoaming.toString()))
+ assertThat(logger.changes)
+ .contains(Pair(COL_OPERATOR, connection.operatorAlphaShort.toString()))
+ assertThat(logger.changes).contains(Pair(COL_IS_GSM, connection.isGsm.toString()))
+ assertThat(logger.changes).contains(Pair(COL_CDMA_LEVEL, connection.cdmaLevel.toString()))
+ assertThat(logger.changes)
+ .contains(Pair(COL_PRIMARY_LEVEL, connection.primaryLevel.toString()))
+ assertThat(logger.changes)
+ .contains(Pair(COL_CONNECTION_STATE, connection.dataConnectionState.toString()))
+ assertThat(logger.changes)
+ .contains(Pair(COL_ACTIVITY_DIRECTION, connection.dataActivityDirection.toString()))
+ assertThat(logger.changes)
+ .contains(
+ Pair(COL_CARRIER_NETWORK_CHANGE, connection.carrierNetworkChangeActive.toString())
+ )
+ assertThat(logger.changes)
+ .contains(Pair(COL_RESOLVED_NETWORK_TYPE, connection.resolvedNetworkType.toString()))
+ }
+
+ @Test
+ fun `log diff - primary level changes - only level is logged`() {
+ val logger = TestLogger()
+ val connectionOld = MobileConnectionModel(primaryLevel = 1)
+
+ val connectionNew = MobileConnectionModel(primaryLevel = 2)
+
+ connectionNew.logDiffs(connectionOld, logger)
+
+ assertThat(logger.changes).isEqualTo(listOf(Pair(COL_PRIMARY_LEVEL, "2")))
+ }
+
+ private class TestLogger : TableRowLogger {
+ val changes = mutableListOf<Pair<String, String>>()
+
+ override fun logChange(columnName: String, value: String?) {
+ changes.add(Pair(columnName, value.toString()))
+ }
+
+ override fun logChange(columnName: String, value: Int) {
+ changes.add(Pair(columnName, value.toString()))
+ }
+
+ override fun logChange(columnName: String, value: Boolean) {
+ changes.add(Pair(columnName, value.toString()))
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
index 7b9929d..d6a9ee3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
@@ -16,11 +16,16 @@
package com.android.systemui.statusbar.pipeline.mobile.data.repository
+import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
import kotlinx.coroutines.flow.MutableStateFlow
// TODO(b/261632894): remove this in favor of the real impl or DemoMobileConnectionRepository
-class FakeMobileConnectionRepository(override val subId: Int) : MobileConnectionRepository {
+class FakeMobileConnectionRepository(
+ override val subId: Int,
+ override val tableLogBuffer: TableLogBuffer,
+) : MobileConnectionRepository {
private val _connectionInfo = MutableStateFlow(MobileConnectionModel())
override val connectionInfo = _connectionInfo
@@ -32,6 +37,9 @@
override val cdmaRoaming = MutableStateFlow(false)
+ override val networkName =
+ MutableStateFlow<NetworkNameModel>(NetworkNameModel.Default("default"))
+
fun setConnectionInfo(model: MobileConnectionModel) {
_connectionInfo.value = model
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
index 04d3cdd..7f93328 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
@@ -22,14 +22,17 @@
import com.android.settingslib.SignalIcon
import com.android.settingslib.mobile.MobileMappings
import com.android.settingslib.mobile.TelephonyIcons
+import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
import kotlinx.coroutines.flow.MutableStateFlow
// TODO(b/261632894): remove this in favor of the real impl or DemoMobileConnectionsRepository
-class FakeMobileConnectionsRepository(mobileMappings: MobileMappingsProxy) :
- MobileConnectionsRepository {
+class FakeMobileConnectionsRepository(
+ mobileMappings: MobileMappingsProxy,
+ val tableLogBuffer: TableLogBuffer,
+) : MobileConnectionsRepository {
val GSM_KEY = mobileMappings.toIconKey(GSM)
val LTE_KEY = mobileMappings.toIconKey(LTE)
val UMTS_KEY = mobileMappings.toIconKey(UMTS)
@@ -63,7 +66,7 @@
private val subIdRepos = mutableMapOf<Int, MobileConnectionRepository>()
override fun getRepoForSubId(subId: Int): MobileConnectionRepository {
return subIdRepos[subId]
- ?: FakeMobileConnectionRepository(subId).also { subIdRepos[subId] = it }
+ ?: FakeMobileConnectionRepository(subId, tableLogBuffer).also { subIdRepos[subId] = it }
}
private val _globalMobileDataSettingChangedEvent = MutableStateFlow(Unit)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt
index 18ae90d..5d377a8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt
@@ -24,6 +24,8 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.demomode.DemoMode
import com.android.systemui.demomode.DemoModeController
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.log.table.TableLogBufferFactory
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.DemoMobileConnectionsRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.DemoModeMobileConnectionDataSource
@@ -37,6 +39,7 @@
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.settings.FakeSettings
+import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -69,12 +72,14 @@
private lateinit var realRepo: MobileConnectionsRepositoryImpl
private lateinit var demoRepo: DemoMobileConnectionsRepository
private lateinit var mockDataSource: DemoModeMobileConnectionDataSource
+ private lateinit var logFactory: TableLogBufferFactory
@Mock private lateinit var connectivityManager: ConnectivityManager
@Mock private lateinit var subscriptionManager: SubscriptionManager
@Mock private lateinit var telephonyManager: TelephonyManager
@Mock private lateinit var logger: ConnectivityPipelineLogger
@Mock private lateinit var demoModeController: DemoModeController
+ @Mock private lateinit var dumpManager: DumpManager
private val globalSettings = FakeSettings()
private val fakeNetworkEventsFlow = MutableStateFlow<FakeNetworkEventModel?>(null)
@@ -86,6 +91,8 @@
fun setUp() {
MockitoAnnotations.initMocks(this)
+ logFactory = TableLogBufferFactory(dumpManager, FakeSystemClock())
+
// Never start in demo mode
whenever(demoModeController.isInDemoMode).thenReturn(false)
@@ -114,6 +121,7 @@
dataSource = mockDataSource,
scope = scope,
context = context,
+ logFactory = logFactory,
)
underTest =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt
index b2423da..2102085 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt
@@ -18,15 +18,20 @@
import android.telephony.Annotation
import android.telephony.TelephonyManager
+import android.telephony.TelephonyManager.DATA_ACTIVITY_NONE
import androidx.test.filters.SmallTest
import com.android.settingslib.SignalIcon
import com.android.settingslib.mobile.TelephonyIcons
import com.android.systemui.SysuiTestCase
+import com.android.systemui.log.table.TableLogBufferFactory
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
+import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel
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.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
@@ -51,6 +56,9 @@
@RunWith(Parameterized::class)
internal class DemoMobileConnectionParameterizedTest(private val testCase: TestCase) :
SysuiTestCase() {
+
+ private val logFactory = TableLogBufferFactory(mock(), FakeSystemClock())
+
private val testDispatcher = UnconfinedTestDispatcher()
private val testScope = TestScope(testDispatcher)
@@ -73,6 +81,7 @@
dataSource = mockDataSource,
scope = testScope.backgroundScope,
context = context,
+ logFactory = logFactory,
)
connectionsRepo.startProcessingCommands()
@@ -96,6 +105,7 @@
activity = testCase.activity,
carrierNetworkChange = testCase.carrierNetworkChange,
roaming = testCase.roaming,
+ name = "demo name",
)
fakeNetworkEventFlow.value = networkModel
@@ -114,10 +124,12 @@
assertThat(conn.subId).isEqualTo(model.subId)
assertThat(connectionInfo.cdmaLevel).isEqualTo(model.level)
assertThat(connectionInfo.primaryLevel).isEqualTo(model.level)
- assertThat(connectionInfo.dataActivityDirection).isEqualTo(model.activity)
+ assertThat(connectionInfo.dataActivityDirection)
+ .isEqualTo((model.activity ?: DATA_ACTIVITY_NONE).toMobileDataActivityModel())
assertThat(connectionInfo.carrierNetworkChangeActive)
.isEqualTo(model.carrierNetworkChange)
assertThat(connectionInfo.isRoaming).isEqualTo(model.roaming)
+ assertThat(conn.networkName.value).isEqualTo(NetworkNameModel.Derived(model.name))
// TODO(b/261029387): check these once we start handling them
assertThat(connectionInfo.isEmergencyOnly).isFalse()
@@ -141,6 +153,7 @@
@Annotation.DataActivityType val activity: Int,
val carrierNetworkChange: Boolean,
val roaming: Boolean,
+ val name: String,
) {
override fun toString(): String {
return "INPUT(level=$level, " +
@@ -150,7 +163,8 @@
"inflateStrength=$inflateStrength, " +
"activity=$activity, " +
"carrierNetworkChange=$carrierNetworkChange, " +
- "roaming=$roaming)"
+ "roaming=$roaming, " +
+ "name=$name)"
}
// Convenience for iterating test data and creating new cases
@@ -163,6 +177,7 @@
@Annotation.DataActivityType activity: Int? = null,
carrierNetworkChange: Boolean? = null,
roaming: Boolean? = null,
+ name: String? = null,
): TestCase =
TestCase(
level = level ?: this.level,
@@ -173,6 +188,7 @@
activity = activity ?: this.activity,
carrierNetworkChange = carrierNetworkChange ?: this.carrierNetworkChange,
roaming = roaming ?: this.roaming,
+ name = name ?: this.name,
)
}
@@ -201,6 +217,7 @@
private val carrierNetworkChange = booleanList
// false first so the base case doesn't have roaming set (more common)
private val roaming = listOf(false, true)
+ private val names = listOf("name 1", "name 2")
@Parameters(name = "{0}") @JvmStatic fun data() = testData()
@@ -236,6 +253,7 @@
activity.first(),
carrierNetworkChange.first(),
roaming.first(),
+ names.first(),
)
val tail =
@@ -246,7 +264,8 @@
inflateStrength.map { baseCase.modifiedBy(inflateStrength = it) },
activity.map { baseCase.modifiedBy(activity = it) },
carrierNetworkChange.map { baseCase.modifiedBy(carrierNetworkChange = it) },
- roaming.map { baseCase.modifiedBy(roaming = it) }
+ roaming.map { baseCase.modifiedBy(roaming = it) },
+ names.map { baseCase.modifiedBy(name = it) },
)
.flatten()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt
index e4f29e2..cdbe75e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt
@@ -17,18 +17,24 @@
package com.android.systemui.statusbar.pipeline.mobile.data.repository.demo
import android.telephony.TelephonyManager.DATA_ACTIVITY_INOUT
+import android.telephony.TelephonyManager.DATA_ACTIVITY_NONE
import android.telephony.TelephonyManager.UNKNOWN_CARRIER_ID
import androidx.test.filters.SmallTest
import com.android.settingslib.SignalIcon
import com.android.settingslib.mobile.TelephonyIcons.THREE_G
import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.log.table.TableLogBufferFactory
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled
+import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel
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 junit.framework.Assert
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -44,6 +50,9 @@
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
class DemoMobileConnectionsRepositoryTest : SysuiTestCase() {
+ private val dumpManager: DumpManager = mock()
+ private val logFactory = TableLogBufferFactory(dumpManager, FakeSystemClock())
+
private val testDispatcher = UnconfinedTestDispatcher()
private val testScope = TestScope(testDispatcher)
@@ -65,6 +74,7 @@
dataSource = mockDataSource,
scope = testScope.backgroundScope,
context = context,
+ logFactory = logFactory,
)
underTest.startProcessingCommands()
@@ -289,10 +299,12 @@
assertThat(conn.subId).isEqualTo(model.subId)
assertThat(connectionInfo.cdmaLevel).isEqualTo(model.level)
assertThat(connectionInfo.primaryLevel).isEqualTo(model.level)
- assertThat(connectionInfo.dataActivityDirection).isEqualTo(model.activity)
+ assertThat(connectionInfo.dataActivityDirection)
+ .isEqualTo((model.activity ?: DATA_ACTIVITY_NONE).toMobileDataActivityModel())
assertThat(connectionInfo.carrierNetworkChangeActive)
.isEqualTo(model.carrierNetworkChange)
assertThat(connectionInfo.isRoaming).isEqualTo(model.roaming)
+ assertThat(conn.networkName.value).isEqualTo(NetworkNameModel.Derived(model.name))
// TODO(b/261029387) check these once we start handling them
assertThat(connectionInfo.isEmergencyOnly).isFalse()
@@ -325,4 +337,5 @@
activity = activity,
carrierNetworkChange = carrierNetworkChange,
roaming = roaming,
+ name = "demo name",
)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
index 0b3e5b5..7970443 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod
+import android.content.Intent
import android.os.UserHandle
import android.provider.Settings
import android.telephony.CellSignalStrengthCdma
@@ -23,10 +24,16 @@
import android.telephony.SignalStrength
import android.telephony.SubscriptionInfo
import android.telephony.TelephonyCallback
+import android.telephony.TelephonyCallback.DataActivityListener
import android.telephony.TelephonyCallback.ServiceStateListener
import android.telephony.TelephonyDisplayInfo
import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_LTE_CA
import android.telephony.TelephonyManager
+import android.telephony.TelephonyManager.DATA_ACTIVITY_DORMANT
+import android.telephony.TelephonyManager.DATA_ACTIVITY_IN
+import android.telephony.TelephonyManager.DATA_ACTIVITY_INOUT
+import android.telephony.TelephonyManager.DATA_ACTIVITY_NONE
+import android.telephony.TelephonyManager.DATA_ACTIVITY_OUT
import android.telephony.TelephonyManager.DATA_CONNECTED
import android.telephony.TelephonyManager.DATA_CONNECTING
import android.telephony.TelephonyManager.DATA_DISCONNECTED
@@ -34,18 +41,28 @@
import android.telephony.TelephonyManager.DATA_UNKNOWN
import android.telephony.TelephonyManager.ERI_OFF
import android.telephony.TelephonyManager.ERI_ON
+import android.telephony.TelephonyManager.EXTRA_PLMN
+import android.telephony.TelephonyManager.EXTRA_SHOW_PLMN
+import android.telephony.TelephonyManager.EXTRA_SHOW_SPN
+import android.telephony.TelephonyManager.EXTRA_SPN
+import android.telephony.TelephonyManager.EXTRA_SUBSCRIPTION_ID
import android.telephony.TelephonyManager.NETWORK_TYPE_LTE
import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.OverrideNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.UnknownNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.toNetworkNameModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
+import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.mock
@@ -71,14 +88,15 @@
@SmallTest
class MobileConnectionRepositoryTest : SysuiTestCase() {
private lateinit var underTest: MobileConnectionRepositoryImpl
+ private lateinit var connectionsRepo: FakeMobileConnectionsRepository
@Mock private lateinit var telephonyManager: TelephonyManager
@Mock private lateinit var logger: ConnectivityPipelineLogger
+ @Mock private lateinit var tableLogger: TableLogBuffer
private val scope = CoroutineScope(IMMEDIATE)
private val mobileMappings = FakeMobileMappingsProxy()
private val globalSettings = FakeSettings()
- private val connectionsRepo = FakeMobileConnectionsRepository(mobileMappings)
@Before
fun setUp() {
@@ -86,17 +104,23 @@
globalSettings.userId = UserHandle.USER_ALL
whenever(telephonyManager.subscriptionId).thenReturn(SUB_1_ID)
+ connectionsRepo = FakeMobileConnectionsRepository(mobileMappings, tableLogger)
+
underTest =
MobileConnectionRepositoryImpl(
context,
SUB_1_ID,
+ DEFAULT_NAME,
+ SEP,
telephonyManager,
globalSettings,
+ fakeBroadcastDispatcher,
connectionsRepo.defaultDataSubId,
connectionsRepo.globalMobileDataSettingChangedEvent,
mobileMappings,
IMMEDIATE,
logger,
+ tableLogger,
scope,
)
}
@@ -249,10 +273,11 @@
var latest: MobileConnectionModel? = null
val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
- val callback = getTelephonyCallbackForType<TelephonyCallback.DataActivityListener>()
- callback.onDataActivity(3)
+ val callback = getTelephonyCallbackForType<DataActivityListener>()
+ callback.onDataActivity(DATA_ACTIVITY_INOUT)
- assertThat(latest?.dataActivityDirection).isEqualTo(3)
+ assertThat(latest?.dataActivityDirection)
+ .isEqualTo(DATA_ACTIVITY_INOUT.toMobileDataActivityModel())
job.cancel()
}
@@ -459,6 +484,112 @@
job.cancel()
}
+ @Test
+ fun `activity - updates from callback`() =
+ runBlocking(IMMEDIATE) {
+ var latest: DataActivityModel? = null
+ val job =
+ underTest.connectionInfo.onEach { latest = it.dataActivityDirection }.launchIn(this)
+
+ assertThat(latest)
+ .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = false))
+
+ val cb = getTelephonyCallbackForType<DataActivityListener>()
+ cb.onDataActivity(DATA_ACTIVITY_IN)
+ assertThat(latest)
+ .isEqualTo(DataActivityModel(hasActivityIn = true, hasActivityOut = false))
+
+ cb.onDataActivity(DATA_ACTIVITY_OUT)
+ assertThat(latest)
+ .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = true))
+
+ cb.onDataActivity(DATA_ACTIVITY_INOUT)
+ assertThat(latest)
+ .isEqualTo(DataActivityModel(hasActivityIn = true, hasActivityOut = true))
+
+ cb.onDataActivity(DATA_ACTIVITY_NONE)
+ assertThat(latest)
+ .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = false))
+
+ cb.onDataActivity(DATA_ACTIVITY_DORMANT)
+ assertThat(latest)
+ .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = false))
+
+ cb.onDataActivity(1234)
+ assertThat(latest)
+ .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = false))
+
+ job.cancel()
+ }
+
+ @Test
+ fun `network name - default`() =
+ runBlocking(IMMEDIATE) {
+ var latest: NetworkNameModel? = null
+ val job = underTest.networkName.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isEqualTo(DEFAULT_NAME)
+
+ job.cancel()
+ }
+
+ @Test
+ fun `network name - uses broadcast info - returns derived`() =
+ runBlocking(IMMEDIATE) {
+ var latest: NetworkNameModel? = null
+ val job = underTest.networkName.onEach { latest = it }.launchIn(this)
+
+ val intent = spnIntent()
+
+ fakeBroadcastDispatcher.registeredReceivers.forEach { receiver ->
+ receiver.onReceive(context, intent)
+ }
+
+ assertThat(latest).isEqualTo(intent.toNetworkNameModel(SEP))
+
+ job.cancel()
+ }
+
+ @Test
+ fun `network name - broadcast not for this sub id - returns default`() =
+ runBlocking(IMMEDIATE) {
+ var latest: NetworkNameModel? = null
+ val job = underTest.networkName.onEach { latest = it }.launchIn(this)
+
+ val intent = spnIntent(subId = 101)
+
+ fakeBroadcastDispatcher.registeredReceivers.forEach { receiver ->
+ receiver.onReceive(context, intent)
+ }
+
+ assertThat(latest).isEqualTo(DEFAULT_NAME)
+
+ job.cancel()
+ }
+
+ @Test
+ fun `network name - operatorAlphaShort - tracked`() =
+ runBlocking(IMMEDIATE) {
+ var latest: String? = null
+
+ val job =
+ underTest.connectionInfo.onEach { latest = it.operatorAlphaShort }.launchIn(this)
+
+ val shortName = "short name"
+ val serviceState = ServiceState()
+ serviceState.setOperatorName(
+ /* longName */ "long name",
+ /* shortName */ shortName,
+ /* numeric */ "12345",
+ )
+
+ getTelephonyCallbackForType<ServiceStateListener>().onServiceStateChanged(serviceState)
+
+ assertThat(latest).isEqualTo(shortName)
+
+ job.cancel()
+ }
+
private fun getTelephonyCallbacks(): List<TelephonyCallback> {
val callbackCaptor = argumentCaptor<TelephonyCallback>()
Mockito.verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture())
@@ -484,10 +615,31 @@
return signalStrength
}
+ private fun spnIntent(
+ subId: Int = SUB_1_ID,
+ showSpn: Boolean = true,
+ spn: String = SPN,
+ showPlmn: Boolean = true,
+ plmn: String = PLMN,
+ ): Intent =
+ Intent(TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED).apply {
+ putExtra(EXTRA_SUBSCRIPTION_ID, subId)
+ putExtra(EXTRA_SHOW_SPN, showSpn)
+ putExtra(EXTRA_SPN, spn)
+ putExtra(EXTRA_SHOW_PLMN, showPlmn)
+ putExtra(EXTRA_PLMN, plmn)
+ }
+
companion object {
private val IMMEDIATE = Dispatchers.Main.immediate
private const val SUB_1_ID = 1
private val SUB_1 =
mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) }
+
+ private val DEFAULT_NAME = NetworkNameModel.Default("default name")
+ private const val SEP = "-"
+
+ private const val SPN = "testSpn"
+ private const val PLMN = "testPlmn"
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
index 6d80acb..b8cd7a4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
@@ -34,12 +34,15 @@
import com.android.settingslib.R
import com.android.settingslib.mobile.MobileMappings
import com.android.systemui.SysuiTestCase
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.TableLogBufferFactory
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
+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.settings.FakeSettings
@@ -57,6 +60,7 @@
import org.junit.Before
import org.junit.Test
import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@@ -72,6 +76,7 @@
@Mock private lateinit var subscriptionManager: SubscriptionManager
@Mock private lateinit var telephonyManager: TelephonyManager
@Mock private lateinit var logger: ConnectivityPipelineLogger
+ @Mock private lateinit var logBufferFactory: TableLogBufferFactory
private val mobileMappings = FakeMobileMappingsProxy()
@@ -89,8 +94,13 @@
}
}
+ whenever(logBufferFactory.create(anyString(), anyInt())).thenAnswer { _ ->
+ mock<TableLogBuffer>()
+ }
+
connectionFactory =
MobileConnectionRepositoryImpl.Factory(
+ fakeBroadcastDispatcher,
context = context,
telephonyManager = telephonyManager,
bgDispatcher = IMMEDIATE,
@@ -98,6 +108,7 @@
logger = logger,
mobileMappingsProxy = mobileMappings,
scope = scope,
+ logFactory = logBufferFactory,
)
underTest =
@@ -270,6 +281,32 @@
}
@Test
+ fun `connection repository - log buffer contains sub id in its name`() =
+ runBlocking(IMMEDIATE) {
+ val job = underTest.subscriptions.launchIn(this)
+
+ whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+ .thenReturn(listOf(SUB_1, SUB_2))
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ // Get repos to trigger creation
+ underTest.getRepoForSubId(SUB_1_ID)
+ verify(logBufferFactory)
+ .create(
+ eq(MobileConnectionRepositoryImpl.tableBufferLogName(SUB_1_ID)),
+ anyInt(),
+ )
+ underTest.getRepoForSubId(SUB_2_ID)
+ verify(logBufferFactory)
+ .create(
+ eq(MobileConnectionRepositoryImpl.tableBufferLogName(SUB_2_ID)),
+ anyInt(),
+ )
+
+ job.cancel()
+ }
+
+ @Test
fun testDefaultDataSubId_updatesOnBroadcast() =
runBlocking(IMMEDIATE) {
var latest: Int? = null
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
index 0e2c38e..c494589 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
@@ -19,14 +19,29 @@
import android.telephony.CellSignalStrength
import com.android.settingslib.SignalIcon
import com.android.settingslib.mobile.TelephonyIcons
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
import kotlinx.coroutines.flow.MutableStateFlow
-class FakeMobileIconInteractor : MobileIconInteractor {
+class FakeMobileIconInteractor(
+ override val tableLogBuffer: TableLogBuffer,
+) : MobileIconInteractor {
override val alwaysShowDataRatIcon = MutableStateFlow(false)
+ override val activity =
+ MutableStateFlow(
+ DataActivityModel(
+ hasActivityIn = false,
+ hasActivityOut = false,
+ )
+ )
+
private val _iconGroup = MutableStateFlow<SignalIcon.MobileIconGroup>(TelephonyIcons.THREE_G)
override val networkTypeIconGroup = _iconGroup
+ override val networkName = MutableStateFlow(NetworkNameModel.Derived("demo mode"))
+
private val _isEmergencyOnly = MutableStateFlow(false)
override val isEmergencyOnly = _isEmergencyOnly
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
index 9f300e9..19e5516 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
@@ -22,12 +22,15 @@
import android.telephony.TelephonyManager.NETWORK_TYPE_UMTS
import com.android.settingslib.SignalIcon.MobileIconGroup
import com.android.settingslib.mobile.TelephonyIcons
+import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
-import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
-class FakeMobileIconsInteractor(mobileMappings: MobileMappingsProxy) : MobileIconsInteractor {
+class FakeMobileIconsInteractor(
+ mobileMappings: MobileMappingsProxy,
+ val tableLogBuffer: TableLogBuffer,
+) : MobileIconsInteractor {
val THREE_G_KEY = mobileMappings.toIconKey(THREE_G)
val LTE_KEY = mobileMappings.toIconKey(LTE)
val FOUR_G_KEY = mobileMappings.toIconKey(FOUR_G)
@@ -48,8 +51,7 @@
override val isDefaultConnectionFailed = MutableStateFlow(false)
- private val _filteredSubscriptions = MutableStateFlow<List<SubscriptionModel>>(listOf())
- override val filteredSubscriptions: Flow<List<SubscriptionModel>> = _filteredSubscriptions
+ override val filteredSubscriptions = MutableStateFlow<List<SubscriptionModel>>(listOf())
private val _activeDataConnectionHasDataEnabled = MutableStateFlow(false)
override val activeDataConnectionHasDataEnabled = _activeDataConnectionHasDataEnabled
@@ -67,7 +69,7 @@
/** Always returns a new fake interactor */
override fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor {
- return FakeMobileIconInteractor()
+ return FakeMobileIconInteractor(tableLogBuffer)
}
companion object {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
index 9b6f6df..83c5055 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
@@ -25,6 +25,7 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.OverrideNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository
@@ -48,8 +49,8 @@
class MobileIconInteractorTest : SysuiTestCase() {
private lateinit var underTest: MobileIconInteractor
private val mobileMappingsProxy = FakeMobileMappingsProxy()
- private val mobileIconsInteractor = FakeMobileIconsInteractor(mobileMappingsProxy)
- private val connectionRepository = FakeMobileConnectionRepository(SUB_1_ID)
+ private val mobileIconsInteractor = FakeMobileIconsInteractor(mobileMappingsProxy, mock())
+ private val connectionRepository = FakeMobileConnectionRepository(SUB_1_ID, mock())
private val scope = CoroutineScope(IMMEDIATE)
@@ -392,6 +393,41 @@
job.cancel()
}
+ @Test
+ fun `network name - uses operatorAlphaShot when non null and repo is default`() =
+ runBlocking(IMMEDIATE) {
+ var latest: NetworkNameModel? = null
+ val job = underTest.networkName.onEach { latest = it }.launchIn(this)
+
+ val testOperatorName = "operatorAlphaShort"
+
+ // Default network name, operator name is non-null, uses the operator name
+ connectionRepository.networkName.value = DEFAULT_NAME
+ connectionRepository.setConnectionInfo(
+ MobileConnectionModel(operatorAlphaShort = testOperatorName)
+ )
+ yield()
+
+ assertThat(latest).isEqualTo(NetworkNameModel.Derived(testOperatorName))
+
+ // Default network name, operator name is null, uses the default
+ connectionRepository.setConnectionInfo(MobileConnectionModel(operatorAlphaShort = null))
+ yield()
+
+ assertThat(latest).isEqualTo(DEFAULT_NAME)
+
+ // Derived network name, operator name non-null, uses the derived name
+ connectionRepository.networkName.value = DERIVED_NAME
+ connectionRepository.setConnectionInfo(
+ MobileConnectionModel(operatorAlphaShort = testOperatorName)
+ )
+ yield()
+
+ assertThat(latest).isEqualTo(DERIVED_NAME)
+
+ job.cancel()
+ }
+
companion object {
private val IMMEDIATE = Dispatchers.Main.immediate
@@ -401,5 +437,8 @@
private const val SUB_1_ID = 1
private val SUB_1 =
mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) }
+
+ private val DEFAULT_NAME = NetworkNameModel.Default("test default name")
+ private val DERIVED_NAME = NetworkNameModel.Derived("test derived name")
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
index 8557894..2fa3467 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
@@ -20,6 +20,7 @@
import androidx.test.filters.SmallTest
import com.android.settingslib.mobile.MobileMappings
import com.android.systemui.SysuiTestCase
+import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository
@@ -28,6 +29,7 @@
import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
import com.android.systemui.util.CarrierConfigTracker
import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -44,9 +46,9 @@
@SmallTest
class MobileIconsInteractorTest : SysuiTestCase() {
private lateinit var underTest: MobileIconsInteractor
+ private lateinit var connectionsRepository: FakeMobileConnectionsRepository
private val userSetupRepository = FakeUserSetupRepository()
private val mobileMappingsProxy = FakeMobileMappingsProxy()
- private val connectionsRepository = FakeMobileConnectionsRepository(mobileMappingsProxy)
private val scope = CoroutineScope(IMMEDIATE)
@Mock private lateinit var carrierConfigTracker: CarrierConfigTracker
@@ -55,6 +57,7 @@
fun setUp() {
MockitoAnnotations.initMocks(this)
+ connectionsRepository = FakeMobileConnectionsRepository(mobileMappingsProxy, tableLogBuffer)
connectionsRepository.setMobileConnectionRepositoryMap(
mapOf(
SUB_1_ID to CONNECTION_1,
@@ -290,21 +293,23 @@
companion object {
private val IMMEDIATE = Dispatchers.Main.immediate
+ private val tableLogBuffer =
+ TableLogBuffer(8, "MobileIconsInteractorTest", FakeSystemClock())
private const val SUB_1_ID = 1
private val SUB_1 = SubscriptionModel(subscriptionId = SUB_1_ID)
- private val CONNECTION_1 = FakeMobileConnectionRepository(SUB_1_ID)
+ private val CONNECTION_1 = FakeMobileConnectionRepository(SUB_1_ID, tableLogBuffer)
private const val SUB_2_ID = 2
private val SUB_2 = SubscriptionModel(subscriptionId = SUB_2_ID)
- private val CONNECTION_2 = FakeMobileConnectionRepository(SUB_2_ID)
+ private val CONNECTION_2 = FakeMobileConnectionRepository(SUB_2_ID, tableLogBuffer)
private const val SUB_3_ID = 3
private val SUB_3_OPP = SubscriptionModel(subscriptionId = SUB_3_ID, isOpportunistic = true)
- private val CONNECTION_3 = FakeMobileConnectionRepository(SUB_3_ID)
+ private val CONNECTION_3 = FakeMobileConnectionRepository(SUB_3_ID, tableLogBuffer)
private const val SUB_4_ID = 4
private val SUB_4_OPP = SubscriptionModel(subscriptionId = SUB_4_ID, isOpportunistic = true)
- private val CONNECTION_4 = FakeMobileConnectionRepository(SUB_4_ID)
+ private val CONNECTION_4 = FakeMobileConnectionRepository(SUB_4_ID, tableLogBuffer)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt
new file mode 100644
index 0000000..043d55a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.settingslib.mobile.TelephonyIcons
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconInteractor
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModelTest.Companion.defaultSignal
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class LocationBasedMobileIconViewModelTest : SysuiTestCase() {
+ private lateinit var commonImpl: MobileIconViewModelCommon
+ private lateinit var homeIcon: HomeMobileIconViewModel
+ private lateinit var qsIcon: QsMobileIconViewModel
+ private lateinit var keyguardIcon: KeyguardMobileIconViewModel
+ private lateinit var interactor: FakeMobileIconInteractor
+ @Mock private lateinit var logger: ConnectivityPipelineLogger
+ @Mock private lateinit var constants: ConnectivityConstants
+ @Mock private lateinit var tableLogBuffer: TableLogBuffer
+
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ interactor = FakeMobileIconInteractor(tableLogBuffer)
+ interactor.apply {
+ setLevel(1)
+ setIsDefaultDataEnabled(true)
+ setIsFailedConnection(false)
+ setIconGroup(TelephonyIcons.THREE_G)
+ setIsEmergencyOnly(false)
+ setNumberOfLevels(4)
+ isDataConnected.value = true
+ }
+ commonImpl =
+ MobileIconViewModel(SUB_1_ID, interactor, logger, constants, testScope.backgroundScope)
+
+ homeIcon = HomeMobileIconViewModel(commonImpl, logger)
+ qsIcon = QsMobileIconViewModel(commonImpl, logger)
+ keyguardIcon = KeyguardMobileIconViewModel(commonImpl, logger)
+ }
+
+ @Test
+ fun `location based view models receive same icon id when common impl updates`() =
+ testScope.runTest {
+ var latestHome: Int? = null
+ val homeJob = homeIcon.iconId.onEach { latestHome = it }.launchIn(this)
+
+ var latestQs: Int? = null
+ val qsJob = qsIcon.iconId.onEach { latestQs = it }.launchIn(this)
+
+ var latestKeyguard: Int? = null
+ val keyguardJob = keyguardIcon.iconId.onEach { latestKeyguard = it }.launchIn(this)
+
+ var expected = defaultSignal(level = 1)
+
+ assertThat(latestHome).isEqualTo(expected)
+ assertThat(latestQs).isEqualTo(expected)
+ assertThat(latestKeyguard).isEqualTo(expected)
+
+ interactor.setLevel(2)
+ expected = defaultSignal(level = 2)
+
+ assertThat(latestHome).isEqualTo(expected)
+ assertThat(latestQs).isEqualTo(expected)
+ assertThat(latestKeyguard).isEqualTo(expected)
+
+ homeJob.cancel()
+ qsJob.cancel()
+ keyguardJob.cancel()
+ }
+
+ companion object {
+ private const val SUB_1_ID = 1
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
index 2c8f0a7..50221bc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
@@ -22,28 +22,42 @@
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.log.table.TableLogBuffer
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconInteractor
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
+import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
-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.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.yield
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.MockitoAnnotations
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
class MobileIconViewModelTest : SysuiTestCase() {
private lateinit var underTest: MobileIconViewModel
- private val interactor = FakeMobileIconInteractor()
+ private lateinit var interactor: FakeMobileIconInteractor
@Mock private lateinit var logger: ConnectivityPipelineLogger
+ @Mock private lateinit var constants: ConnectivityConstants
+ @Mock private lateinit var tableLogBuffer: TableLogBuffer
+
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
+ interactor = FakeMobileIconInteractor(tableLogBuffer)
interactor.apply {
setLevel(1)
setIsDefaultDataEnabled(true)
@@ -53,12 +67,13 @@
setNumberOfLevels(4)
isDataConnected.value = true
}
- underTest = MobileIconViewModel(SUB_1_ID, interactor, logger)
+ underTest =
+ MobileIconViewModel(SUB_1_ID, interactor, logger, constants, testScope.backgroundScope)
}
@Test
fun iconId_correctLevel_notCutout() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: Int? = null
val job = underTest.iconId.onEach { latest = it }.launchIn(this)
val expected = defaultSignal()
@@ -70,7 +85,7 @@
@Test
fun iconId_cutout_whenDefaultDataDisabled() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
interactor.setIsDefaultDataEnabled(false)
var latest: Int? = null
@@ -84,7 +99,7 @@
@Test
fun networkType_dataEnabled_groupIsRepresented() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
val expected =
Icon.Resource(
THREE_G.dataType,
@@ -102,7 +117,7 @@
@Test
fun networkType_nullWhenDisabled() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
interactor.setIconGroup(THREE_G)
interactor.setIsDataEnabled(false)
var latest: Icon? = null
@@ -115,7 +130,7 @@
@Test
fun networkType_nullWhenFailedConnection() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
interactor.setIconGroup(THREE_G)
interactor.setIsDataEnabled(true)
interactor.setIsFailedConnection(true)
@@ -129,7 +144,7 @@
@Test
fun networkType_nullWhenDataDisconnects() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
val initial =
Icon.Resource(
THREE_G.dataType,
@@ -153,7 +168,7 @@
@Test
fun networkType_null_changeToDisabled() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
val expected =
Icon.Resource(
THREE_G.dataType,
@@ -176,7 +191,7 @@
@Test
fun networkType_alwaysShow_shownEvenWhenDisabled() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
interactor.setIconGroup(THREE_G)
interactor.setIsDataEnabled(true)
interactor.alwaysShowDataRatIcon.value = true
@@ -196,7 +211,7 @@
@Test
fun networkType_alwaysShow_shownEvenWhenDisconnected() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
interactor.setIconGroup(THREE_G)
interactor.isDataConnected.value = false
interactor.alwaysShowDataRatIcon.value = true
@@ -216,7 +231,7 @@
@Test
fun networkType_alwaysShow_shownEvenWhenFailedConnection() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
interactor.setIconGroup(THREE_G)
interactor.setIsFailedConnection(true)
interactor.alwaysShowDataRatIcon.value = true
@@ -236,7 +251,7 @@
@Test
fun roaming() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
interactor.isRoaming.value = true
var latest: Boolean? = null
val job = underTest.roaming.onEach { latest = it }.launchIn(this)
@@ -250,16 +265,115 @@
job.cancel()
}
- /** Convenience constructor for these tests */
- private fun defaultSignal(
- level: Int = 1,
- connected: Boolean = true,
- ): Int {
- return SignalDrawable.getState(level, /* numLevels */ 4, !connected)
- }
+ @Test
+ fun `data activity - null when config is off`() =
+ testScope.runTest {
+ // Create a new view model here so the constants are properly read
+ whenever(constants.shouldShowActivityConfig).thenReturn(false)
+ underTest =
+ MobileIconViewModel(
+ SUB_1_ID,
+ interactor,
+ logger,
+ constants,
+ testScope.backgroundScope,
+ )
+
+ var inVisible: Boolean? = null
+ val inJob = underTest.activityInVisible.onEach { inVisible = it }.launchIn(this)
+
+ var outVisible: Boolean? = null
+ val outJob = underTest.activityInVisible.onEach { outVisible = it }.launchIn(this)
+
+ var containerVisible: Boolean? = null
+ val containerJob =
+ underTest.activityInVisible.onEach { containerVisible = it }.launchIn(this)
+
+ interactor.activity.value =
+ DataActivityModel(
+ hasActivityIn = true,
+ hasActivityOut = true,
+ )
+
+ assertThat(inVisible).isFalse()
+ assertThat(outVisible).isFalse()
+ assertThat(containerVisible).isFalse()
+
+ inJob.cancel()
+ outJob.cancel()
+ containerJob.cancel()
+ }
+
+ @Test
+ fun `data activity - config on - test indicators`() =
+ testScope.runTest {
+ // Create a new view model here so the constants are properly read
+ whenever(constants.shouldShowActivityConfig).thenReturn(true)
+ underTest =
+ MobileIconViewModel(
+ SUB_1_ID,
+ interactor,
+ logger,
+ constants,
+ testScope.backgroundScope,
+ )
+
+ var inVisible: Boolean? = null
+ val inJob = underTest.activityInVisible.onEach { inVisible = it }.launchIn(this)
+
+ var outVisible: Boolean? = null
+ val outJob = underTest.activityOutVisible.onEach { outVisible = it }.launchIn(this)
+
+ var containerVisible: Boolean? = null
+ val containerJob =
+ underTest.activityContainerVisible.onEach { containerVisible = it }.launchIn(this)
+
+ interactor.activity.value =
+ DataActivityModel(
+ hasActivityIn = true,
+ hasActivityOut = false,
+ )
+
+ yield()
+
+ assertThat(inVisible).isTrue()
+ assertThat(outVisible).isFalse()
+ assertThat(containerVisible).isTrue()
+
+ interactor.activity.value =
+ DataActivityModel(
+ hasActivityIn = false,
+ hasActivityOut = true,
+ )
+
+ assertThat(inVisible).isFalse()
+ assertThat(outVisible).isTrue()
+ assertThat(containerVisible).isTrue()
+
+ interactor.activity.value =
+ DataActivityModel(
+ hasActivityIn = false,
+ hasActivityOut = false,
+ )
+
+ assertThat(inVisible).isFalse()
+ assertThat(outVisible).isFalse()
+ assertThat(containerVisible).isFalse()
+
+ inJob.cancel()
+ outJob.cancel()
+ containerJob.cancel()
+ }
companion object {
- private val IMMEDIATE = Dispatchers.Main.immediate
private const val SUB_1_ID = 1
+
+ /** Convenience constructor for these tests */
+ fun defaultSignal(
+ level: Int = 1,
+ connected: Boolean = true,
+ ): Int {
+ return SignalDrawable.getState(level, /* numLevels */ 4, !connected)
+ }
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt
new file mode 100644
index 0000000..d6cb762
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.phone.StatusBarLocation
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class MobileIconsViewModelTest : SysuiTestCase() {
+ private lateinit var underTest: MobileIconsViewModel
+ private val interactor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock())
+
+ @Mock private lateinit var logger: ConnectivityPipelineLogger
+ @Mock private lateinit var constants: ConnectivityConstants
+
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ val subscriptionIdsFlow =
+ interactor.filteredSubscriptions
+ .map { subs -> subs.map { it.subscriptionId } }
+ .stateIn(testScope.backgroundScope, SharingStarted.WhileSubscribed(), listOf())
+
+ underTest =
+ MobileIconsViewModel(
+ subscriptionIdsFlow,
+ interactor,
+ logger,
+ constants,
+ testScope.backgroundScope,
+ )
+
+ interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2)
+ }
+
+ @Test
+ fun `caching - mobile icon view model is reused for same sub id`() =
+ testScope.runTest {
+ val model1 = underTest.viewModelForSub(1, StatusBarLocation.HOME)
+ val model2 = underTest.viewModelForSub(1, StatusBarLocation.QS)
+
+ assertThat(model1.commonImpl).isSameInstanceAs(model2.commonImpl)
+ }
+
+ @Test
+ fun `caching - invalid view models are removed from cache when sub disappears`() =
+ testScope.runTest {
+ // Retrieve models to trigger caching
+ val model1 = underTest.viewModelForSub(1, StatusBarLocation.HOME)
+ val model2 = underTest.viewModelForSub(2, StatusBarLocation.QS)
+
+ // Both impls are cached
+ assertThat(underTest.mobileIconSubIdCache)
+ .containsExactly(1, model1.commonImpl, 2, model2.commonImpl)
+
+ // SUB_1 is removed from the list...
+ interactor.filteredSubscriptions.value = listOf(SUB_2)
+
+ // ... and dropped from the cache
+ assertThat(underTest.mobileIconSubIdCache).containsExactly(2, model2.commonImpl)
+ }
+
+ companion object {
+ private val SUB_1 = SubscriptionModel(subscriptionId = 1, isOpportunistic = false)
+ private val SUB_2 = SubscriptionModel(subscriptionId = 2, isOpportunistic = false)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt
new file mode 100644
index 0000000..b935442
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.wifi.data.repository
+
+import android.net.ConnectivityManager
+import android.net.wifi.WifiManager
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.demomode.DemoMode
+import com.android.systemui.demomode.DemoModeController
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoWifiRepository
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.kotlinArgumentCaptor
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class WifiRepositorySwitcherTest : SysuiTestCase() {
+ private lateinit var underTest: WifiRepositorySwitcher
+ private lateinit var realImpl: WifiRepositoryImpl
+ private lateinit var demoImpl: DemoWifiRepository
+
+ @Mock private lateinit var demoModeController: DemoModeController
+ @Mock private lateinit var logger: ConnectivityPipelineLogger
+ @Mock private lateinit var tableLogger: TableLogBuffer
+ @Mock private lateinit var connectivityManager: ConnectivityManager
+ @Mock private lateinit var wifiManager: WifiManager
+ @Mock private lateinit var demoModeWifiDataSource: DemoModeWifiDataSource
+ private val demoModelFlow = MutableStateFlow<FakeWifiEventModel?>(null)
+
+ private val mainExecutor = FakeExecutor(FakeSystemClock())
+
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ // Never start in demo mode
+ whenever(demoModeController.isInDemoMode).thenReturn(false)
+
+ realImpl =
+ WifiRepositoryImpl(
+ fakeBroadcastDispatcher,
+ connectivityManager,
+ logger,
+ tableLogger,
+ mainExecutor,
+ testScope.backgroundScope,
+ wifiManager,
+ )
+
+ whenever(demoModeWifiDataSource.wifiEvents).thenReturn(demoModelFlow)
+
+ demoImpl =
+ DemoWifiRepository(
+ demoModeWifiDataSource,
+ testScope.backgroundScope,
+ )
+
+ underTest =
+ WifiRepositorySwitcher(
+ realImpl,
+ demoImpl,
+ demoModeController,
+ testScope.backgroundScope,
+ )
+ }
+
+ @Test
+ fun `switcher active repo - updates when demo mode changes`() =
+ testScope.runTest {
+ assertThat(underTest.activeRepo.value).isSameInstanceAs(realImpl)
+
+ var latest: WifiRepository? = null
+ val job = underTest.activeRepo.onEach { latest = it }.launchIn(this)
+
+ startDemoMode()
+
+ assertThat(latest).isSameInstanceAs(demoImpl)
+
+ finishDemoMode()
+
+ assertThat(latest).isSameInstanceAs(realImpl)
+
+ job.cancel()
+ }
+
+ private fun startDemoMode() {
+ whenever(demoModeController.isInDemoMode).thenReturn(true)
+ getDemoModeCallback().onDemoModeStarted()
+ }
+
+ private fun finishDemoMode() {
+ whenever(demoModeController.isInDemoMode).thenReturn(false)
+ getDemoModeCallback().onDemoModeFinished()
+ }
+
+ private fun getDemoModeCallback(): DemoMode {
+ val captor = kotlinArgumentCaptor<DemoMode>()
+ Mockito.verify(demoModeController).addCallback(captor.capture())
+ return captor.value
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
index b47f177..4158434 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
@@ -146,7 +146,7 @@
@Test
fun activity_showActivityConfigFalse_outputsFalse() = runBlocking(IMMEDIATE) {
- whenever(wifiConstants.shouldShowActivityConfig).thenReturn(false)
+ whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(false)
createAndSetViewModel()
wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
@@ -183,7 +183,7 @@
@Test
fun activity_showActivityConfigFalse_noUpdatesReceived() = runBlocking(IMMEDIATE) {
- whenever(wifiConstants.shouldShowActivityConfig).thenReturn(false)
+ whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(false)
createAndSetViewModel()
wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
@@ -225,7 +225,7 @@
@Test
fun activity_nullSsid_outputsFalse() = runBlocking(IMMEDIATE) {
- whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true)
+ whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
createAndSetViewModel()
wifiRepository.setWifiNetwork(WifiNetworkModel.Active(NETWORK_ID, ssid = null))
@@ -268,7 +268,7 @@
@Test
fun activity_allLocationViewModelsReceiveSameData() = runBlocking(IMMEDIATE) {
- whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true)
+ whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
createAndSetViewModel()
wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
@@ -308,7 +308,7 @@
@Test
fun activityIn_hasActivityInTrue_outputsTrue() = runBlocking(IMMEDIATE) {
- whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true)
+ whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
createAndSetViewModel()
wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
@@ -330,7 +330,7 @@
@Test
fun activityIn_hasActivityInFalse_outputsFalse() = runBlocking(IMMEDIATE) {
- whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true)
+ whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
createAndSetViewModel()
wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
@@ -352,7 +352,7 @@
@Test
fun activityOut_hasActivityOutTrue_outputsTrue() = runBlocking(IMMEDIATE) {
- whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true)
+ whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
createAndSetViewModel()
wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
@@ -374,7 +374,7 @@
@Test
fun activityOut_hasActivityOutFalse_outputsFalse() = runBlocking(IMMEDIATE) {
- whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true)
+ whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
createAndSetViewModel()
wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
@@ -396,7 +396,7 @@
@Test
fun activityContainer_hasActivityInTrue_outputsTrue() = runBlocking(IMMEDIATE) {
- whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true)
+ whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
createAndSetViewModel()
wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
@@ -418,7 +418,7 @@
@Test
fun activityContainer_hasActivityOutTrue_outputsTrue() = runBlocking(IMMEDIATE) {
- whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true)
+ whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
createAndSetViewModel()
wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
@@ -440,7 +440,7 @@
@Test
fun activityContainer_inAndOutTrue_outputsTrue() = runBlocking(IMMEDIATE) {
- whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true)
+ whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
createAndSetViewModel()
wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
@@ -462,7 +462,7 @@
@Test
fun activityContainer_inAndOutFalse_outputsFalse() = runBlocking(IMMEDIATE) {
- whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true)
+ whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
createAndSetViewModel()
wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
index 2c47204..4b32ee2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
@@ -55,6 +55,7 @@
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.widget.EditText;
+import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.window.OnBackInvokedCallback;
import android.window.OnBackInvokedDispatcher;
@@ -414,7 +415,9 @@
mDependency,
TestableLooper.get(this));
ExpandableNotificationRow row = helper.createRow();
+ FrameLayout remoteInputViewParent = new FrameLayout(mContext);
RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController);
+ remoteInputViewParent.addView(view);
bindController(view, row.getEntry());
// Start defocus animation
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 b9a5bd7..4ef4e6c 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
@@ -64,7 +64,7 @@
wakeLockBuilder,
systemClock,
) {
- override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
+ override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) {
// Just bypass the animation in tests
onAnimationEnd.run()
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt
index ffa4e2d..9534575 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt
@@ -175,6 +175,8 @@
underTest.onRecordSelected(UserRecord(info = userInfos[1]), dialogShower)
+ verify(uiEventLogger, times(1))
+ .log(MultiUserActionsEvent.SWITCH_TO_USER_FROM_USER_SWITCHER)
verify(dialogShower).dismiss()
verify(activityManager).switchUser(userInfos[1].id)
Unit
@@ -190,6 +192,33 @@
underTest.onRecordSelected(UserRecord(info = userInfos.last()))
+ verify(uiEventLogger, times(1))
+ .log(MultiUserActionsEvent.SWITCH_TO_GUEST_FROM_USER_SWITCHER)
+ verify(activityManager).switchUser(userInfos.last().id)
+ Unit
+ }
+
+ @Test
+ fun `onRecordSelected - switch to restricted user`() =
+ runBlocking(IMMEDIATE) {
+ var userInfos = createUserInfos(count = 2, includeGuest = false).toMutableList()
+ userInfos.add(
+ UserInfo(
+ 60,
+ "Restricted user",
+ /* iconPath= */ "",
+ /* flags= */ UserInfo.FLAG_FULL,
+ UserManager.USER_TYPE_FULL_RESTRICTED,
+ )
+ )
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+
+ underTest.onRecordSelected(UserRecord(info = userInfos.last()))
+
+ verify(uiEventLogger, times(1))
+ .log(MultiUserActionsEvent.SWITCH_TO_RESTRICTED_USER_FROM_USER_SWITCHER)
verify(activityManager).switchUser(userInfos.last().id)
Unit
}
@@ -206,6 +235,8 @@
underTest.onRecordSelected(UserRecord(isGuest = true), dialogShower)
+ verify(uiEventLogger, times(1))
+ .log(MultiUserActionsEvent.CREATE_GUEST_FROM_USER_SWITCHER)
verify(dialogShower).dismiss()
verify(manager).createGuest(any())
Unit
@@ -221,6 +252,8 @@
underTest.onRecordSelected(UserRecord(isAddSupervisedUser = true), dialogShower)
+ verify(uiEventLogger, times(1))
+ .log(MultiUserActionsEvent.CREATE_RESTRICTED_USER_FROM_USER_SWITCHER)
verify(dialogShower, never()).dismiss()
verify(activityStarter).startActivity(any(), anyBoolean())
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
index 5501949..39d2eca 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
@@ -46,12 +46,18 @@
private val _isKeyguardShowing = MutableStateFlow(false)
override val isKeyguardShowing: Flow<Boolean> = _isKeyguardShowing
+ private val _isKeyguardOccluded = MutableStateFlow(false)
+ override val isKeyguardOccluded: Flow<Boolean> = _isKeyguardOccluded
+
private val _isDozing = MutableStateFlow(false)
override val isDozing: Flow<Boolean> = _isDozing
private val _isDreaming = MutableStateFlow(false)
override val isDreaming: Flow<Boolean> = _isDreaming
+ private val _isDreamingWithOverlay = MutableStateFlow(false)
+ override val isDreamingWithOverlay: Flow<Boolean> = _isDreamingWithOverlay
+
private val _dozeAmount = MutableStateFlow(0f)
override val linearDozeAmount: Flow<Float> = _dozeAmount
@@ -112,10 +118,18 @@
_isKeyguardShowing.value = isShowing
}
+ fun setKeyguardOccluded(isOccluded: Boolean) {
+ _isKeyguardOccluded.value = isOccluded
+ }
+
fun setDozing(isDozing: Boolean) {
_isDozing.value = isDozing
}
+ fun setDreamingWithOverlay(isDreaming: Boolean) {
+ _isDreamingWithOverlay.value = isDreaming
+ }
+
fun setDozeAmount(dozeAmount: Float) {
_dozeAmount.value = dozeAmount
}
@@ -144,6 +158,10 @@
_fingerprintSensorLocation.tryEmit(location)
}
+ fun setDozeTransitionModel(model: DozeTransitionModel) {
+ _dozeTransitionModel.value = model
+ }
+
override fun isUdfpsSupported(): Boolean {
return _isUdfpsSupported.value
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/RankingBuilder.java b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/RankingBuilder.java
index 045e6f1..7bcad45 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/RankingBuilder.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/RankingBuilder.java
@@ -16,6 +16,8 @@
package com.android.systemui.statusbar;
+import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED;
+
import android.annotation.NonNull;
import android.app.Notification;
import android.app.NotificationChannel;
@@ -57,6 +59,7 @@
private ShortcutInfo mShortcutInfo = null;
private int mRankingAdjustment = 0;
private boolean mIsBubble = false;
+ private int mProposedImportance = IMPORTANCE_UNSPECIFIED;
public RankingBuilder() {
}
@@ -86,6 +89,7 @@
mShortcutInfo = ranking.getConversationShortcutInfo();
mRankingAdjustment = ranking.getRankingAdjustment();
mIsBubble = ranking.isBubble();
+ mProposedImportance = ranking.getProposedImportance();
}
public Ranking build() {
@@ -114,7 +118,8 @@
mIsConversation,
mShortcutInfo,
mRankingAdjustment,
- mIsBubble);
+ mIsBubble,
+ mProposedImportance);
return ranking;
}
@@ -214,6 +219,11 @@
return this;
}
+ public RankingBuilder setProposedImportance(@Importance int importance) {
+ mProposedImportance = importance;
+ return this;
+ }
+
public RankingBuilder setUserSentiment(int userSentiment) {
mUserSentiment = userSentiment;
return this;
diff --git a/packages/overlays/DisplayCutoutEmulationDoubleOverlay/res/values-b+sr+Latn/strings.xml b/packages/overlays/DisplayCutoutEmulationDoubleOverlay/res/values-b+sr+Latn/strings.xml
index d2eb7db..99b89a2 100644
--- a/packages/overlays/DisplayCutoutEmulationDoubleOverlay/res/values-b+sr+Latn/strings.xml
+++ b/packages/overlays/DisplayCutoutEmulationDoubleOverlay/res/values-b+sr+Latn/strings.xml
@@ -17,5 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="display_cutout_emulation_overlay" msgid="5323179900047630217">"Izrezana slika za duple ekrane"</string>
+ <string name="display_cutout_emulation_overlay" msgid="5323179900047630217">"Изрезана слика за дупле екране"</string>
</resources>
diff --git a/proto/src/windowmanager.proto b/proto/src/windowmanager.proto
index f26404c6..da4dfa9 100644
--- a/proto/src/windowmanager.proto
+++ b/proto/src/windowmanager.proto
@@ -68,4 +68,8 @@
LetterboxHorizontalReachability letterbox_position_for_horizontal_reachability = 1;
// Represents the current vertical position for the letterboxed activity
LetterboxVerticalReachability letterbox_position_for_vertical_reachability = 2;
+ // Represents the current horizontal position for the letterboxed activity in book mode
+ LetterboxHorizontalReachability letterbox_position_for_book_mode_reachability = 3;
+ // Represents the current vertical position for the letterboxed activity in tabletop mode
+ LetterboxVerticalReachability letterbox_position_for_tabletop_mode_reachability = 4;
}
\ No newline at end of file
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index b6fecf5..1183d6b 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -1760,6 +1760,8 @@
AccessibilityServiceConnection service = state.mBoundServices.get(i);
service.notifyClearAccessibilityNodeInfoCache();
}
+
+ mProxyManager.clearCacheLocked();
}
private void notifyMagnificationChangedLocked(int displayId, @NonNull Region region,
@@ -3714,6 +3716,10 @@
mProxyManager.registerProxy(client, displayId, mContext,
sIdCounter++, mMainHandler, mSecurityPolicy, this, getTraceManager(),
mWindowManagerService, mA11yWindowManager);
+
+ synchronized (mLock) {
+ notifyClearAccessibilityCacheLocked();
+ }
return true;
}
diff --git a/services/accessibility/java/com/android/server/accessibility/ProxyManager.java b/services/accessibility/java/com/android/server/accessibility/ProxyManager.java
index 8527358..fcbdc4e 100644
--- a/services/accessibility/java/com/android/server/accessibility/ProxyManager.java
+++ b/services/accessibility/java/com/android/server/accessibility/ProxyManager.java
@@ -235,4 +235,15 @@
}
}
}
+
+ /**
+ * Clears all proxy caches.
+ */
+ public void clearCacheLocked() {
+ for (int i = 0; i < mProxyA11yServiceConnections.size(); i++) {
+ final ProxyAccessibilityServiceConnection proxy =
+ mProxyA11yServiceConnections.valueAt(i);
+ proxy.notifyClearAccessibilityNodeInfoCache();
+ }
+ }
}
\ No newline at end of file
diff --git a/services/api/current.txt b/services/api/current.txt
index b5798d5..090a449 100644
--- a/services/api/current.txt
+++ b/services/api/current.txt
@@ -72,16 +72,90 @@
package com.android.server.pm.pkg {
public interface AndroidPackage {
+ method @Nullable public String getAppComponentFactory();
+ method @Nullable public String getApplicationClassName();
+ method @Nullable public String getBackupAgentName();
+ method @DrawableRes public int getBannerRes();
+ method public int getBaseRevisionCode();
+ method public int getCategory();
+ method @Nullable public String getClassLoaderName();
+ method @Dimension(unit=android.annotation.Dimension.DP) public int getCompatibleWidthLimitDp();
+ method @XmlRes public int getDataExtractionRulesRes();
+ method @StringRes public int getDescriptionRes();
+ method @XmlRes public int getFullBackupContentRes();
+ method public int getGwpAsanMode();
+ method @DrawableRes public int getIconRes();
+ method @StringRes public int getLabelRes();
+ method @Dimension(unit=android.annotation.Dimension.DP) public int getLargestWidthLimitDp();
method @NonNull public java.util.List<java.lang.String> getLibraryNames();
+ method @XmlRes public int getLocaleConfigRes();
+ method @DrawableRes public int getLogoRes();
+ method public long getLongVersionCode();
+ method public float getMaxAspectRatio();
+ method public float getMinAspectRatio();
+ method public int getNativeHeapZeroInitialized();
+ method @XmlRes public int getNetworkSecurityConfigRes();
+ method @Nullable public String getRequiredAccountType();
+ method @Dimension(unit=android.annotation.Dimension.DP) public int getRequiresSmallestWidthDp();
+ method @Nullable public String getRestrictedAccountType();
+ method @DrawableRes public int getRoundIconRes();
method @Nullable public String getSdkLibraryName();
+ method @Nullable public String getSharedUserId();
+ method @StringRes public int getSharedUserLabelRes();
method @NonNull public java.util.List<com.android.server.pm.pkg.AndroidPackageSplit> getSplits();
method @Nullable public String getStaticSharedLibraryName();
method @NonNull public java.util.UUID getStorageUuid();
method public int getTargetSdkVersion();
+ method @StyleRes public int getThemeRes();
+ method public int getUiOptions();
+ method @Nullable public String getVersionName();
+ method @Nullable public String getZygotePreloadName();
+ method public boolean isAllowAudioPlaybackCapture();
+ method public boolean isAllowBackup();
+ method public boolean isAllowClearUserData();
+ method public boolean isAllowClearUserDataOnFailedRestore();
+ method public boolean isAllowNativeHeapPointerTagging();
+ method public boolean isAllowTaskReparenting();
+ method public boolean isAnyDensity();
+ method public boolean isAttributionsUserVisible();
+ method public boolean isBackupInForeground();
+ method public boolean isCantSaveState();
+ method public boolean isCoreApp();
+ method public boolean isCrossProfile();
method public boolean isDebuggable();
+ method public boolean isDefaultToDeviceProtectedStorage();
+ method public boolean isDirectBootAware();
+ method public boolean isExtractNativeLibs();
+ method public boolean isFactoryTest();
+ method public boolean isForceQueryable();
+ method public boolean isFullBackupOnly();
+ method public boolean isHardwareAccelerated();
+ method public boolean isHasCode();
+ method public boolean isHasFragileUserData();
method public boolean isIsolatedSplitLoading();
+ method public boolean isKillAfterRestore();
+ method public boolean isLargeHeap();
+ method public boolean isLeavingSharedUser();
+ method public boolean isMultiArch();
+ method public boolean isNativeLibraryRootRequiresIsa();
+ method public boolean isOnBackInvokedCallbackEnabled();
+ method public boolean isPersistent();
+ method public boolean isProfileable();
+ method public boolean isProfileableByShell();
+ method public boolean isRequestLegacyExternalStorage();
+ method public boolean isRequiredForAllUsers();
+ method public boolean isResetEnabledSettingsOnAppDataCleared();
+ method public boolean isRestoreAnyVersion();
method public boolean isSignedWithPlatformKey();
+ method public boolean isSupportsExtraLargeScreens();
+ method public boolean isSupportsLargeScreens();
+ method public boolean isSupportsNormalScreens();
+ method public boolean isSupportsRtl();
+ method public boolean isSupportsSmallScreens();
+ method public boolean isTestOnly();
+ method public boolean isUse32BitAbi();
method public boolean isUseEmbeddedDex();
+ method public boolean isUsesCleartextTraffic();
method public boolean isUsesNonSdkApi();
method public boolean isVmSafeMode();
}
diff --git a/services/backup/java/com/android/server/backup/BackupManagerService.java b/services/backup/java/com/android/server/backup/BackupManagerService.java
index 1c571a7..53f5fe1 100644
--- a/services/backup/java/com/android/server/backup/BackupManagerService.java
+++ b/services/backup/java/com/android/server/backup/BackupManagerService.java
@@ -25,6 +25,8 @@
import android.app.ActivityManager;
import android.app.admin.DevicePolicyManager;
import android.app.backup.BackupManager;
+import android.app.backup.BackupRestoreEventLogger;
+import android.app.backup.BackupRestoreEventLogger.DataTypeResult;
import android.app.backup.IBackupManager;
import android.app.backup.IBackupManagerMonitor;
import android.app.backup.IBackupObserver;
@@ -1556,6 +1558,22 @@
}
}
+ public void reportDelayedRestoreResult(String packageName, List<DataTypeResult> results) {
+ int userId = Binder.getCallingUserHandle().getIdentifier();
+ if (!isUserReadyForBackup(userId)) {
+ Slog.w(TAG, "Returning from reportDelayedRestoreResult as backup for user" + userId +
+ " is not initialized yet");
+ return;
+ }
+ UserBackupManagerService userBackupManagerService =
+ getServiceForUserIfCallerHasPermission(userId,
+ /* caller */ "reportDelayedRestoreResult()");
+
+ if (userBackupManagerService != null) {
+ userBackupManagerService.reportDelayedRestoreResult(packageName, results);
+ }
+ }
+
/**
* Returns the {@link UserBackupManagerService} instance for the specified user {@code userId}.
* If the user is not registered with the service (either the user is locked or not eligible for
diff --git a/services/backup/java/com/android/server/backup/UserBackupManagerService.java b/services/backup/java/com/android/server/backup/UserBackupManagerService.java
index ce3e628..6ba01d7 100644
--- a/services/backup/java/com/android/server/backup/UserBackupManagerService.java
+++ b/services/backup/java/com/android/server/backup/UserBackupManagerService.java
@@ -49,6 +49,7 @@
import android.app.backup.BackupAnnotations.BackupDestination;
import android.app.backup.BackupManager;
import android.app.backup.BackupManagerMonitor;
+import android.app.backup.BackupRestoreEventLogger;
import android.app.backup.FullBackup;
import android.app.backup.IBackupManager;
import android.app.backup.IBackupManagerMonitor;
@@ -505,13 +506,14 @@
@VisibleForTesting
UserBackupManagerService(Context context, PackageManager packageManager,
- LifecycleOperationStorage operationStorage) {
+ LifecycleOperationStorage operationStorage, TransportManager transportManager) {
mContext = context;
mUserId = 0;
mRegisterTransportsRequestedTime = 0;
mPackageManager = packageManager;
mOperationStorage = operationStorage;
+ mTransportManager = transportManager;
mBaseStateDir = null;
mDataDir = null;
@@ -521,7 +523,6 @@
mRunInitReceiver = null;
mRunInitIntent = null;
mAgentTimeoutParameters = null;
- mTransportManager = null;
mActivityManagerInternal = null;
mAlarmManager = null;
mConstants = null;
@@ -3038,6 +3039,37 @@
mBackupPreferences.addExcludedKeys(packageName, keys);
}
+ public void reportDelayedRestoreResult(String packageName,
+ List<BackupRestoreEventLogger.DataTypeResult> results) {
+ String transport = mTransportManager.getCurrentTransportName();
+ if (transport == null) {
+ Slog.w(TAG, "Failed to send delayed restore logs as no transport selected");
+ return;
+ }
+
+ TransportConnection transportConnection = null;
+ try {
+ PackageInfo packageInfo = getPackageManager().getPackageInfoAsUser(packageName,
+ PackageManager.PackageInfoFlags.of(/* value */ 0), getUserId());
+
+ transportConnection = mTransportManager.getTransportClientOrThrow(
+ transport, /* caller */"BMS.reportDelayedRestoreResult");
+ BackupTransportClient transportClient = transportConnection.connectOrThrow(
+ /* caller */ "BMS.reportDelayedRestoreResult");
+
+ IBackupManagerMonitor monitor = transportClient.getBackupManagerMonitor();
+ BackupManagerMonitorUtils.sendAgentLoggingResults(monitor, packageInfo, results);
+ } catch (NameNotFoundException | TransportNotAvailableException
+ | TransportNotRegisteredException | RemoteException e) {
+ Slog.w(TAG, "Failed to send delayed restore logs: " + e);
+ } finally {
+ if (transportConnection != null) {
+ mTransportManager.disposeOfTransportClient(transportConnection,
+ /* caller */"BMS.reportDelayedRestoreResult");
+ }
+ }
+ }
+
private boolean startConfirmationUi(int token, String action) {
try {
Intent confIntent = new Intent(action);
diff --git a/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorUtils.java b/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorUtils.java
index 8eda5b9..57ad89b 100644
--- a/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorUtils.java
+++ b/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorUtils.java
@@ -24,10 +24,11 @@
import static com.android.server.backup.BackupManagerService.DEBUG;
import static com.android.server.backup.BackupManagerService.TAG;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.IBackupAgent;
import android.app.backup.BackupManagerMonitor;
-import android.app.backup.BackupRestoreEventLogger;
+import android.app.backup.BackupRestoreEventLogger.DataTypeResult;
import android.app.backup.IBackupManagerMonitor;
import android.content.pm.PackageInfo;
import android.os.Bundle;
@@ -119,19 +120,11 @@
}
try {
- AndroidFuture<List<BackupRestoreEventLogger.DataTypeResult>> resultsFuture =
+ AndroidFuture<List<DataTypeResult>> resultsFuture =
new AndroidFuture<>();
agent.getLoggerResults(resultsFuture);
- Bundle loggerResultsBundle = new Bundle();
- loggerResultsBundle.putParcelableList(
- EXTRA_LOG_AGENT_LOGGING_RESULTS,
+ return sendAgentLoggingResults(monitor, pkg,
resultsFuture.get(AGENT_LOGGER_RESULTS_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS));
- return BackupManagerMonitorUtils.monitorEvent(
- monitor,
- LOG_EVENT_ID_AGENT_LOGGING_RESULTS,
- pkg,
- LOG_EVENT_CATEGORY_AGENT,
- loggerResultsBundle);
} catch (TimeoutException e) {
Slog.w(TAG, "Timeout while waiting to retrieve logging results from agent", e);
} catch (Exception e) {
@@ -140,6 +133,19 @@
return monitor;
}
+ public static IBackupManagerMonitor sendAgentLoggingResults(
+ @NonNull IBackupManagerMonitor monitor, PackageInfo pkg, List<DataTypeResult> results) {
+ Bundle loggerResultsBundle = new Bundle();
+ loggerResultsBundle.putParcelableList(
+ EXTRA_LOG_AGENT_LOGGING_RESULTS, results);
+ return monitorEvent(
+ monitor,
+ LOG_EVENT_ID_AGENT_LOGGING_RESULTS,
+ pkg,
+ LOG_EVENT_CATEGORY_AGENT,
+ loggerResultsBundle);
+ }
+
/**
* Adds given key-value pair in the bundle and returns the bundle. If bundle was null it will
* be created.
diff --git a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncInCallService.java b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncInCallService.java
new file mode 100644
index 0000000..7ea1e6c
--- /dev/null
+++ b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncInCallService.java
@@ -0,0 +1,75 @@
+/*
+ * 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.companion.datatransfer.contextsync;
+
+import android.telecom.Call;
+import android.telecom.InCallService;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicLong;
+
+/** In-call service to sync call metadata across a user's devices. */
+public class CallMetadataSyncInCallService extends InCallService {
+
+ @VisibleForTesting
+ final Set<CrossDeviceCall> mCurrentCalls = new HashSet<>();
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mCurrentCalls.addAll(getCalls().stream().map(CrossDeviceCall::new).toList());
+ }
+
+ @Override
+ public void onCallAdded(Call call) {
+ onCallAdded(new CrossDeviceCall(call));
+ }
+
+ @VisibleForTesting
+ void onCallAdded(CrossDeviceCall call) {
+ mCurrentCalls.add(call);
+ }
+
+ @Override
+ public void onCallRemoved(Call call) {
+ mCurrentCalls.removeIf(crossDeviceCall -> crossDeviceCall.getCall().equals(call));
+ }
+
+ /** Data holder for a telecom call and additional metadata. */
+ public static final class CrossDeviceCall {
+ private static final AtomicLong sNextId = new AtomicLong(1);
+
+ private final Call mCall;
+ private final long mId;
+
+ public CrossDeviceCall(Call call) {
+ mCall = call;
+ mId = sNextId.getAndIncrement();
+ }
+
+ public Call getCall() {
+ return mCall;
+ }
+
+ public long getId() {
+ return mId;
+ }
+ }
+}
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 4d173d6..cdd5471 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -22,9 +22,9 @@
import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
+import android.annotation.EnforcePermission;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.annotation.RequiresPermission;
import android.annotation.StringRes;
import android.app.Activity;
import android.app.ActivityOptions;
@@ -62,6 +62,7 @@
import android.os.IBinder;
import android.os.LocaleList;
import android.os.Looper;
+import android.os.PermissionEnforcer;
import android.os.PowerManager;
import android.os.RemoteException;
import android.os.ResultReceiver;
@@ -198,6 +199,7 @@
IVirtualDeviceActivityListener activityListener,
Consumer<ArraySet<Integer>> runningAppsChangedCallback,
VirtualDeviceParams params) {
+ super(PermissionEnforcer.fromContext(context));
UserHandle ownerUserHandle = UserHandle.getUserHandleForUid(ownerUid);
mContext = context.createContextAsUser(ownerUserHandle, 0);
mAssociationInfo = associationInfo;
@@ -337,11 +339,9 @@
}
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void close() {
- mContext.enforceCallingOrSelfPermission(
- android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
- "Permission required to close the virtual device");
-
+ super.close_enforcePermission();
synchronized (mVirtualDeviceLock) {
if (!mPerDisplayWakelocks.isEmpty()) {
mPerDisplayWakelocks.forEach((displayId, wakeLock) -> {
@@ -389,14 +389,12 @@
return mWindowPolicyControllers;
}
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void onAudioSessionStarting(int displayId,
@NonNull IAudioRoutingCallback routingCallback,
@Nullable IAudioConfigChangedCallback configChangedCallback) {
- mContext.enforceCallingOrSelfPermission(
- android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
- "Permission required to start audio session");
+ super.onAudioSessionStarting_enforcePermission();
synchronized (mVirtualDeviceLock) {
if (!mVirtualDisplayIds.contains(displayId)) {
throw new SecurityException(
@@ -413,12 +411,10 @@
}
}
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void onAudioSessionEnded() {
- mContext.enforceCallingOrSelfPermission(
- android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
- "Permission required to stop audio session");
+ super.onAudioSessionEnded_enforcePermission();
synchronized (mVirtualDeviceLock) {
if (mVirtualAudioController != null) {
mVirtualAudioController.stopListening();
@@ -428,9 +424,9 @@
}
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void createVirtualDpad(VirtualDpadConfig config, @NonNull IBinder deviceToken) {
- mContext.enforceCallingOrSelfPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
- "Permission required to create a virtual dpad");
+ super.createVirtualDpad_enforcePermission();
synchronized (mVirtualDeviceLock) {
if (!mVirtualDisplayIds.contains(config.getAssociatedDisplayId())) {
throw new SecurityException(
@@ -448,9 +444,9 @@
}
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void createVirtualKeyboard(VirtualKeyboardConfig config, @NonNull IBinder deviceToken) {
- mContext.enforceCallingOrSelfPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
- "Permission required to create a virtual keyboard");
+ super.createVirtualKeyboard_enforcePermission();
synchronized (mVirtualDeviceLock) {
if (!mVirtualDisplayIds.contains(config.getAssociatedDisplayId())) {
throw new SecurityException(
@@ -470,9 +466,9 @@
}
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void createVirtualMouse(VirtualMouseConfig config, @NonNull IBinder deviceToken) {
- mContext.enforceCallingOrSelfPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
- "Permission required to create a virtual mouse");
+ super.createVirtualMouse_enforcePermission();
synchronized (mVirtualDeviceLock) {
if (!mVirtualDisplayIds.contains(config.getAssociatedDisplayId())) {
throw new SecurityException(
@@ -490,10 +486,10 @@
}
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void createVirtualTouchscreen(VirtualTouchscreenConfig config,
@NonNull IBinder deviceToken) {
- mContext.enforceCallingOrSelfPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
- "Permission required to create a virtual touchscreen");
+ super.createVirtualTouchscreen_enforcePermission();
synchronized (mVirtualDeviceLock) {
if (!mVirtualDisplayIds.contains(config.getAssociatedDisplayId())) {
throw new SecurityException(
@@ -501,30 +497,29 @@
+ "this virtual device");
}
}
- int screenHeightPixels = config.getHeightInPixels();
- int screenWidthPixels = config.getWidthInPixels();
- if (screenHeightPixels <= 0 || screenWidthPixels <= 0) {
+ int screenHeight = config.getHeight();
+ int screenWidth = config.getWidth();
+ if (screenHeight <= 0 || screenWidth <= 0) {
throw new IllegalArgumentException(
"Cannot create a virtual touchscreen, screen dimensions must be positive. Got: "
- + "(" + screenWidthPixels + ", " + screenHeightPixels + ")");
+ + "(" + screenWidth + ", " + screenHeight + ")");
}
final long ident = Binder.clearCallingIdentity();
try {
mInputController.createTouchscreen(config.getInputDeviceName(), config.getVendorId(),
config.getProductId(), deviceToken, config.getAssociatedDisplayId(),
- screenHeightPixels, screenWidthPixels);
+ screenHeight, screenWidth);
} finally {
Binder.restoreCallingIdentity(ident);
}
}
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void createVirtualNavigationTouchpad(VirtualNavigationTouchpadConfig config,
@NonNull IBinder deviceToken) {
- mContext.enforceCallingOrSelfPermission(
- android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
- "Permission required to create a virtual navigation touchpad");
+ super.createVirtualNavigationTouchpad_enforcePermission();
synchronized (mVirtualDeviceLock) {
if (!mVirtualDisplayIds.contains(config.getAssociatedDisplayId())) {
throw new SecurityException(
@@ -552,11 +547,9 @@
}
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void unregisterInputDevice(IBinder token) {
- mContext.enforceCallingOrSelfPermission(
- android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
- "Permission required to unregister this input device");
-
+ super.unregisterInputDevice_enforcePermission();
final long ident = Binder.clearCallingIdentity();
try {
mInputController.unregisterInputDevice(token);
@@ -577,7 +570,9 @@
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public boolean sendDpadKeyEvent(IBinder token, VirtualKeyEvent event) {
+ super.sendDpadKeyEvent_enforcePermission();
final long ident = Binder.clearCallingIdentity();
try {
return mInputController.sendDpadKeyEvent(token, event);
@@ -587,7 +582,9 @@
}
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public boolean sendKeyEvent(IBinder token, VirtualKeyEvent event) {
+ super.sendKeyEvent_enforcePermission();
final long ident = Binder.clearCallingIdentity();
try {
return mInputController.sendKeyEvent(token, event);
@@ -597,7 +594,9 @@
}
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public boolean sendButtonEvent(IBinder token, VirtualMouseButtonEvent event) {
+ super.sendButtonEvent_enforcePermission();
final long ident = Binder.clearCallingIdentity();
try {
return mInputController.sendButtonEvent(token, event);
@@ -607,7 +606,9 @@
}
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public boolean sendTouchEvent(IBinder token, VirtualTouchEvent event) {
+ super.sendTouchEvent_enforcePermission();
final long ident = Binder.clearCallingIdentity();
try {
return mInputController.sendTouchEvent(token, event);
@@ -617,7 +618,9 @@
}
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public boolean sendRelativeEvent(IBinder token, VirtualMouseRelativeEvent event) {
+ super.sendRelativeEvent_enforcePermission();
final long ident = Binder.clearCallingIdentity();
try {
return mInputController.sendRelativeEvent(token, event);
@@ -627,7 +630,9 @@
}
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public boolean sendScrollEvent(IBinder token, VirtualMouseScrollEvent event) {
+ super.sendScrollEvent_enforcePermission();
final long ident = Binder.clearCallingIdentity();
try {
return mInputController.sendScrollEvent(token, event);
@@ -647,11 +652,9 @@
}
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void setShowPointerIcon(boolean showPointerIcon) {
- mContext.enforceCallingOrSelfPermission(
- android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
- "Permission required to unregister this input device");
-
+ super.setShowPointerIcon_enforcePermission();
final long ident = Binder.clearCallingIdentity();
try {
synchronized (mVirtualDeviceLock) {
@@ -666,12 +669,11 @@
}
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void createVirtualSensor(
@NonNull IBinder deviceToken,
@NonNull VirtualSensorConfig config) {
- mContext.enforceCallingOrSelfPermission(
- android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
- "Permission required to create a virtual sensor");
+ super.createVirtualSensor_enforcePermission();
Objects.requireNonNull(config);
Objects.requireNonNull(deviceToken);
final long ident = Binder.clearCallingIdentity();
@@ -683,10 +685,9 @@
}
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void unregisterSensor(@NonNull IBinder token) {
- mContext.enforceCallingOrSelfPermission(
- android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
- "Permission required to unregister a virtual sensor");
+ super.unregisterSensor_enforcePermission();
final long ident = Binder.clearCallingIdentity();
try {
mSensorController.unregisterSensor(token);
@@ -696,10 +697,9 @@
}
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public boolean sendSensorEvent(@NonNull IBinder token, @NonNull VirtualSensorEvent event) {
- mContext.enforceCallingOrSelfPermission(
- android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
- "Permission required to send a virtual sensor event");
+ super.sendSensorEvent_enforcePermission();
final long ident = Binder.clearCallingIdentity();
try {
return mSensorController.sendSensorEvent(token, event);
@@ -709,25 +709,23 @@
}
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void registerIntentInterceptor(IVirtualDeviceIntentInterceptor intentInterceptor,
IntentFilter filter) {
+ super.registerIntentInterceptor_enforcePermission();
Objects.requireNonNull(intentInterceptor);
Objects.requireNonNull(filter);
- mContext.enforceCallingOrSelfPermission(
- android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
- "Permission required to register intent interceptor");
synchronized (mVirtualDeviceLock) {
mIntentInterceptors.put(intentInterceptor.asBinder(), filter);
}
}
@Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void unregisterIntentInterceptor(
@NonNull IVirtualDeviceIntentInterceptor intentInterceptor) {
+ super.unregisterIntentInterceptor_enforcePermission();
Objects.requireNonNull(intentInterceptor);
- mContext.enforceCallingOrSelfPermission(
- android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
- "Permission required to unregister intent interceptor");
synchronized (mVirtualDeviceLock) {
mIntentInterceptors.remove(intentInterceptor.asBinder());
}
diff --git a/services/core/java/com/android/server/AlarmManagerInternal.java b/services/core/java/com/android/server/AlarmManagerInternal.java
index b6a8227..b7f2b8d 100644
--- a/services/core/java/com/android/server/AlarmManagerInternal.java
+++ b/services/core/java/com/android/server/AlarmManagerInternal.java
@@ -47,11 +47,11 @@
void remove(PendingIntent rec);
/**
- * Returns if the given package in the given user holds
- * {@link android.Manifest.permission#SCHEDULE_EXACT_ALARM} or
- * {@link android.Manifest.permission#USE_EXACT_ALARM}.
+ * Returns {@code true} if the given package in the given uid holds
+ * {@link android.Manifest.permission#USE_EXACT_ALARM} or
+ * {@link android.Manifest.permission#SCHEDULE_EXACT_ALARM} for apps targeting T or lower.
*/
- boolean hasExactAlarmPermission(String packageName, int uid);
+ boolean shouldGetBucketElevation(String packageName, int uid);
/**
* Sets the device's current time zone and time zone confidence.
diff --git a/services/core/java/com/android/server/MmsServiceBroker.java b/services/core/java/com/android/server/MmsServiceBroker.java
index 59db686..f149fda 100644
--- a/services/core/java/com/android/server/MmsServiceBroker.java
+++ b/services/core/java/com/android/server/MmsServiceBroker.java
@@ -45,6 +45,7 @@
import android.util.Slog;
import com.android.internal.telephony.IMms;
+import com.android.internal.telephony.TelephonyPermissions;
import com.android.server.uri.NeededUriGrants;
import com.android.server.uri.UriGrantsManagerInternal;
@@ -337,6 +338,14 @@
throws RemoteException {
Slog.d(TAG, "sendMessage() by " + callingPkg);
mContext.enforceCallingPermission(Manifest.permission.SEND_SMS, "Send MMS message");
+
+ // Check if user is associated with the subscription
+ if (!TelephonyPermissions.checkSubscriptionAssociatedWithUser(mContext, subId,
+ Binder.getCallingUserHandle())) {
+ // TODO(b/258629881): Display error dialog.
+ return;
+ }
+
if (getAppOpsManager().noteOp(AppOpsManager.OP_SEND_SMS, Binder.getCallingUid(),
callingPkg, attributionTag, null) != AppOpsManager.MODE_ALLOWED) {
Slog.e(TAG, callingPkg + " is not allowed to call sendMessage()");
diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java
index 9922818..7b8ca91 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -493,9 +493,7 @@
private boolean isLocationPermissionRequired(Set<Integer> events) {
return events.contains(TelephonyCallback.EVENT_CELL_LOCATION_CHANGED)
- || events.contains(TelephonyCallback.EVENT_CELL_INFO_CHANGED)
- || events.contains(TelephonyCallback.EVENT_REGISTRATION_FAILURE)
- || events.contains(TelephonyCallback.EVENT_BARRING_INFO_CHANGED);
+ || events.contains(TelephonyCallback.EVENT_CELL_INFO_CHANGED);
}
private boolean isPhoneStatePermissionRequired(Set<Integer> events, String callingPackage,
@@ -1002,6 +1000,10 @@
@Override
public void notifySubscriptionInfoChanged() {
if (VDBG) log("notifySubscriptionInfoChanged:");
+ if (!checkNotifyPermission("notifySubscriptionInfoChanged()")) {
+ return;
+ }
+
synchronized (mRecords) {
if (!mHasNotifySubscriptionInfoChangedOccurred) {
log("notifySubscriptionInfoChanged: first invocation mRecords.size="
@@ -1028,6 +1030,10 @@
@Override
public void notifyOpportunisticSubscriptionInfoChanged() {
if (VDBG) log("notifyOpptSubscriptionInfoChanged:");
+ if (!checkNotifyPermission("notifyOpportunisticSubscriptionInfoChanged()")) {
+ return;
+ }
+
synchronized (mRecords) {
if (!mHasNotifyOpportunisticSubscriptionInfoChangedOccurred) {
log("notifyOpptSubscriptionInfoChanged: first invocation mRecords.size="
@@ -1359,15 +1365,19 @@
}
if (events.contains(TelephonyCallback.EVENT_BARRING_INFO_CHANGED)) {
BarringInfo barringInfo = mBarringInfo.get(r.phoneId);
- BarringInfo biNoLocation = barringInfo != null
- ? barringInfo.createLocationInfoSanitizedCopy() : null;
- if (VDBG) log("listen: call onBarringInfoChanged=" + barringInfo);
- try {
- r.callback.onBarringInfoChanged(
- checkFineLocationAccess(r, Build.VERSION_CODES.BASE)
- ? barringInfo : biNoLocation);
- } catch (RemoteException ex) {
- remove(r.binder);
+ if (VDBG) {
+ log("listen: call onBarringInfoChanged=" + barringInfo);
+ }
+ if (barringInfo != null) {
+ BarringInfo biNoLocation = barringInfo.createLocationInfoSanitizedCopy();
+
+ try {
+ r.callback.onBarringInfoChanged(
+ checkFineLocationAccess(r, Build.VERSION_CODES.BASE)
+ ? barringInfo : biNoLocation);
+ } catch (RemoteException ex) {
+ remove(r.binder);
+ }
}
}
if (events.contains(
@@ -3618,29 +3628,21 @@
private boolean checkListenerPermission(Set<Integer> events, int subId, String callingPackage,
@Nullable String callingFeatureId, String message) {
- LocationAccessPolicy.LocationPermissionQuery.Builder locationQueryBuilder =
- new LocationAccessPolicy.LocationPermissionQuery.Builder()
- .setCallingPackage(callingPackage)
- .setCallingFeatureId(callingFeatureId)
- .setMethod(message + " events: " + events)
- .setCallingPid(Binder.getCallingPid())
- .setCallingUid(Binder.getCallingUid());
-
- boolean shouldCheckLocationPermissions = false;
-
+ boolean isPermissionCheckSuccessful = true;
if (isLocationPermissionRequired(events)) {
+ LocationAccessPolicy.LocationPermissionQuery.Builder locationQueryBuilder =
+ new LocationAccessPolicy.LocationPermissionQuery.Builder()
+ .setCallingPackage(callingPackage)
+ .setCallingFeatureId(callingFeatureId)
+ .setMethod(message + " events: " + events)
+ .setCallingPid(Binder.getCallingPid())
+ .setCallingUid(Binder.getCallingUid());
// Everything that requires fine location started in Q. So far...
locationQueryBuilder.setMinSdkVersionForFine(Build.VERSION_CODES.Q);
// If we're enforcing fine starting in Q, we also want to enforce coarse even for
// older SDK versions.
locationQueryBuilder.setMinSdkVersionForCoarse(0);
locationQueryBuilder.setMinSdkVersionForEnforcement(0);
- shouldCheckLocationPermissions = true;
- }
-
- boolean isPermissionCheckSuccessful = true;
-
- if (shouldCheckLocationPermissions) {
LocationAccessPolicy.LocationPermissionResult result =
LocationAccessPolicy.checkLocationPermission(
mContext, locationQueryBuilder.build());
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 907205c..4ebd714 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -3979,7 +3979,7 @@
= new Intent.FilterComparison(service.cloneFilter());
final ServiceRestarter res = new ServiceRestarter();
String processName = getProcessNameForService(sInfo, name, callingPackage,
- instanceName, false, inSharedIsolatedProcess);
+ instanceName, isSdkSandboxService, inSharedIsolatedProcess);
r = new ServiceRecord(mAm, className, name, definingPackageName,
definingUid, filter, sInfo, callingFromFg, res,
processName, sdkSandboxClientAppUid,
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index d7c4286..191460c 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -16610,8 +16610,8 @@
}
@Override
- public boolean startUserInBackgroundOnSecondaryDisplay(int userId, int displayId) {
- int[] displayIds = getSecondaryDisplayIdsForStartingBackgroundUsers();
+ public boolean startUserInBackgroundVisibleOnDisplay(int userId, int displayId) {
+ int[] displayIds = getDisplayIdsForStartingVisibleBackgroundUsers();
boolean validDisplay = false;
if (displayIds != null) {
for (int i = 0; i < displayIds.length; i++) {
@@ -16631,14 +16631,14 @@
displayId, mInjector);
}
// Permission check done inside UserController.
- return mInjector.startUserOnSecondaryDisplay(userId, displayId);
+ return mInjector.startUserInBackgroundVisibleOnDisplay(userId, displayId);
}
@Override
- public int[] getSecondaryDisplayIdsForStartingBackgroundUsers() {
- enforceCallingHasAtLeastOnePermission("getSecondaryDisplayIdsForStartingBackgroundUsers()",
+ public int[] getDisplayIdsForStartingVisibleBackgroundUsers() {
+ enforceCallingHasAtLeastOnePermission("getDisplayIdsForStartingVisibleBackgroundUsers()",
MANAGE_USERS, INTERACT_ACROSS_USERS);
- return mInjector.getSecondaryDisplayIdsForStartingBackgroundUsers();
+ return mInjector.getDisplayIdsForStartingVisibleBackgroundUsers();
}
/** @deprecated see the AIDL documentation {@inheritDoc} */
@@ -18837,7 +18837,7 @@
}
/**
- * Called by {@code AMS.getSecondaryDisplayIdsForStartingBackgroundUsers()}.
+ * Called by {@code AMS.getDisplayIdsForStartingVisibleBackgroundUsers()}.
*/
// NOTE: ideally Injector should have no complex logic, but if this logic was moved to AMS,
// it could not be tested with the existing ActivityManagerServiceTest (as DisplayManager,
@@ -18847,9 +18847,9 @@
// was added on FrameworksMockingServicesTests and hence uses Extended Mockito to mock
// final and static stuff)
@Nullable
- public int[] getSecondaryDisplayIdsForStartingBackgroundUsers() {
- if (!UserManager.isUsersOnSecondaryDisplaysEnabled()) {
- Slogf.w(TAG, "getSecondaryDisplayIdsForStartingBackgroundUsers(): not supported");
+ public int[] getDisplayIdsForStartingVisibleBackgroundUsers() {
+ if (!UserManager.isVisibleBackgroundUsersEnabled()) {
+ Slogf.w(TAG, "getDisplayIdsForStartingVisibleBackgroundUsers(): not supported");
return null;
}
@@ -18895,7 +18895,7 @@
// KitchenSink (or other app) can be used while running CTS tests on devices that
// don't have a real display.
// STOPSHIP: if not removed, it should at least be unit tested
- String testingProp = "fw.secondary_display_for_starting_users_for_testing_purposes";
+ String testingProp = "fw.display_ids_for_starting_users_for_testing_purposes";
int displayId = SystemProperties.getInt(testingProp, Display.DEFAULT_DISPLAY);
if (displayId != Display.DEFAULT_DISPLAY && displayId > 0) {
Slogf.w(TAG, "getSecondaryDisplayIdsForStartingBackgroundUsers(): no valid "
@@ -18903,8 +18903,8 @@
testingProp);
return new int[] { displayId };
}
- Slogf.e(TAG, "getSecondaryDisplayIdsForStartingBackgroundUsers(): no valid display"
- + " on %s", Arrays.toString(allDisplays));
+ Slogf.e(TAG, "getDisplayIdsForStartingBackgroundUsers(): no valid display on %s",
+ Arrays.toString(allDisplays));
return null;
}
@@ -18912,7 +18912,7 @@
int[] validDisplayIds = new int[numberValidDisplays];
System.arraycopy(displayIds, 0, validDisplayIds, 0, numberValidDisplays);
if (DEBUG_MU) {
- Slogf.d(TAG, "getSecondaryDisplayIdsForStartingBackgroundUsers(): returning "
+ Slogf.d(TAG, "getDisplayIdsForStartingVisibleBackgroundUsers(): returning "
+ "only valid displays (%d instead of %d): %s", numberValidDisplays,
displayIds.length, Arrays.toString(validDisplayIds));
}
@@ -18920,17 +18920,17 @@
}
if (DEBUG_MU) {
- Slogf.d(TAG, "getSecondaryDisplayIdsForStartingBackgroundUsers(): returning all "
- + "(but DEFAULT_DISPLAY) displays : %s", Arrays.toString(displayIds));
+ Slogf.d(TAG, "getDisplayIdsForStartingVisibleBackgroundUsers(): returning all (but "
+ + "DEFAULT_DISPLAY) displays : %s", Arrays.toString(displayIds));
}
return displayIds;
}
/**
- * Called by {@code AMS.startUserOnSecondaryDisplay()}.
+ * Called by {@code AMS.startUserInBackgroundVisibleOnDisplay()}.
*/
- public boolean startUserOnSecondaryDisplay(int userId, int displayId) {
- return mUserController.startUserOnSecondaryDisplay(userId, displayId);
+ public boolean startUserInBackgroundVisibleOnDisplay(int userId, int displayId) {
+ return mUserController.startUserVisibleOnDisplay(userId, displayId);
}
/**
diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
index 7f2e5fb..80684bf 100644
--- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
+++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
@@ -373,8 +373,8 @@
return runGetCurrentForegroundProcess(pw, mInternal, mTaskInterface);
case "reset-dropbox-rate-limiter":
return runResetDropboxRateLimiter();
- case "list-secondary-displays-for-starting-users":
- return runListSecondaryDisplaysForStartingUsers(pw);
+ case "list-displays-for-starting-users":
+ return runListDisplaysForStartingUsers(pw);
case "set-foreground-service-delegate":
return runSetForegroundServiceDelegate(pw);
default:
@@ -2157,11 +2157,11 @@
success = mInterface.startUserInBackgroundWithListener(userId, waiter);
displaySuffix = "";
} else {
- if (!UserManager.isUsersOnSecondaryDisplaysEnabled()) {
+ if (!UserManager.isVisibleBackgroundUsersEnabled()) {
pw.println("Not supported");
return -1;
}
- success = mInterface.startUserInBackgroundOnSecondaryDisplay(userId, displayId);
+ success = mInterface.startUserInBackgroundVisibleOnDisplay(userId, displayId);
displaySuffix = " on display " + displayId;
}
if (wait && success) {
@@ -3745,8 +3745,8 @@
return 0;
}
- int runListSecondaryDisplaysForStartingUsers(PrintWriter pw) throws RemoteException {
- int[] displayIds = mInterface.getSecondaryDisplayIdsForStartingBackgroundUsers();
+ int runListDisplaysForStartingUsers(PrintWriter pw) throws RemoteException {
+ int[] displayIds = mInterface.getDisplayIdsForStartingVisibleBackgroundUsers();
pw.println(displayIds == null || displayIds.length == 0
? "none"
: Arrays.toString(displayIds));
@@ -4116,7 +4116,7 @@
pw.println(" Set an app's background restriction level which in turn map to a app standby bucket.");
pw.println(" get-bg-restriction-level [--user <USER_ID>] <PACKAGE>");
pw.println(" Get an app's background restriction level.");
- pw.println(" list-secondary-displays-for-starting-users");
+ pw.println(" list-displays-for-starting-users");
pw.println(" Lists the id of displays that can be used to start users on "
+ "background.");
pw.println(" set-foreground-service-delegate [--user <USER_ID>] <PACKAGE> start|stop");
diff --git a/services/core/java/com/android/server/am/AppProfiler.java b/services/core/java/com/android/server/am/AppProfiler.java
index 45b11e1..e06ce2c9 100644
--- a/services/core/java/com/android/server/am/AppProfiler.java
+++ b/services/core/java/com/android/server/am/AppProfiler.java
@@ -2314,7 +2314,8 @@
void printCurrentCpuState(StringBuilder report, long time) {
synchronized (mProcessCpuTracker) {
- report.append(mProcessCpuTracker.printCurrentState(time));
+ // Only print the first 10 processes
+ report.append(mProcessCpuTracker.printCurrentState(time, /* maxProcesses= */10));
}
}
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index 23ed0c4..5b453b2 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -153,6 +153,7 @@
private final Context mContext;
private final BatteryExternalStatsWorker mWorker;
private final BatteryUsageStatsProvider mBatteryUsageStatsProvider;
+ private volatile boolean mMonitorEnabled = true;
private native void getRailEnergyPowerStats(RailStats railStats);
private CharsetDecoder mDecoderStat = StandardCharsets.UTF_8
@@ -481,6 +482,9 @@
@Override
public void monitor() {
+ if (!mMonitorEnabled) {
+ return;
+ }
synchronized (mLock) {
}
synchronized (mStats) {
@@ -2602,6 +2606,19 @@
@Override
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ // If the monitor() method is already holding a lock on mStats, no harm done: we will
+ // just wait for mStats in the dumpUnmonitored method below. In fact, we would want
+ // Watchdog to catch the service in the act in that situation. We just don't want the
+ // dump method itself to be blamed for holding the lock for too long.
+ mMonitorEnabled = false;
+ try {
+ dumpUnmonitored(fd, pw, args);
+ } finally {
+ mMonitorEnabled = true;
+ }
+ }
+
+ private void dumpUnmonitored(FileDescriptor fd, PrintWriter pw, String[] args) {
if (!DumpUtils.checkDumpAndUsageStatsPermission(mContext, TAG, pw)) return;
int flags = 0;
diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
index d53f8fb..b89084c 100644
--- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java
+++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
@@ -42,6 +42,7 @@
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayDeque;
import java.util.Iterator;
+import java.util.List;
import java.util.Objects;
/**
@@ -124,6 +125,12 @@
private final ArrayDeque<SomeArgs> mPendingOffload = new ArrayDeque<>(4);
/**
+ * List of all queues holding broadcasts that are waiting to be dispatched.
+ */
+ private final List<ArrayDeque<SomeArgs>> mPendingQueues = List.of(
+ mPendingUrgent, mPending, mPendingOffload);
+
+ /**
* Broadcast actively being dispatched to this process.
*/
private @Nullable BroadcastRecord mActive;
@@ -218,11 +225,11 @@
* given count of other receivers have reached a terminal state; typically
* used for ordered broadcasts and priority traunches.
*/
- public void enqueueOrReplaceBroadcast(@NonNull BroadcastRecord record, int recordIndex) {
+ public void enqueueOrReplaceBroadcast(@NonNull BroadcastRecord record, int recordIndex,
+ @NonNull BroadcastConsumer replacedBroadcastConsumer) {
if (record.isReplacePending()) {
- boolean didReplace = replaceBroadcastInQueue(mPending, record, recordIndex)
- || replaceBroadcastInQueue(mPendingUrgent, record, recordIndex)
- || replaceBroadcastInQueue(mPendingOffload, record, recordIndex);
+ final boolean didReplace = replaceBroadcast(record, recordIndex,
+ replacedBroadcastConsumer);
if (didReplace) {
return;
}
@@ -243,6 +250,26 @@
}
/**
+ * Searches from newest to oldest in the pending broadcast queues, and at the first matching
+ * pending broadcast it finds, replaces it in-place and returns -- does not attempt to handle
+ * "duplicate" broadcasts in the queue.
+ * <p>
+ * @return {@code true} if it found and replaced an existing record in the queue;
+ * {@code false} otherwise.
+ */
+ private boolean replaceBroadcast(@NonNull BroadcastRecord record, int recordIndex,
+ @NonNull BroadcastConsumer replacedBroadcastConsumer) {
+ final int count = mPendingQueues.size();
+ for (int i = 0; i < count; ++i) {
+ final ArrayDeque<SomeArgs> queue = mPendingQueues.get(i);
+ if (replaceBroadcastInQueue(queue, record, recordIndex, replacedBroadcastConsumer)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
* Searches from newest to oldest, and at the first matching pending broadcast
* it finds, replaces it in-place and returns -- does not attempt to handle
* "duplicate" broadcasts in the queue.
@@ -251,7 +278,8 @@
* {@code false} otherwise.
*/
private boolean replaceBroadcastInQueue(@NonNull ArrayDeque<SomeArgs> queue,
- @NonNull BroadcastRecord record, int recordIndex) {
+ @NonNull BroadcastRecord record, int recordIndex,
+ @NonNull BroadcastConsumer replacedBroadcastConsumer) {
final Iterator<SomeArgs> it = queue.descendingIterator();
final Object receiver = record.receivers.get(recordIndex);
while (it.hasNext()) {
@@ -262,12 +290,14 @@
if ((record.callingUid == testRecord.callingUid)
&& (record.userId == testRecord.userId)
&& record.intent.filterEquals(testRecord.intent)
- && isReceiverEquals(receiver, testReceiver)) {
+ && isReceiverEquals(receiver, testReceiver)
+ && testRecord.allReceiversPending()) {
// Exact match found; perform in-place swap
args.arg1 = record;
args.argi1 = recordIndex;
onBroadcastDequeued(testRecord, testRecordIndex);
onBroadcastEnqueued(record, recordIndex);
+ replacedBroadcastConsumer.accept(testRecord, testRecordIndex);
return true;
}
}
diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
index a850c8a..8f241b2 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
@@ -64,6 +64,7 @@
import android.os.SystemClock;
import android.os.UserHandle;
import android.text.format.DateUtils;
+import android.util.ArraySet;
import android.util.IndentingPrintWriter;
import android.util.MathUtils;
import android.util.Pair;
@@ -629,30 +630,26 @@
applyDeliveryGroupPolicy(r);
- if (r.isReplacePending()) {
- // Leave the skipped broadcasts intact in queue, so that we can
- // replace them at their current position during enqueue below
- forEachMatchingBroadcast(QUEUE_PREDICATE_ANY, (testRecord, testIndex) -> {
- // We only allow caller to replace broadcasts they enqueued
- return (r.callingUid == testRecord.callingUid)
- && (r.userId == testRecord.userId)
- && r.intent.filterEquals(testRecord.intent);
- }, mBroadcastConsumerSkipAndCanceled, false);
- }
-
r.enqueueTime = SystemClock.uptimeMillis();
r.enqueueRealTime = SystemClock.elapsedRealtime();
r.enqueueClockTime = System.currentTimeMillis();
+ final ArraySet<BroadcastRecord> replacedBroadcasts = new ArraySet<>();
+ final BroadcastConsumer replacedBroadcastConsumer =
+ (record, i) -> replacedBroadcasts.add(record);
for (int i = 0; i < r.receivers.size(); i++) {
final Object receiver = r.receivers.get(i);
final BroadcastProcessQueue queue = getOrCreateProcessQueue(
getReceiverProcessName(receiver), getReceiverUid(receiver));
- queue.enqueueOrReplaceBroadcast(r, i);
+ queue.enqueueOrReplaceBroadcast(r, i, replacedBroadcastConsumer);
updateRunnableList(queue);
enqueueUpdateRunningList();
}
+ // Skip any broadcasts that have been replaced by newer broadcasts with
+ // FLAG_RECEIVER_REPLACE_PENDING.
+ skipAndCancelReplacedBroadcasts(replacedBroadcasts);
+
// If nothing to dispatch, send any pending result immediately
if (r.receivers.isEmpty()) {
scheduleResultTo(r);
@@ -662,6 +659,17 @@
traceEnd(cookie);
}
+ private void skipAndCancelReplacedBroadcasts(ArraySet<BroadcastRecord> replacedBroadcasts) {
+ for (int i = 0; i < replacedBroadcasts.size(); ++i) {
+ final BroadcastRecord r = replacedBroadcasts.valueAt(i);
+ r.resultCode = Activity.RESULT_CANCELED;
+ r.resultData = null;
+ r.resultExtras = null;
+ scheduleResultTo(r);
+ notifyFinishBroadcast(r);
+ }
+ }
+
private void applyDeliveryGroupPolicy(@NonNull BroadcastRecord r) {
if (mService.shouldIgnoreDeliveryGroupPolicy(r.intent.getAction())) {
return;
diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java
index 24cf3d2..37225d1 100644
--- a/services/core/java/com/android/server/am/BroadcastRecord.java
+++ b/services/core/java/com/android/server/am/BroadcastRecord.java
@@ -907,6 +907,17 @@
return record.options == null ? null : record.options.getDeliveryGroupMatchingFilter();
}
+ /**
+ * Returns {@code true} if all the receivers are still waiting to receive the broadcast.
+ * Otherwise {@code false}.
+ */
+ boolean allReceiversPending() {
+ // We could also count the number of receivers with deliver state DELIVERY_PENDING, but
+ // checking how many receivers have finished (either skipped or cancelled) and whether or
+ // not the dispatch has been started should be sufficient.
+ return (terminalCount == 0 && dispatchTime <= 0);
+ }
+
@Override
public String toString() {
if (mCachedToString == null) {
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index e5123ef..234eec3 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -1567,8 +1567,8 @@
*
* @return whether the user was started
*/
- boolean startUserOnSecondaryDisplay(@UserIdInt int userId, int displayId) {
- checkCallingHasOneOfThosePermissions("startUserOnSecondaryDisplay",
+ boolean startUserVisibleOnDisplay(@UserIdInt int userId, int displayId) {
+ checkCallingHasOneOfThosePermissions("startUserOnDisplay",
MANAGE_USERS, INTERACT_ACROSS_USERS);
try {
@@ -3713,7 +3713,7 @@
}
boolean isUsersOnSecondaryDisplaysEnabled() {
- return UserManager.isUsersOnSecondaryDisplaysEnabled();
+ return UserManager.isVisibleBackgroundUsersEnabled();
}
void onUserStarting(@UserIdInt int userId) {
diff --git a/services/core/java/com/android/server/ambientcontext/AmbientContextManagerPerUserService.java b/services/core/java/com/android/server/ambientcontext/AmbientContextManagerPerUserService.java
index dcadd5f..a8066c1 100644
--- a/services/core/java/com/android/server/ambientcontext/AmbientContextManagerPerUserService.java
+++ b/services/core/java/com/android/server/ambientcontext/AmbientContextManagerPerUserService.java
@@ -16,9 +16,7 @@
package com.android.server.ambientcontext;
-import android.Manifest;
import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.app.ActivityOptions;
@@ -58,187 +56,105 @@
import java.util.function.Consumer;
/**
- * Per-user manager service for {@link AmbientContextEvent}s.
+ * Base per-user manager service for {@link AmbientContextEvent}s.
*/
-final class AmbientContextManagerPerUserService extends
+abstract class AmbientContextManagerPerUserService extends
AbstractPerUserSystemService<AmbientContextManagerPerUserService,
AmbientContextManagerService> {
- private static final String TAG = AmbientContextManagerPerUserService.class.getSimpleName();
+ private static final String TAG =
+ AmbientContextManagerPerUserService.class.getSimpleName();
- @Nullable
- @VisibleForTesting
- RemoteAmbientContextDetectionService mRemoteService;
-
- private ComponentName mComponentName;
+ /**
+ * The type of service.
+ */
+ enum ServiceType {
+ DEFAULT,
+ WEARABLE
+ }
AmbientContextManagerPerUserService(
@NonNull AmbientContextManagerService master, Object lock, @UserIdInt int userId) {
super(master, lock, userId);
}
- void destroyLocked() {
- Slog.d(TAG, "Trying to cancel the remote request. Reason: Service destroyed.");
- if (mRemoteService != null) {
- synchronized (mLock) {
- mRemoteService.unbind();
- mRemoteService = null;
- }
- }
- }
-
- @GuardedBy("mLock")
- private void ensureRemoteServiceInitiated() {
- if (mRemoteService == null) {
- mRemoteService = new RemoteAmbientContextDetectionService(
- getContext(), mComponentName, getUserId());
- }
- }
+ /**
+ * Returns the current bound AmbientContextManagerPerUserService component for this user.
+ */
+ abstract ComponentName getComponentName();
/**
- * get the currently bound component name.
+ * Sets the component name for the per user service.
*/
- @VisibleForTesting
- ComponentName getComponentName() {
- return mComponentName;
- }
-
+ abstract void setComponentName(ComponentName componentName);
/**
- * Resolves and sets up the service if it had not been done yet. Returns true if the service
- * is available.
+ * Ensures that the remote service is initiated.
*/
- @GuardedBy("mLock")
- @VisibleForTesting
- boolean setUpServiceIfNeeded() {
- if (mComponentName == null) {
- mComponentName = updateServiceInfoLocked();
- }
- if (mComponentName == null) {
- return false;
- }
-
- ServiceInfo serviceInfo;
- try {
- serviceInfo = AppGlobals.getPackageManager().getServiceInfo(
- mComponentName, 0, mUserId);
- } catch (RemoteException e) {
- Slog.w(TAG, "RemoteException while setting up service");
- return false;
- }
- return serviceInfo != null;
- }
-
- @Override
- protected ServiceInfo newServiceInfoLocked(@NonNull ComponentName serviceComponent)
- throws PackageManager.NameNotFoundException {
- ServiceInfo serviceInfo;
- try {
- serviceInfo = AppGlobals.getPackageManager().getServiceInfo(serviceComponent,
- 0, mUserId);
- if (serviceInfo != null) {
- final String permission = serviceInfo.permission;
- if (!Manifest.permission.BIND_AMBIENT_CONTEXT_DETECTION_SERVICE.equals(
- permission)) {
- throw new SecurityException(String.format(
- "Service %s requires %s permission. Found %s permission",
- serviceInfo.getComponentName(),
- Manifest.permission.BIND_AMBIENT_CONTEXT_DETECTION_SERVICE,
- serviceInfo.permission));
- }
- }
- } catch (RemoteException e) {
- throw new PackageManager.NameNotFoundException(
- "Could not get service for " + serviceComponent);
- }
- return serviceInfo;
- }
-
- @Override
- protected void dumpLocked(@NonNull String prefix, @NonNull PrintWriter pw) {
- synchronized (super.mLock) {
- super.dumpLocked(prefix, pw);
- }
- if (mRemoteService != null) {
- mRemoteService.dump("", new IndentingPrintWriter(pw, " "));
- }
- }
+ abstract void ensureRemoteServiceInitiated();
/**
- * Handles client registering as an observer. Only one registration is supported per app
- * package. A new registration from the same package will overwrite the previous registration.
+ * Returns the AmbientContextManagerPerUserService {@link ServiceType} for this user.
*/
- public void onRegisterObserver(AmbientContextEventRequest request,
- String packageName, IAmbientContextObserver observer) {
+ abstract ServiceType getServiceType();
+
+ /**
+ * Returns the int config for the consent component for the
+ * specific AmbientContextManagerPerUserService type
+ */
+ abstract int getConsentComponentConfig();
+
+ /**
+ * Returns the int config for the intent extra key for the
+ * caller's package name while requesting ambient context consent.
+ */
+ abstract int getAmbientContextPackageNameExtraKeyConfig();
+
+ /**
+ * Returns the int config for the Intent extra key for the event code int array while
+ * requesting ambient context consent.
+ */
+ abstract int getAmbientContextEventArrayExtraKeyConfig();
+
+ /**
+ * Returns the permission that is required to bind to this service.
+ */
+ abstract String getProtectedBindPermission();
+
+ /**
+ * Returns the remote service implementation for this user.
+ */
+ abstract RemoteAmbientDetectionService getRemoteService();
+
+ /**
+ * Clears the remote service.
+ */
+ abstract void clearRemoteService();
+
+ /**
+ * Called when there's an application with the callingPackage name is requesting for
+ * the AmbientContextDetection's service status.
+ *
+ * @param eventTypes the event types to query for
+ * @param callingPackage the package query for information
+ * @param statusCallback the callback to deliver the status on
+ */
+ public void onQueryServiceStatus(int[] eventTypes, String callingPackage,
+ RemoteCallback statusCallback) {
+ Slog.d(TAG, "Query event status of " + Arrays.toString(eventTypes)
+ + " for " + callingPackage);
synchronized (mLock) {
if (!setUpServiceIfNeeded()) {
Slog.w(TAG, "Detection service is not available at this moment.");
- completeRegistration(observer, AmbientContextManager.STATUS_SERVICE_UNAVAILABLE);
+ sendStatusCallback(statusCallback,
+ AmbientContextManager.STATUS_SERVICE_UNAVAILABLE);
return;
}
-
- // Register package and add to existing ClientRequests cache
- startDetection(request, packageName, observer);
- mMaster.newClientAdded(mUserId, request, packageName, observer);
- }
- }
-
- /**
- * Returns a RemoteCallback that handles the status from the detection service, and
- * sends results to the client callback.
- */
- private RemoteCallback getServerStatusCallback(Consumer<Integer> statusConsumer) {
- return new RemoteCallback(result -> {
- AmbientContextDetectionServiceStatus serviceStatus =
- (AmbientContextDetectionServiceStatus) result.get(
- AmbientContextDetectionServiceStatus.STATUS_RESPONSE_BUNDLE_KEY);
- final long token = Binder.clearCallingIdentity();
- try {
- int statusCode = serviceStatus.getStatusCode();
- statusConsumer.accept(statusCode);
- Slog.i(TAG, "Got detection status of " + statusCode
- + " for " + serviceStatus.getPackageName());
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- });
- }
-
- void startDetection(AmbientContextEventRequest request, String callingPackage,
- IAmbientContextObserver observer) {
- Slog.d(TAG, "Requested detection of " + request.getEventTypes());
- synchronized (mLock) {
- if (setUpServiceIfNeeded()) {
- ensureRemoteServiceInitiated();
- mRemoteService.startDetection(request, callingPackage,
- createDetectionResultRemoteCallback(),
- getServerStatusCallback(
- statusCode -> completeRegistration(observer, statusCode)));
- } else {
- Slog.w(TAG, "No valid component found for AmbientContextDetectionService");
- completeRegistration(observer,
- AmbientContextManager.STATUS_NOT_SUPPORTED);
- }
- }
- }
-
- /**
- * Sends the result response with the specified status to the callback.
- */
- static void sendStatusCallback(RemoteCallback statusCallback,
- @AmbientContextManager.StatusCode int statusCode) {
- Bundle bundle = new Bundle();
- bundle.putInt(
- AmbientContextManager.STATUS_RESPONSE_BUNDLE_KEY,
- statusCode);
- statusCallback.sendResult(bundle);
- }
-
- static void completeRegistration(IAmbientContextObserver observer, int statusCode) {
- try {
- observer.onRegistrationComplete(statusCode);
- } catch (RemoteException e) {
- Slog.w(TAG, "Failed to call IAmbientContextObserver.onRegistrationComplete: "
- + e.getMessage());
+ ensureRemoteServiceInitiated();
+ getRemoteService().queryServiceStatus(
+ eventTypes,
+ callingPackage,
+ getServerStatusCallback(
+ statusCode -> sendStatusCallback(statusCallback, statusCode)));
}
}
@@ -254,26 +170,9 @@
}
}
- public void onQueryServiceStatus(int[] eventTypes, String callingPackage,
- RemoteCallback statusCallback) {
- Slog.d(TAG, "Query event status of " + Arrays.toString(eventTypes)
- + " for " + callingPackage);
- synchronized (mLock) {
- if (!setUpServiceIfNeeded()) {
- Slog.w(TAG, "Detection service is not available at this moment.");
- sendStatusCallback(statusCallback,
- AmbientContextManager.STATUS_NOT_SUPPORTED);
- return;
- }
- ensureRemoteServiceInitiated();
- mRemoteService.queryServiceStatus(
- eventTypes,
- callingPackage,
- getServerStatusCallback(
- statusCode -> sendStatusCallback(statusCallback, statusCode)));
- }
- }
-
+ /**
+ * Starts the consent activity for the calling package and event types.
+ */
public void onStartConsentActivity(int[] eventTypes, String callingPackage) {
Slog.d(TAG, "Opening consent activity of " + Arrays.toString(eventTypes)
+ " for " + callingPackage);
@@ -315,9 +214,9 @@
try {
Context context = getContext();
String packageNameExtraKey = context.getResources().getString(
- com.android.internal.R.string.config_ambientContextPackageNameExtraKey);
+ getAmbientContextPackageNameExtraKeyConfig());
String eventArrayExtraKey = context.getResources().getString(
- com.android.internal.R.string.config_ambientContextEventArrayExtraKey);
+ getAmbientContextEventArrayExtraKeyConfig());
// Create consent activity intent with the calling package name and requested events
intent.setComponent(consentComponent);
@@ -344,37 +243,163 @@
}
/**
- * Returns the consent activity component from config lookup.
+ * Handles client registering as an observer. Only one registration is supported per app
+ * package. A new registration from the same package will overwrite the previous registration.
*/
- private ComponentName getConsentComponent() {
- Context context = getContext();
- String consentComponent = context.getResources().getString(
- com.android.internal.R.string.config_defaultAmbientContextConsentComponent);
- if (TextUtils.isEmpty(consentComponent)) {
- return null;
+ public void onRegisterObserver(AmbientContextEventRequest request,
+ String packageName, IAmbientContextObserver observer) {
+ synchronized (mLock) {
+ if (!setUpServiceIfNeeded()) {
+ Slog.w(TAG, "Detection service is not available at this moment.");
+ completeRegistration(observer, AmbientContextManager.STATUS_SERVICE_UNAVAILABLE);
+ return;
+ }
+
+ // Register package and add to existing ClientRequests cache
+ startDetection(request, packageName, observer);
+ mMaster.newClientAdded(mUserId, request, packageName, observer);
}
- Slog.i(TAG, "Consent component name: " + consentComponent);
- return ComponentName.unflattenFromString(consentComponent);
}
+ @Override
+ protected ServiceInfo newServiceInfoLocked(@NonNull ComponentName serviceComponent)
+ throws PackageManager.NameNotFoundException {
+ Slog.d(TAG, "newServiceInfoLocked with component name: "
+ + serviceComponent.getClassName());
+
+ if (getComponentName() == null
+ || !serviceComponent.getClassName().equals(getComponentName().getClassName())) {
+ Slog.d(TAG, "service name does not match this per user, returning...");
+ return null;
+ }
+
+ ServiceInfo serviceInfo;
+ try {
+ serviceInfo = AppGlobals.getPackageManager().getServiceInfo(serviceComponent,
+ 0, mUserId);
+ if (serviceInfo != null) {
+ final String permission = serviceInfo.permission;
+ if (!getProtectedBindPermission().equals(
+ permission)) {
+ throw new SecurityException(String.format(
+ "Service %s requires %s permission. Found %s permission",
+ serviceInfo.getComponentName(),
+ getProtectedBindPermission(),
+ serviceInfo.permission));
+ }
+ }
+ } catch (RemoteException e) {
+ throw new PackageManager.NameNotFoundException(
+ "Could not get service for " + serviceComponent);
+ }
+ return serviceInfo;
+ }
+
+ /**
+ * Dumps the remote service.
+ */
+ protected void dumpLocked(@NonNull String prefix, @NonNull PrintWriter pw) {
+ synchronized (super.mLock) {
+ super.dumpLocked(prefix, pw);
+ }
+ RemoteAmbientDetectionService remoteService = getRemoteService();
+ if (remoteService != null) {
+ remoteService.dump("", new IndentingPrintWriter(pw, " "));
+ }
+ }
+
+ /**
+ * Send request to the remote AmbientContextDetectionService impl to stop detecting the
+ * specified events. Intended for use by shell command for testing.
+ * Requires ACCESS_AMBIENT_CONTEXT_EVENT permission.
+ */
@VisibleForTesting
- void stopDetection(String packageName) {
+ protected void stopDetection(String packageName) {
Slog.d(TAG, "Stop detection for " + packageName);
synchronized (mLock) {
- if (mComponentName != null) {
+ if (getComponentName() != null) {
ensureRemoteServiceInitiated();
- mRemoteService.stopDetection(packageName);
+ RemoteAmbientDetectionService remoteService = getRemoteService();
+ remoteService.stopDetection(packageName);
}
}
}
/**
+ * Destroys this service and unbinds from the remote service.
+ */
+ protected void destroyLocked() {
+ Slog.d(TAG, "Trying to cancel the remote request. Reason: Service destroyed.");
+ RemoteAmbientDetectionService remoteService = getRemoteService();
+ if (remoteService != null) {
+ synchronized (mLock) {
+ remoteService.unbind();
+ clearRemoteService();
+ }
+ }
+ }
+
+ /**
+ * Send request to the remote AmbientContextDetectionService impl to start detecting the
+ * specified events. Intended for use by shell command for testing.
+ * Requires ACCESS_AMBIENT_CONTEXT_EVENT permission.
+ */
+ protected void startDetection(AmbientContextEventRequest request, String callingPackage,
+ IAmbientContextObserver observer) {
+ Slog.d(TAG, "Requested detection of " + request.getEventTypes());
+ synchronized (mLock) {
+ if (setUpServiceIfNeeded()) {
+ ensureRemoteServiceInitiated();
+ RemoteAmbientDetectionService remoteService = getRemoteService();
+ remoteService.startDetection(request, callingPackage,
+ createDetectionResultRemoteCallback(),
+ getServerStatusCallback(
+ statusCode -> completeRegistration(observer, statusCode)));
+ } else {
+ Slog.w(TAG, "No valid component found for AmbientContextDetectionService");
+ completeRegistration(observer,
+ AmbientContextManager.STATUS_NOT_SUPPORTED);
+ }
+ }
+ }
+
+ /**
+ * Notifies the observer the status of the registration.
+ *
+ * @param observer the observer to notify
+ * @param statusCode the status to notify
+ */
+ protected void completeRegistration(IAmbientContextObserver observer, int statusCode) {
+ try {
+ observer.onRegistrationComplete(statusCode);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Failed to call IAmbientContextObserver.onRegistrationComplete: "
+ + e.getMessage());
+ }
+ }
+
+ /**
+ * Sends the status on the {@link RemoteCallback}.
+ *
+ * @param statusCallback the callback to send the status on
+ * @param statusCode the status to send
+ */
+ protected void sendStatusCallback(RemoteCallback statusCallback,
+ @AmbientContextManager.StatusCode int statusCode) {
+ Bundle bundle = new Bundle();
+ bundle.putInt(
+ AmbientContextManager.STATUS_RESPONSE_BUNDLE_KEY,
+ statusCode);
+ statusCallback.sendResult(bundle);
+ }
+
+ /**
* Sends out the Intent to the client after the event is detected.
*
* @param pendingIntent Client's PendingIntent for callback
* @param events detected events from the detection service
*/
- void sendDetectionResultIntent(PendingIntent pendingIntent,
+ protected void sendDetectionResultIntent(PendingIntent pendingIntent,
List<AmbientContextEvent> events) {
Intent intent = new Intent();
intent.putExtra(AmbientContextManager.EXTRA_AMBIENT_CONTEXT_EVENTS,
@@ -384,8 +409,8 @@
BroadcastOptions options = BroadcastOptions.makeBasic();
options.setPendingIntentBackgroundActivityLaunchAllowed(false);
try {
- pendingIntent.send(getContext(), 0, intent, null, null, null,
- options.toBundle());
+ pendingIntent.send(getContext(), 0, intent, null,
+ null, null, options.toBundle());
Slog.i(TAG, "Sending PendingIntent to " + pendingIntent.getCreatorPackage() + ": "
+ events);
} catch (PendingIntent.CanceledException e) {
@@ -394,7 +419,7 @@
}
@NonNull
- RemoteCallback createDetectionResultRemoteCallback() {
+ protected RemoteCallback createDetectionResultRemoteCallback() {
return new RemoteCallback(result -> {
AmbientContextDetectionResult detectionResult =
(AmbientContextDetectionResult) result.get(
@@ -418,4 +443,80 @@
}
});
}
+
+ /**
+ * Resolves and sets up the service if it had not been done yet. Returns true if the service
+ * is available.
+ */
+ @GuardedBy("mLock")
+ @VisibleForTesting
+ private boolean setUpServiceIfNeeded() {
+ if (getComponentName() == null) {
+ ComponentName[] componentNames = updateServiceInfoListLocked();
+ if (componentNames == null || componentNames.length != 2) {
+ Slog.d(TAG, "updateServiceInfoListLocked returned incorrect componentNames");
+ return false;
+ }
+
+ switch (getServiceType()) {
+ case DEFAULT:
+ setComponentName(componentNames[0]);
+ break;
+ case WEARABLE:
+ setComponentName(componentNames[1]);
+ break;
+ default:
+ Slog.d(TAG, "updateServiceInfoListLocked returned unknown service types.");
+ return false;
+ }
+ }
+
+ if (getComponentName() == null) {
+ return false;
+ }
+
+ ServiceInfo serviceInfo;
+ try {
+ serviceInfo = AppGlobals.getPackageManager().getServiceInfo(
+ getComponentName(), 0, mUserId);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "RemoteException while setting up service");
+ return false;
+ }
+ return serviceInfo != null;
+ }
+
+ /**
+ * Returns a RemoteCallback that handles the status from the detection service, and
+ * sends results to the client callback.
+ */
+ private RemoteCallback getServerStatusCallback(Consumer<Integer> statusConsumer) {
+ return new RemoteCallback(result -> {
+ AmbientContextDetectionServiceStatus serviceStatus =
+ (AmbientContextDetectionServiceStatus) result.get(
+ AmbientContextDetectionServiceStatus.STATUS_RESPONSE_BUNDLE_KEY);
+ final long token = Binder.clearCallingIdentity();
+ try {
+ int statusCode = serviceStatus.getStatusCode();
+ statusConsumer.accept(statusCode);
+ Slog.i(TAG, "Got detection status of " + statusCode
+ + " for " + serviceStatus.getPackageName());
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ });
+ }
+
+ /**
+ * Returns the consent activity component from config lookup.
+ */
+ private ComponentName getConsentComponent() {
+ Context context = getContext();
+ String consentComponent = context.getResources().getString(getConsentComponentConfig());
+ if (TextUtils.isEmpty(consentComponent)) {
+ return null;
+ }
+ Slog.i(TAG, "Consent component name: " + consentComponent);
+ return ComponentName.unflattenFromString(consentComponent);
+ }
}
diff --git a/services/core/java/com/android/server/ambientcontext/AmbientContextManagerService.java b/services/core/java/com/android/server/ambientcontext/AmbientContextManagerService.java
index e205e84..5c18827 100644
--- a/services/core/java/com/android/server/ambientcontext/AmbientContextManagerService.java
+++ b/services/core/java/com/android/server/ambientcontext/AmbientContextManagerService.java
@@ -17,6 +17,7 @@
package com.android.server.ambientcontext;
import static android.provider.DeviceConfig.NAMESPACE_AMBIENT_CONTEXT_MANAGER_SERVICE;
+import static android.provider.DeviceConfig.NAMESPACE_WEARABLE_SENSING;
import android.Manifest;
import android.annotation.NonNull;
@@ -44,12 +45,16 @@
import com.android.internal.util.DumpUtils;
import com.android.server.LocalServices;
import com.android.server.SystemService;
+import com.android.server.ambientcontext.AmbientContextManagerPerUserService.ServiceType;
import com.android.server.infra.AbstractMasterSystemService;
import com.android.server.infra.FrameworkResourcesServiceNameResolver;
import com.android.server.pm.KnownPackages;
import java.io.FileDescriptor;
import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
@@ -62,6 +67,12 @@
AmbientContextManagerPerUserService> {
private static final String TAG = AmbientContextManagerService.class.getSimpleName();
private static final String KEY_SERVICE_ENABLED = "service_enabled";
+ private static final Set<Integer> DEFAULT_EVENT_SET = new HashSet<>(){{
+ add(AmbientContextEvent.EVENT_COUGH);
+ add(AmbientContextEvent.EVENT_SNORE);
+ add(AmbientContextEvent.EVENT_BACK_DOUBLE_TAP);
+ }
+ };
/** Default value in absence of {@link DeviceConfig} override. */
private static final boolean DEFAULT_SERVICE_ENABLED = true;
@@ -104,14 +115,16 @@
private final Context mContext;
boolean mIsServiceEnabled;
+ boolean mIsWearableServiceEnabled;
private Set<ClientRequest> mExistingClientRequests;
public AmbientContextManagerService(Context context) {
super(context,
new FrameworkResourcesServiceNameResolver(
context,
- R.string.config_defaultAmbientContextDetectionService),
- /*disallowProperty=*/null,
+ R.array.config_defaultAmbientContextServices,
+ /*isMultiple=*/ true),
+ /*disallowProperty=*/null,
PACKAGE_UPDATE_POLICY_REFRESH_EAGER
| /*To avoid high latency*/ PACKAGE_RESTART_POLICY_REFRESH_EAGER);
mContext = context;
@@ -134,6 +147,9 @@
mIsServiceEnabled = DeviceConfig.getBoolean(
NAMESPACE_AMBIENT_CONTEXT_MANAGER_SERVICE,
KEY_SERVICE_ENABLED, DEFAULT_SERVICE_ENABLED);
+ mIsWearableServiceEnabled = DeviceConfig.getBoolean(
+ NAMESPACE_WEARABLE_SENSING,
+ KEY_SERVICE_ENABLED, DEFAULT_SERVICE_ENABLED);
}
}
@@ -180,13 +196,62 @@
mIsServiceEnabled = DeviceConfig.getBoolean(
NAMESPACE_AMBIENT_CONTEXT_MANAGER_SERVICE,
KEY_SERVICE_ENABLED, DEFAULT_SERVICE_ENABLED);
+ mIsWearableServiceEnabled = DeviceConfig.getBoolean(
+ NAMESPACE_WEARABLE_SENSING,
+ KEY_SERVICE_ENABLED, DEFAULT_SERVICE_ENABLED);
}
}
@Override
protected AmbientContextManagerPerUserService newServiceLocked(int resolvedUserId,
boolean disabled) {
- return new AmbientContextManagerPerUserService(this, mLock, resolvedUserId);
+ // This service uses newServiceListLocked, it is configured in multiple mode.
+ return null;
+ }
+
+ @Override // from AbstractMasterSystemService
+ protected List<AmbientContextManagerPerUserService> newServiceListLocked(int resolvedUserId,
+ boolean disabled, String[] serviceNames) {
+ if (serviceNames == null || serviceNames.length == 0) {
+ Slog.i(TAG, "serviceNames sent in newServiceListLocked is null, or empty");
+ return new ArrayList<>();
+ }
+
+ List<AmbientContextManagerPerUserService> serviceList =
+ new ArrayList<>(serviceNames.length);
+ if (serviceNames.length == 2) {
+ Slog.i(TAG, "Not using default services, "
+ + "services provided for testing should be exactly two services.");
+ if (!isDefaultService(serviceNames[0]) && !isDefaultWearableService(serviceNames[1])) {
+ serviceList.add(new DefaultAmbientContextManagerPerUserService(
+ this, mLock, resolvedUserId,
+ AmbientContextManagerPerUserService.ServiceType.DEFAULT, serviceNames[0]));
+ serviceList.add(new WearableAmbientContextManagerPerUserService(
+ this, mLock, resolvedUserId,
+ AmbientContextManagerPerUserService.ServiceType.WEARABLE,
+ serviceNames[1]));
+ }
+ return serviceList;
+ } else {
+ Slog.i(TAG, "Incorrect number of services provided for testing.");
+ }
+
+ for (String serviceName : serviceNames) {
+ Slog.d(TAG, "newServicesListLocked with service name: " + serviceName);
+ if (getServiceType(serviceName)
+ == AmbientContextManagerPerUserService.ServiceType.WEARABLE) {
+ serviceList.add(new
+ WearableAmbientContextManagerPerUserService(
+ this, mLock, resolvedUserId,
+ AmbientContextManagerPerUserService.ServiceType.WEARABLE, serviceName));
+ } else {
+ serviceList.add(new DefaultAmbientContextManagerPerUserService(
+ this, mLock, resolvedUserId,
+ AmbientContextManagerPerUserService.ServiceType.DEFAULT, serviceName));
+ }
+
+ }
+ return serviceList;
}
@Override
@@ -239,7 +304,10 @@
mContext.enforceCallingOrSelfPermission(
Manifest.permission.ACCESS_AMBIENT_CONTEXT_EVENT, TAG);
synchronized (mLock) {
- final AmbientContextManagerPerUserService service = getServiceForUserLocked(userId);
+ AmbientContextManagerPerUserService service =
+ getAmbientContextManagerPerUserServiceForEventTypes(
+ userId,
+ request.getEventTypes());
if (service != null) {
service.startDetection(request, packageName, observer);
} else {
@@ -257,11 +325,19 @@
mContext.enforceCallingOrSelfPermission(
Manifest.permission.ACCESS_AMBIENT_CONTEXT_EVENT, TAG);
synchronized (mLock) {
- final AmbientContextManagerPerUserService service = getServiceForUserLocked(userId);
- if (service != null) {
- service.stopDetection(packageName);
- } else {
- Slog.i(TAG, "service not available for user_id: " + userId);
+ for (ClientRequest cr : mExistingClientRequests) {
+ Slog.i(TAG, "Looping through clients");
+ if (cr.hasUserIdAndPackageName(userId, packageName)) {
+ Slog.i(TAG, "we have an existing client");
+ AmbientContextManagerPerUserService service =
+ getAmbientContextManagerPerUserServiceForEventTypes(
+ userId, cr.getRequest().getEventTypes());
+ if (service != null) {
+ service.stopDetection(packageName);
+ } else {
+ Slog.i(TAG, "service not available for user_id: " + userId);
+ }
+ }
}
}
}
@@ -276,7 +352,9 @@
mContext.enforceCallingOrSelfPermission(
Manifest.permission.ACCESS_AMBIENT_CONTEXT_EVENT, TAG);
synchronized (mLock) {
- final AmbientContextManagerPerUserService service = getServiceForUserLocked(userId);
+ AmbientContextManagerPerUserService service =
+ getAmbientContextManagerPerUserServiceForEventTypes(
+ userId, intArrayToIntegerSet(eventTypes));
if (service != null) {
service.onQueryServiceStatus(eventTypes, packageName, callback);
} else {
@@ -287,14 +365,18 @@
private void restorePreviouslyEnabledClients(int userId) {
synchronized (mLock) {
- final AmbientContextManagerPerUserService service = getServiceForUserLocked(userId);
- for (ClientRequest clientRequest : mExistingClientRequests) {
- // Start detection for previously enabled clients
- if (clientRequest.hasUserId(userId)) {
- Slog.d(TAG, "Restoring detection for " + clientRequest.getPackageName());
- service.startDetection(clientRequest.getRequest(),
- clientRequest.getPackageName(),
- clientRequest.getObserver());
+ final List<AmbientContextManagerPerUserService> services =
+ getServiceListForUserLocked(userId);
+ for (AmbientContextManagerPerUserService service : services) {
+ for (ClientRequest clientRequest : mExistingClientRequests) {
+ // Start detection for previously enabled clients
+ if (clientRequest.hasUserId(userId)) {
+ Slog.d(TAG, "Restoring detection for "
+ + clientRequest.getPackageName());
+ service.startDetection(clientRequest.getRequest(),
+ clientRequest.getPackageName(),
+ clientRequest.getObserver());
+ }
}
}
}
@@ -303,9 +385,12 @@
/**
* Returns the AmbientContextManagerPerUserService component for this user.
*/
- public ComponentName getComponentName(@UserIdInt int userId) {
+ public ComponentName getComponentName(
+ @UserIdInt int userId,
+ AmbientContextManagerPerUserService.ServiceType serviceType) {
synchronized (mLock) {
- final AmbientContextManagerPerUserService service = getServiceForUserLocked(userId);
+ final AmbientContextManagerPerUserService service =
+ getServiceForType(userId, serviceType);
if (service != null) {
return service.getComponentName();
}
@@ -313,10 +398,132 @@
return null;
}
- private final class AmbientContextManagerInternal extends IAmbientContextManager.Stub {
- final AmbientContextManagerPerUserService mService = getServiceForUserLocked(
- UserHandle.getCallingUserId());
+ private AmbientContextManagerPerUserService getAmbientContextManagerPerUserServiceForEventTypes(
+ @UserIdInt int userId, Set<Integer> eventTypes) {
+ if (isWearableEventTypesOnly(eventTypes)) {
+ return getServiceForType(userId,
+ AmbientContextManagerPerUserService.ServiceType.WEARABLE);
+ } else {
+ return getServiceForType(userId,
+ AmbientContextManagerPerUserService.ServiceType.DEFAULT);
+ }
+ }
+ private Set<Integer> intArrayToIntegerSet(int[] eventTypes) {
+ Set<Integer> types = new HashSet<>();
+ for (Integer i : eventTypes) {
+ types.add(i);
+ }
+ return types;
+ }
+
+ private AmbientContextManagerPerUserService.ServiceType getServiceType(String serviceName) {
+ final String wearableService = mContext.getResources()
+ .getString(R.string.config_defaultWearableSensingService);
+ if (wearableService != null && wearableService.equals(serviceName)) {
+ return AmbientContextManagerPerUserService.ServiceType.WEARABLE;
+ }
+
+ return AmbientContextManagerPerUserService.ServiceType.DEFAULT;
+ }
+
+ private boolean isDefaultService(String serviceName) {
+ final String defaultService = mContext.getResources()
+ .getString(R.string.config_defaultAmbientContextDetectionService);
+ if (defaultService != null && defaultService.equals(serviceName)) {
+ return true;
+ }
+ return false;
+ }
+
+ private boolean isDefaultWearableService(String serviceName) {
+ final String wearableService = mContext.getResources()
+ .getString(R.string.config_defaultWearableSensingService);
+ if (wearableService != null && wearableService.equals(serviceName)) {
+ return true;
+ }
+ return false;
+ }
+
+ private AmbientContextManagerPerUserService getServiceForType(int userId,
+ AmbientContextManagerPerUserService.ServiceType serviceType) {
+ Slog.d(TAG, "getServiceForType with userid: "
+ + userId + " service type: " + serviceType.name());
+ synchronized (mLock) {
+ final List<AmbientContextManagerPerUserService> services =
+ getServiceListForUserLocked(userId);
+ Slog.d(TAG, "Services that are available: "
+ + (services == null ? "null services" : services.size()
+ + " number of services"));
+ if (services == null) {
+ return null;
+ }
+
+ for (AmbientContextManagerPerUserService service : services) {
+ if (service.getServiceType() == serviceType) {
+ return service;
+ }
+ }
+ }
+ return null;
+ }
+
+ private boolean isWearableEventTypesOnly(Set<Integer> eventTypes) {
+ if (eventTypes.isEmpty()) {
+ Slog.d(TAG, "empty event types.");
+ return false;
+ }
+ for (Integer eventType : eventTypes) {
+ if (eventType < AmbientContextEvent.EVENT_VENDOR_WEARABLE_START) {
+ Slog.d(TAG, "Not all events types are wearable events.");
+ return false;
+ }
+ }
+ Slog.d(TAG, "only wearable events.");
+ return true;
+ }
+
+ private boolean isWearableEventTypesOnly(int[] eventTypes) {
+ Integer[] events = intArrayToIntegerArray(eventTypes);
+ return isWearableEventTypesOnly(new HashSet<>(Arrays.asList(events)));
+ }
+
+ private boolean containsMixedEvents(int[] eventTypes) {
+ if (isWearableEventTypesOnly(eventTypes)) {
+ return false;
+ }
+ // It's not only wearable events so check if it's only default events.
+ for (Integer event : eventTypes) {
+ if (!DEFAULT_EVENT_SET.contains(event)) {
+ // mixed events.
+ Slog.w(TAG, "Received mixed event types, this is not supported.");
+ return true;
+ }
+ }
+ // Only default events.
+ return false;
+ }
+
+ private static int[] integerSetToIntArray(@NonNull Set<Integer> integerSet) {
+ int[] intArray = new int[integerSet.size()];
+ int i = 0;
+ for (Integer type : integerSet) {
+ intArray[i++] = type;
+ }
+ return intArray;
+ }
+
+ @NonNull
+ private static Integer[] intArrayToIntegerArray(@NonNull int[] integerSet) {
+ Integer[] intArray = new Integer[integerSet.length];
+ int i = 0;
+ for (Integer type : integerSet) {
+ intArray[i++] = type;
+ }
+ return intArray;
+ }
+
+ private final class AmbientContextManagerInternal extends IAmbientContextManager.Stub {
@Override
public void registerObserver(
AmbientContextEventRequest request, PendingIntent resultPendingIntent,
@@ -324,17 +531,21 @@
Objects.requireNonNull(request);
Objects.requireNonNull(resultPendingIntent);
Objects.requireNonNull(statusCallback);
+ AmbientContextManagerPerUserService service =
+ getAmbientContextManagerPerUserServiceForEventTypes(
+ UserHandle.getCallingUserId(),
+ request.getEventTypes());
// Wrap the PendingIntent and statusCallback in a IAmbientContextObserver to make the
// code unified
IAmbientContextObserver observer = new IAmbientContextObserver.Stub() {
@Override
public void onEvents(List<AmbientContextEvent> events) throws RemoteException {
- mService.sendDetectionResultIntent(resultPendingIntent, events);
+ service.sendDetectionResultIntent(resultPendingIntent, events);
}
@Override
public void onRegistrationComplete(int statusCode) throws RemoteException {
- AmbientContextManagerPerUserService.sendStatusCallback(statusCallback,
+ service.sendStatusCallback(statusCallback,
statusCode);
}
};
@@ -356,13 +567,37 @@
mContext.enforceCallingOrSelfPermission(
Manifest.permission.ACCESS_AMBIENT_CONTEXT_EVENT, TAG);
assertCalledByPackageOwner(packageName);
- if (!mIsServiceEnabled) {
- Slog.w(TAG, "Service not available.");
- AmbientContextManagerPerUserService.completeRegistration(observer,
+ AmbientContextManagerPerUserService service =
+ getAmbientContextManagerPerUserServiceForEventTypes(
+ UserHandle.getCallingUserId(),
+ request.getEventTypes());
+
+ if (service == null) {
+ Slog.w(TAG, "onRegisterObserver unavailable user_id: "
+ + UserHandle.getCallingUserId());
+ }
+
+ if (service.getServiceType() == ServiceType.DEFAULT && !mIsServiceEnabled) {
+ Slog.d(TAG, "Service not available.");
+ service.completeRegistration(observer,
AmbientContextManager.STATUS_SERVICE_UNAVAILABLE);
return;
}
- mService.onRegisterObserver(request, packageName, observer);
+ if (service.getServiceType() == ServiceType.WEARABLE && !mIsWearableServiceEnabled) {
+ Slog.d(TAG, "Wearable Service not available.");
+ service.completeRegistration(observer,
+ AmbientContextManager.STATUS_SERVICE_UNAVAILABLE);
+ return;
+ }
+ if (containsMixedEvents(integerSetToIntArray(request.getEventTypes()))) {
+ Slog.d(TAG, "AmbientContextEventRequest contains mixed events,"
+ + " this is not supported.");
+ service.completeRegistration(observer,
+ AmbientContextManager.STATUS_NOT_SUPPORTED);
+ return;
+ }
+
+ service.onRegisterObserver(request, packageName, observer);
}
@Override
@@ -370,7 +605,20 @@
mContext.enforceCallingOrSelfPermission(
Manifest.permission.ACCESS_AMBIENT_CONTEXT_EVENT, TAG);
assertCalledByPackageOwner(callingPackage);
- mService.onUnregisterObserver(callingPackage);
+
+ AmbientContextManagerPerUserService service = null;
+ for (ClientRequest cr : mExistingClientRequests) {
+ if (cr.getPackageName().equals(callingPackage)) {
+ service = getAmbientContextManagerPerUserServiceForEventTypes(
+ UserHandle.getCallingUserId(), cr.getRequest().getEventTypes());
+ if (service != null) {
+ service.onUnregisterObserver(callingPackage);
+ } else {
+ Slog.w(TAG, "onUnregisterObserver unavailable user_id: "
+ + UserHandle.getCallingUserId());
+ }
+ }
+ }
}
@Override
@@ -382,14 +630,40 @@
mContext.enforceCallingOrSelfPermission(
Manifest.permission.ACCESS_AMBIENT_CONTEXT_EVENT, TAG);
assertCalledByPackageOwner(callingPackage);
- if (!mIsServiceEnabled) {
- Slog.w(TAG, "Detection service not available.");
- AmbientContextManagerPerUserService.sendStatusCallback(statusCallback,
- AmbientContextManager.STATUS_SERVICE_UNAVAILABLE);
- return;
+ synchronized (mLock) {
+ AmbientContextManagerPerUserService service =
+ getAmbientContextManagerPerUserServiceForEventTypes(
+ UserHandle.getCallingUserId(), intArrayToIntegerSet(eventTypes));
+ if (service == null) {
+ Slog.w(TAG, "onQueryServiceStatus unavailable user_id: "
+ + UserHandle.getCallingUserId());
+ }
+
+ if (service.getServiceType() == ServiceType.DEFAULT && !mIsServiceEnabled) {
+ Slog.d(TAG, "Service not available.");
+ service.sendStatusCallback(statusCallback,
+ AmbientContextManager.STATUS_SERVICE_UNAVAILABLE);
+ return;
+ }
+ if (service.getServiceType() == ServiceType.WEARABLE
+ && !mIsWearableServiceEnabled) {
+ Slog.d(TAG, "Wearable Service not available.");
+ service.sendStatusCallback(statusCallback,
+ AmbientContextManager.STATUS_SERVICE_UNAVAILABLE);
+ return;
+ }
+
+ if (containsMixedEvents(eventTypes)) {
+ Slog.d(TAG, "AmbientContextEventRequest contains mixed events,"
+ + " this is not supported.");
+ service.sendStatusCallback(statusCallback,
+ AmbientContextManager.STATUS_NOT_SUPPORTED);
+ return;
+ }
+
+ service.onQueryServiceStatus(eventTypes, callingPackage,
+ statusCallback);
}
- mService.onQueryServiceStatus(eventTypes, callingPackage,
- statusCallback);
}
@Override
@@ -399,7 +673,23 @@
assertCalledByPackageOwner(callingPackage);
mContext.enforceCallingOrSelfPermission(
Manifest.permission.ACCESS_AMBIENT_CONTEXT_EVENT, TAG);
- mService.onStartConsentActivity(eventTypes, callingPackage);
+
+ if (containsMixedEvents(eventTypes)) {
+ Slog.d(TAG, "AmbientContextEventRequest contains mixed events,"
+ + " this is not supported.");
+ return;
+ }
+
+ AmbientContextManagerPerUserService service =
+ getAmbientContextManagerPerUserServiceForEventTypes(
+ UserHandle.getCallingUserId(), intArrayToIntegerSet(eventTypes));
+
+ if (service != null) {
+ service.onStartConsentActivity(eventTypes, callingPackage);
+ } else {
+ Slog.w(TAG, "startConsentActivity unavailable user_id: "
+ + UserHandle.getCallingUserId());
+ }
}
@Override
diff --git a/services/core/java/com/android/server/ambientcontext/AmbientContextShellCommand.java b/services/core/java/com/android/server/ambientcontext/AmbientContextShellCommand.java
index a3ffcde8..8808854 100644
--- a/services/core/java/com/android/server/ambientcontext/AmbientContextShellCommand.java
+++ b/services/core/java/com/android/server/ambientcontext/AmbientContextShellCommand.java
@@ -28,6 +28,7 @@
import android.os.RemoteCallback;
import android.os.RemoteException;
import android.os.ShellCommand;
+import android.util.Slog;
import java.io.PrintWriter;
import java.util.List;
@@ -36,6 +37,7 @@
* Shell command for {@link AmbientContextManagerService}.
*/
final class AmbientContextShellCommand extends ShellCommand {
+ private static final String TAG = AmbientContextShellCommand.class.getSimpleName();
private static final AmbientContextEventRequest REQUEST =
new AmbientContextEventRequest.Builder()
@@ -44,6 +46,20 @@
.addEventType(AmbientContextEvent.EVENT_BACK_DOUBLE_TAP)
.build();
+ private static final int WEARABLE_AMBIENT_CONTEXT_EVENT_FOR_TESTING =
+ AmbientContextEvent.EVENT_VENDOR_WEARABLE_START + 1;
+
+ private static final AmbientContextEventRequest WEARABLE_REQUEST =
+ new AmbientContextEventRequest.Builder()
+ .addEventType(WEARABLE_AMBIENT_CONTEXT_EVENT_FOR_TESTING)
+ .build();
+
+ private static final AmbientContextEventRequest MIXED_REQUEST =
+ new AmbientContextEventRequest.Builder()
+ .addEventType(AmbientContextEvent.EVENT_COUGH)
+ .addEventType(WEARABLE_AMBIENT_CONTEXT_EVENT_FOR_TESTING)
+ .build();
+
@NonNull
private final AmbientContextManagerService mService;
@@ -106,16 +122,26 @@
switch (cmd) {
case "start-detection":
return runStartDetection();
+ case "start-detection-wearable":
+ return runWearableStartDetection();
+ case "start-detection-mixed":
+ return runMixedStartDetection();
case "stop-detection":
return runStopDetection();
case "get-last-status-code":
return getLastStatusCode();
case "query-service-status":
return runQueryServiceStatus();
+ case "query-wearable-service-status":
+ return runQueryWearableServiceStatus();
+ case "query-mixed-service-status":
+ return runQueryMixedServiceStatus();
case "get-bound-package":
return getBoundPackageName();
case "set-temporary-service":
return setTemporaryService();
+ case "set-temporary-services":
+ return setTemporaryServices();
default:
return handleDefaultCommands(cmd);
}
@@ -127,6 +153,30 @@
mService.startDetection(
userId, REQUEST, packageName,
sTestableCallbackInternal.createAmbientContextObserver());
+ mService.newClientAdded(userId, REQUEST, packageName,
+ sTestableCallbackInternal.createAmbientContextObserver());
+ return 0;
+ }
+
+ private int runWearableStartDetection() {
+ final int userId = Integer.parseInt(getNextArgRequired());
+ final String packageName = getNextArgRequired();
+ mService.startDetection(
+ userId, WEARABLE_REQUEST, packageName,
+ sTestableCallbackInternal.createAmbientContextObserver());
+ mService.newClientAdded(userId, WEARABLE_REQUEST, packageName,
+ sTestableCallbackInternal.createAmbientContextObserver());
+ return 0;
+ }
+
+ private int runMixedStartDetection() {
+ final int userId = Integer.parseInt(getNextArgRequired());
+ final String packageName = getNextArgRequired();
+ mService.startDetection(
+ userId, MIXED_REQUEST, packageName,
+ sTestableCallbackInternal.createAmbientContextObserver());
+ mService.newClientAdded(userId, MIXED_REQUEST, packageName,
+ sTestableCallbackInternal.createAmbientContextObserver());
return 0;
}
@@ -148,6 +198,26 @@
return 0;
}
+ private int runQueryWearableServiceStatus() {
+ final int userId = Integer.parseInt(getNextArgRequired());
+ final String packageName = getNextArgRequired();
+ int[] types = new int[] {WEARABLE_AMBIENT_CONTEXT_EVENT_FOR_TESTING};
+ mService.queryServiceStatus(userId, packageName, types,
+ sTestableCallbackInternal.createRemoteStatusCallback());
+ return 0;
+ }
+
+ private int runQueryMixedServiceStatus() {
+ final int userId = Integer.parseInt(getNextArgRequired());
+ final String packageName = getNextArgRequired();
+ int[] types = new int[] {
+ AmbientContextEvent.EVENT_COUGH,
+ WEARABLE_AMBIENT_CONTEXT_EVENT_FOR_TESTING};
+ mService.queryServiceStatus(userId, packageName, types,
+ sTestableCallbackInternal.createRemoteStatusCallback());
+ return 0;
+ }
+
private int getLastStatusCode() {
final PrintWriter resultPrinter = getOutPrintWriter();
int lastStatus = sTestableCallbackInternal.getLastStatus();
@@ -163,20 +233,33 @@
pw.println(" Print this help text.");
pw.println();
pw.println(" start-detection USER_ID PACKAGE_NAME: Starts AmbientContextEvent detection.");
+ pw.println(" start-detection-wearable USER_ID PACKAGE_NAME: "
+ + "Starts AmbientContextEvent detection for wearable.");
+ pw.println(" start-detection-mixed USER_ID PACKAGE_NAME: "
+ + " Starts AmbientContextEvent detection for mixed events.");
pw.println(" stop-detection USER_ID PACKAGE_NAME: Stops AmbientContextEvent detection.");
pw.println(" get-last-status-code: Prints the latest request status code.");
pw.println(" query-service-status USER_ID PACKAGE_NAME: Prints the service status code.");
+ pw.println(" query-wearable-service-status USER_ID PACKAGE_NAME: "
+ + "Prints the service status code for wearable.");
+ pw.println(" query-mixed-service-status USER_ID PACKAGE_NAME: "
+ + "Prints the service status code for mixed events.");
pw.println(" get-bound-package USER_ID:"
+ " Print the bound package that implements the service.");
pw.println(" set-temporary-service USER_ID [PACKAGE_NAME] [COMPONENT_NAME DURATION]");
pw.println(" Temporarily (for DURATION ms) changes the service implementation.");
pw.println(" To reset, call with just the USER_ID argument.");
+ pw.println(" set-temporary-services USER_ID "
+ + "[FIRST_PACKAGE_NAME] [SECOND_PACKAGE_NAME] [COMPONENT_NAME DURATION]");
+ pw.println(" Temporarily (for DURATION ms) changes the service implementation.");
+ pw.println(" To reset, call with just the USER_ID argument.");
}
private int getBoundPackageName() {
final PrintWriter resultPrinter = getOutPrintWriter();
final int userId = Integer.parseInt(getNextArgRequired());
- final ComponentName componentName = mService.getComponentName(userId);
+ final ComponentName componentName = mService.getComponentName(userId,
+ AmbientContextManagerPerUserService.ServiceType.DEFAULT);
resultPrinter.println(componentName == null ? "" : componentName.getPackageName());
return 0;
}
@@ -188,6 +271,7 @@
if (serviceName == null) {
mService.resetTemporaryService(userId);
out.println("AmbientContextDetectionService temporary reset. ");
+ mService.setDefaultServiceEnabled(userId, true);
return 0;
}
@@ -197,4 +281,30 @@
+ " for " + duration + "ms");
return 0;
}
+
+ private int setTemporaryServices() {
+ String[] serviceNames = new String[2];
+ final PrintWriter out = getOutPrintWriter();
+ final int userId = Integer.parseInt(getNextArgRequired());
+ mService.setDefaultServiceEnabled(userId, false);
+ final String firstServiceName = getNextArg();
+ final String secondServiceName = getNextArg();
+ if (firstServiceName == null || secondServiceName == null) {
+ mService.resetTemporaryService(userId);
+ mService.setDefaultServiceEnabled(userId, true);
+ out.println("AmbientContextDetectionService temporary reset.");
+ return 0;
+ }
+ serviceNames[0] = firstServiceName;
+ serviceNames[1] = secondServiceName;
+ final int duration = Integer.parseInt(getNextArgRequired());
+ mService.setTemporaryServices(userId, serviceNames, duration);
+ Slog.w(TAG, "AmbientContextDetectionService temporarily set to " + serviceNames[0]
+ + " and " + serviceNames[1]
+ + " for " + duration + "ms");
+ out.println("AmbientContextDetectionService temporarily set to " + serviceNames[0]
+ + " and " + serviceNames[1]
+ + " for " + duration + "ms");
+ return 0;
+ }
}
diff --git a/services/core/java/com/android/server/ambientcontext/DefaultAmbientContextManagerPerUserService.java b/services/core/java/com/android/server/ambientcontext/DefaultAmbientContextManagerPerUserService.java
new file mode 100644
index 0000000..3fb29a0
--- /dev/null
+++ b/services/core/java/com/android/server/ambientcontext/DefaultAmbientContextManagerPerUserService.java
@@ -0,0 +1,112 @@
+/*
+ * 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.ambientcontext;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.app.ambientcontext.AmbientContextEvent;
+import android.content.ComponentName;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Per-user manager service for {@link AmbientContextEvent}s.
+ */
+public class DefaultAmbientContextManagerPerUserService extends
+ AmbientContextManagerPerUserService {
+ private static final String TAG =
+ DefaultAmbientContextManagerPerUserService.class.getSimpleName();
+
+ @Nullable
+ @VisibleForTesting
+ DefaultRemoteAmbientContextDetectionService mRemoteService;
+
+ private ComponentName mComponentName;
+ private final ServiceType mServiceType;
+ private final String mServiceName;
+
+ DefaultAmbientContextManagerPerUserService(
+ @NonNull AmbientContextManagerService master, Object lock,
+ @UserIdInt int userId, ServiceType serviceType, String serviceName) {
+ super(master, lock, userId);
+ this.mServiceType = serviceType;
+ this.mServiceName = serviceName;
+ this.mComponentName = ComponentName.unflattenFromString(mServiceName);
+ Slog.d(TAG, "Created DefaultAmbientContextManagerPerUserService"
+ + "and service type: " + mServiceType.name() + " and service name: " + serviceName);
+ }
+
+
+ @GuardedBy("mLock")
+ @Override
+ protected void ensureRemoteServiceInitiated() {
+ if (mRemoteService == null) {
+ mRemoteService = new DefaultRemoteAmbientContextDetectionService(
+ getContext(), mComponentName, getUserId());
+ }
+ }
+
+ @VisibleForTesting
+ @Override
+ ComponentName getComponentName() {
+ return mComponentName;
+ }
+
+ @Override
+ protected void setComponentName(ComponentName componentName) {
+ this.mComponentName = componentName;
+ }
+
+ @Override
+ protected RemoteAmbientDetectionService getRemoteService() {
+ return mRemoteService;
+ }
+
+ @Override
+ protected String getProtectedBindPermission() {
+ return Manifest.permission.BIND_AMBIENT_CONTEXT_DETECTION_SERVICE;
+ }
+
+ @Override
+ public ServiceType getServiceType() {
+ return mServiceType;
+ }
+
+ @Override
+ protected int getAmbientContextPackageNameExtraKeyConfig() {
+ return com.android.internal.R.string.config_ambientContextPackageNameExtraKey;
+ }
+
+ @Override
+ protected int getAmbientContextEventArrayExtraKeyConfig() {
+ return com.android.internal.R.string.config_ambientContextEventArrayExtraKey;
+ }
+
+ @Override
+ protected int getConsentComponentConfig() {
+ return com.android.internal.R.string.config_defaultAmbientContextConsentComponent;
+ }
+
+ @Override
+ protected void clearRemoteService() {
+ mRemoteService = null;
+ }
+}
diff --git a/services/core/java/com/android/server/ambientcontext/RemoteAmbientContextDetectionService.java b/services/core/java/com/android/server/ambientcontext/DefaultRemoteAmbientContextDetectionService.java
similarity index 78%
rename from services/core/java/com/android/server/ambientcontext/RemoteAmbientContextDetectionService.java
rename to services/core/java/com/android/server/ambientcontext/DefaultRemoteAmbientContextDetectionService.java
index 8aec752..ebbc4d1 100644
--- a/services/core/java/com/android/server/ambientcontext/RemoteAmbientContextDetectionService.java
+++ b/services/core/java/com/android/server/ambientcontext/DefaultRemoteAmbientContextDetectionService.java
@@ -32,13 +32,16 @@
import com.android.internal.infra.ServiceConnector;
-/** Manages the connection to the remote service. */
-final class RemoteAmbientContextDetectionService
- extends ServiceConnector.Impl<IAmbientContextDetectionService> {
- private static final String TAG =
- RemoteAmbientContextDetectionService.class.getSimpleName();
+import java.io.PrintWriter;
- RemoteAmbientContextDetectionService(Context context, ComponentName serviceName,
+/** Manages the connection to the remote service. */
+final class DefaultRemoteAmbientContextDetectionService
+ extends ServiceConnector.Impl<IAmbientContextDetectionService>
+ implements RemoteAmbientDetectionService {
+ private static final String TAG =
+ DefaultRemoteAmbientContextDetectionService.class.getSimpleName();
+
+ DefaultRemoteAmbientContextDetectionService(Context context, ComponentName serviceName,
int userId) {
super(context, new Intent(
AmbientContextDetectionService.SERVICE_INTERFACE).setComponent(serviceName),
@@ -55,14 +58,7 @@
return -1;
}
- /**
- * Asks the implementation to start detection.
- *
- * @param request The request with events to detect, and optional detection options.
- * @param packageName The app package that requested the detection
- * @param detectionResultCallback callback for detection results
- * @param statusCallback callback for service status
- */
+ @Override
public void startDetection(
@NonNull AmbientContextEventRequest request, String packageName,
RemoteCallback detectionResultCallback, RemoteCallback statusCallback) {
@@ -71,19 +67,13 @@
statusCallback));
}
- /**
- * Asks the implementation to stop detection.
- *
- * @param packageName stop detection for the given package
- */
+ @Override
public void stopDetection(String packageName) {
Slog.i(TAG, "Stop detection for " + packageName);
post(service -> service.stopDetection(packageName));
}
- /**
- * Asks the implementation to return the event status for the package.
- */
+ @Override
public void queryServiceStatus(
@AmbientContextEvent.EventCode int[] eventTypes,
String packageName,
@@ -91,4 +81,14 @@
Slog.i(TAG, "Query status for " + packageName);
post(service -> service.queryServiceStatus(eventTypes, packageName, callback));
}
+
+ @Override
+ public void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
+ super.dump(prefix, pw);
+ }
+
+ @Override
+ public void unbind() {
+ super.unbind();
+ }
}
diff --git a/services/core/java/com/android/server/ambientcontext/RemoteAmbientDetectionService.java b/services/core/java/com/android/server/ambientcontext/RemoteAmbientDetectionService.java
new file mode 100644
index 0000000..802718d
--- /dev/null
+++ b/services/core/java/com/android/server/ambientcontext/RemoteAmbientDetectionService.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.ambientcontext;
+
+import android.annotation.NonNull;
+import android.app.ambientcontext.AmbientContextEvent;
+import android.app.ambientcontext.AmbientContextEventRequest;
+import android.os.RemoteCallback;
+
+import java.io.PrintWriter;
+
+/**
+ * Interface for a remote service implementing Ambient Context Detection Service capabilities.
+ */
+interface RemoteAmbientDetectionService {
+ /**
+ * Asks the implementation to start detection.
+ *
+ * @param request The request with events to detect, and optional detection options.
+ * @param packageName The app package that requested the detection
+ * @param detectionResultCallback callback for detection results
+ * @param statusCallback callback for service status
+ */
+ void startDetection(
+ @NonNull AmbientContextEventRequest request, String packageName,
+ RemoteCallback detectionResultCallback, RemoteCallback statusCallback);
+
+ /**
+ * Asks the implementation to stop detection.
+ *
+ * @param packageName stop detection for the given package
+ */
+ void stopDetection(String packageName);
+
+ /**
+ * Asks the implementation to return the event status for the package.
+ */
+ void queryServiceStatus(
+ @AmbientContextEvent.EventCode int[] eventTypes,
+ String packageName,
+ RemoteCallback callback);
+
+ /**
+ * Dumps the RemoteAmbientDetectionService.
+ */
+ void dump(@NonNull String prefix, @NonNull PrintWriter pw);
+
+ /**
+ * Unbinds from the remote service.
+ */
+ void unbind();
+}
diff --git a/services/core/java/com/android/server/ambientcontext/RemoteAmbientContextDetectionService.java b/services/core/java/com/android/server/ambientcontext/RemoteWearableSensingService.java
similarity index 65%
copy from services/core/java/com/android/server/ambientcontext/RemoteAmbientContextDetectionService.java
copy to services/core/java/com/android/server/ambientcontext/RemoteWearableSensingService.java
index 8aec752..3c6ff98 100644
--- a/services/core/java/com/android/server/ambientcontext/RemoteAmbientContextDetectionService.java
+++ b/services/core/java/com/android/server/ambientcontext/RemoteWearableSensingService.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021 The Android Open Source Project
+ * 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.
@@ -26,24 +26,27 @@
import android.content.Context;
import android.content.Intent;
import android.os.RemoteCallback;
-import android.service.ambientcontext.AmbientContextDetectionService;
-import android.service.ambientcontext.IAmbientContextDetectionService;
+import android.service.wearable.IWearableSensingService;
+import android.service.wearable.WearableSensingService;
import android.util.Slog;
import com.android.internal.infra.ServiceConnector;
-/** Manages the connection to the remote service. */
-final class RemoteAmbientContextDetectionService
- extends ServiceConnector.Impl<IAmbientContextDetectionService> {
- private static final String TAG =
- RemoteAmbientContextDetectionService.class.getSimpleName();
+import java.io.PrintWriter;
- RemoteAmbientContextDetectionService(Context context, ComponentName serviceName,
+/** Manages the connection to the remote wearable sensing service. */
+final class RemoteWearableSensingService
+ extends ServiceConnector.Impl<IWearableSensingService>
+ implements RemoteAmbientDetectionService {
+ private static final String TAG =
+ RemoteWearableSensingService.class.getSimpleName();
+
+ RemoteWearableSensingService(Context context, ComponentName serviceName,
int userId) {
super(context, new Intent(
- AmbientContextDetectionService.SERVICE_INTERFACE).setComponent(serviceName),
+ WearableSensingService.SERVICE_INTERFACE).setComponent(serviceName),
BIND_FOREGROUND_SERVICE | BIND_INCLUDE_CAPABILITIES, userId,
- IAmbientContextDetectionService.Stub::asInterface);
+ IWearableSensingService.Stub::asInterface);
// Bind right away
connect();
@@ -55,14 +58,7 @@
return -1;
}
- /**
- * Asks the implementation to start detection.
- *
- * @param request The request with events to detect, and optional detection options.
- * @param packageName The app package that requested the detection
- * @param detectionResultCallback callback for detection results
- * @param statusCallback callback for service status
- */
+ @Override
public void startDetection(
@NonNull AmbientContextEventRequest request, String packageName,
RemoteCallback detectionResultCallback, RemoteCallback statusCallback) {
@@ -71,19 +67,13 @@
statusCallback));
}
- /**
- * Asks the implementation to stop detection.
- *
- * @param packageName stop detection for the given package
- */
+ @Override
public void stopDetection(String packageName) {
Slog.i(TAG, "Stop detection for " + packageName);
post(service -> service.stopDetection(packageName));
}
- /**
- * Asks the implementation to return the event status for the package.
- */
+ @Override
public void queryServiceStatus(
@AmbientContextEvent.EventCode int[] eventTypes,
String packageName,
@@ -91,4 +81,14 @@
Slog.i(TAG, "Query status for " + packageName);
post(service -> service.queryServiceStatus(eventTypes, packageName, callback));
}
+
+ @Override
+ public void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
+ super.dump(prefix, pw);
+ }
+
+ @Override
+ public void unbind() {
+ super.unbind();
+ }
}
diff --git a/services/core/java/com/android/server/ambientcontext/WearableAmbientContextManagerPerUserService.java b/services/core/java/com/android/server/ambientcontext/WearableAmbientContextManagerPerUserService.java
new file mode 100644
index 0000000..36abd26
--- /dev/null
+++ b/services/core/java/com/android/server/ambientcontext/WearableAmbientContextManagerPerUserService.java
@@ -0,0 +1,112 @@
+/*
+ * 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.ambientcontext;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.app.ambientcontext.AmbientContextEvent;
+import android.content.ComponentName;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Per-user manager service for {@link AmbientContextEvent}s for the Wearable Sensing.
+ */
+public class WearableAmbientContextManagerPerUserService extends
+ AmbientContextManagerPerUserService {
+ private static final String TAG =
+ WearableAmbientContextManagerPerUserService.class.getSimpleName();
+
+ @Nullable
+ @VisibleForTesting
+ RemoteWearableSensingService mRemoteService;
+
+ private ComponentName mComponentName;
+ private final ServiceType mServiceType;
+ private final String mServiceName;
+
+ WearableAmbientContextManagerPerUserService(
+ @NonNull AmbientContextManagerService master, Object lock,
+ @UserIdInt int userId, ServiceType serviceType, String serviceName) {
+ super(master, lock, userId);
+ this.mServiceType = serviceType;
+ this.mServiceName = serviceName;
+ this.mComponentName = ComponentName.unflattenFromString(mServiceName);
+ Slog.d(TAG, "Created WearableAmbientContextManagerPerUserService"
+ + "and service type: " + mServiceType.name() + " and service name: " + serviceName);
+ }
+
+ @GuardedBy("mLock")
+ @Override
+ protected void ensureRemoteServiceInitiated() {
+ if (mRemoteService == null) {
+ mRemoteService = new RemoteWearableSensingService(
+ getContext(), mComponentName, getUserId());
+ }
+ }
+
+ @VisibleForTesting
+ @Override
+ ComponentName getComponentName() {
+ return mComponentName;
+ }
+
+ @Override
+ protected void setComponentName(ComponentName componentName) {
+ this.mComponentName = componentName;
+ }
+
+
+ @Override
+ protected RemoteAmbientDetectionService getRemoteService() {
+ return mRemoteService;
+ }
+
+ @Override
+ protected String getProtectedBindPermission() {
+ return Manifest.permission.BIND_WEARABLE_SENSING_SERVICE;
+ }
+
+ @Override
+ public ServiceType getServiceType() {
+ return mServiceType;
+ }
+
+ @Override
+ protected int getAmbientContextPackageNameExtraKeyConfig() {
+ return com.android.internal.R.string.config_wearableAmbientContextPackageNameExtraKey;
+ }
+
+ @Override
+ protected int getAmbientContextEventArrayExtraKeyConfig() {
+ return com.android.internal.R.string.config_wearableAmbientContextEventArrayExtraKey;
+ }
+
+ @Override
+ protected int getConsentComponentConfig() {
+ return com.android.internal.R.string.config_defaultWearableSensingConsentComponent;
+ }
+
+ @Override
+ protected void clearRemoteService() {
+ mRemoteService = null;
+ }
+}
diff --git a/services/core/java/com/android/server/appop/AppOpsCheckingServiceLoggingDecorator.java b/services/core/java/com/android/server/appop/AppOpsCheckingServiceLoggingDecorator.java
new file mode 100644
index 0000000..ac479b2
--- /dev/null
+++ b/services/core/java/com/android/server/appop/AppOpsCheckingServiceLoggingDecorator.java
@@ -0,0 +1,185 @@
+/*
+ * 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.util.ArraySet;
+import android.util.Log;
+import android.util.SparseBooleanArray;
+import android.util.SparseIntArray;
+
+import java.io.PrintWriter;
+
+/**
+ * Logging decorator for {@link AppOpsCheckingServiceInterface}.
+ */
+public class AppOpsCheckingServiceLoggingDecorator implements AppOpsCheckingServiceInterface {
+ private static final String LOG_TAG =
+ AppOpsCheckingServiceLoggingDecorator.class.getSimpleName();
+
+ @NonNull
+ private final AppOpsCheckingServiceInterface mService;
+
+ public AppOpsCheckingServiceLoggingDecorator(@NonNull AppOpsCheckingServiceInterface service) {
+ mService = service;
+ }
+
+ @Override
+ public SparseIntArray getNonDefaultUidModes(int uid) {
+ Log.i(LOG_TAG, "getNonDefaultUidModes(uid = " + uid + ")");
+ return mService.getNonDefaultUidModes(uid);
+ }
+
+ @Override
+ public int getUidMode(int uid, int op) {
+ Log.i(LOG_TAG, "getUidMode(uid = " + uid + ", op = " + op + ")");
+ return mService.getUidMode(uid, op);
+ }
+
+ @Override
+ public boolean setUidMode(int uid, int op, int mode) {
+ Log.i(LOG_TAG, "setUidMode(uid = " + uid + ", op = " + op + ", mode = " + mode + ")");
+ return mService.setUidMode(uid, op, mode);
+ }
+
+ @Override
+ public int getPackageMode(@NonNull String packageName, int op, int userId) {
+ Log.i(LOG_TAG, "getPackageMode(packageName = " + packageName + ", op = " + op
+ + ", userId = " + userId + ")");
+ return mService.getPackageMode(packageName, op, userId);
+ }
+
+ @Override
+ public void setPackageMode(@NonNull String packageName, int op, int mode, int userId) {
+ Log.i(LOG_TAG, "setPackageMode(packageName = " + packageName + ", op = " + op + ", mode = "
+ + mode + ", userId = " + userId + ")");
+ mService.setPackageMode(packageName, op, mode, userId);
+ }
+
+ @Override
+ public boolean removePackage(@NonNull String packageName, int userId) {
+ Log.i(LOG_TAG, "removePackage(packageName = " + packageName + ", userId = " + userId + ")");
+ return mService.removePackage(packageName, userId);
+ }
+
+ @Override
+ public void removeUid(int uid) {
+ Log.i(LOG_TAG, "removeUid(uid = " + uid + ")");
+ mService.removeUid(uid);
+ }
+
+ @Override
+ public boolean areUidModesDefault(int uid) {
+ Log.i(LOG_TAG, "areUidModesDefault(uid = " + uid + ")");
+ return mService.areUidModesDefault(uid);
+ }
+
+ @Override
+ public boolean arePackageModesDefault(String packageName, int userId) {
+ Log.i(LOG_TAG, "arePackageModesDefault(packageName = " + packageName + ", userId = "
+ + userId + ")");
+ return mService.arePackageModesDefault(packageName, userId);
+ }
+
+ @Override
+ public void clearAllModes() {
+ Log.i(LOG_TAG, "clearAllModes()");
+ mService.clearAllModes();
+ }
+
+ @Override
+ public void startWatchingOpModeChanged(@NonNull OnOpModeChangedListener changedListener,
+ int op) {
+ Log.i(LOG_TAG, "startWatchingOpModeChanged(changedListener = " + changedListener + ", op = "
+ + op + ")");
+ mService.startWatchingOpModeChanged(changedListener, op);
+ }
+
+ @Override
+ public void startWatchingPackageModeChanged(@NonNull OnOpModeChangedListener changedListener,
+ @NonNull String packageName) {
+ Log.i(LOG_TAG, "startWatchingPackageModeChanged(changedListener = " + changedListener
+ + ", packageName = " + packageName + ")");
+ mService.startWatchingPackageModeChanged(changedListener, packageName);
+ }
+
+ @Override
+ public void removeListener(@NonNull OnOpModeChangedListener changedListener) {
+ Log.i(LOG_TAG, "removeListener(changedListener = " + changedListener + ")");
+ mService.removeListener(changedListener);
+ }
+
+ @Override
+ public ArraySet<OnOpModeChangedListener> getOpModeChangedListeners(int op) {
+ Log.i(LOG_TAG, "getOpModeChangedListeners(op = " + op + ")");
+ return mService.getOpModeChangedListeners(op);
+ }
+
+ @Override
+ public ArraySet<OnOpModeChangedListener> getPackageModeChangedListeners(
+ @NonNull String packageName) {
+ Log.i(LOG_TAG, "getPackageModeChangedListeners(packageName = " + packageName + ")");
+ return mService.getPackageModeChangedListeners(packageName);
+ }
+
+ @Override
+ public void notifyWatchersOfChange(int op, int uid) {
+ Log.i(LOG_TAG, "notifyWatchersOfChange(op = " + op + ", uid = " + uid + ")");
+ mService.notifyWatchersOfChange(op, uid);
+ }
+
+ @Override
+ public void notifyOpChanged(@NonNull OnOpModeChangedListener changedListener, int op, int uid,
+ @Nullable String packageName) {
+ Log.i(LOG_TAG, "notifyOpChanged(changedListener = " + changedListener + ", op = " + op
+ + ", uid = " + uid + ", packageName = " + packageName + ")");
+ mService.notifyOpChanged(changedListener, op, uid, packageName);
+ }
+
+ @Override
+ public void notifyOpChangedForAllPkgsInUid(int op, int uid, boolean onlyForeground,
+ @Nullable OnOpModeChangedListener callbackToIgnore) {
+ Log.i(LOG_TAG, "notifyOpChangedForAllPkgsInUid(op = " + op + ", uid = " + uid
+ + ", onlyForeground = " + onlyForeground + ", callbackToIgnore = "
+ + callbackToIgnore + ")");
+ mService.notifyOpChangedForAllPkgsInUid(op, uid, onlyForeground, callbackToIgnore);
+ }
+
+ @Override
+ public SparseBooleanArray evalForegroundUidOps(int uid, SparseBooleanArray foregroundOps) {
+ Log.i(LOG_TAG, "evalForegroundUidOps(uid = " + uid + ", foregroundOps = " + foregroundOps
+ + ")");
+ return mService.evalForegroundUidOps(uid, foregroundOps);
+ }
+
+ @Override
+ public SparseBooleanArray evalForegroundPackageOps(String packageName,
+ SparseBooleanArray foregroundOps, int userId) {
+ Log.i(LOG_TAG, "evalForegroundPackageOps(packageName = " + packageName
+ + ", foregroundOps = " + foregroundOps + ", userId = " + userId + ")");
+ return mService.evalForegroundPackageOps(packageName, foregroundOps, userId);
+ }
+
+ @Override
+ public boolean dumpListeners(int dumpOp, int dumpUid, String dumpPackage,
+ PrintWriter printWriter) {
+ Log.i(LOG_TAG, "dumpListeners(dumpOp = " + dumpOp + ", dumpUid = " + dumpUid
+ + ", dumpPackage = " + dumpPackage + ", printWriter = " + printWriter + ")");
+ return mService.dumpListeners(dumpOp, dumpUid, dumpPackage, printWriter);
+ }
+}
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index 18839a8..9c6cae3 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -956,6 +956,8 @@
}
mAppOpsCheckingService =
new AppOpsCheckingServiceImpl(this, this, handler, context, mSwitchedOps);
+ //mAppOpsCheckingService = new AppOpsCheckingServiceLoggingDecorator(
+ // LocalServices.getService(AppOpsCheckingServiceInterface.class));
mAppOpsRestrictions = new AppOpsRestrictionsImpl(context, handler,
mAppOpsCheckingService);
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index 418027f..9877ed3 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -36,6 +36,7 @@
import android.media.IAudioRoutesObserver;
import android.media.ICapturePresetDevicesRoleDispatcher;
import android.media.ICommunicationDeviceDispatcher;
+import android.media.IStrategyNonDefaultDevicesDispatcher;
import android.media.IStrategyPreferredDevicesDispatcher;
import android.media.MediaMetrics;
import android.media.audiopolicy.AudioProductStrategy;
@@ -871,6 +872,16 @@
return mDeviceInventory.removePreferredDevicesForStrategySync(strategy);
}
+ /*package*/ int setDeviceAsNonDefaultForStrategySync(int strategy,
+ @NonNull AudioDeviceAttributes device) {
+ return mDeviceInventory.setDeviceAsNonDefaultForStrategySync(strategy, device);
+ }
+
+ /*package*/ int removeDeviceAsNonDefaultForStrategySync(int strategy,
+ @NonNull AudioDeviceAttributes device) {
+ return mDeviceInventory.removeDeviceAsNonDefaultForStrategySync(strategy, device);
+ }
+
/*package*/ void registerStrategyPreferredDevicesDispatcher(
@NonNull IStrategyPreferredDevicesDispatcher dispatcher) {
mDeviceInventory.registerStrategyPreferredDevicesDispatcher(dispatcher);
@@ -881,6 +892,16 @@
mDeviceInventory.unregisterStrategyPreferredDevicesDispatcher(dispatcher);
}
+ /*package*/ void registerStrategyNonDefaultDevicesDispatcher(
+ @NonNull IStrategyNonDefaultDevicesDispatcher dispatcher) {
+ mDeviceInventory.registerStrategyNonDefaultDevicesDispatcher(dispatcher);
+ }
+
+ /*package*/ void unregisterStrategyNonDefaultDevicesDispatcher(
+ @NonNull IStrategyNonDefaultDevicesDispatcher dispatcher) {
+ mDeviceInventory.unregisterStrategyNonDefaultDevicesDispatcher(dispatcher);
+ }
+
/*package*/ int setPreferredDevicesForCapturePresetSync(int capturePreset,
@NonNull List<AudioDeviceAttributes> devices) {
return mDeviceInventory.setPreferredDevicesForCapturePresetSync(capturePreset, devices);
@@ -1039,6 +1060,17 @@
sendIMsgNoDelay(MSG_I_SAVE_REMOVE_PREF_DEVICES_FOR_STRATEGY, SENDMSG_QUEUE, strategy);
}
+ /*package*/ void postSaveSetDeviceAsNonDefaultForStrategy(
+ int strategy, AudioDeviceAttributes device) {
+ sendILMsgNoDelay(MSG_IL_SAVE_NDEF_DEVICE_FOR_STRATEGY, SENDMSG_QUEUE, strategy, device);
+ }
+
+ /*package*/ void postSaveRemoveDeviceAsNonDefaultForStrategy(
+ int strategy, AudioDeviceAttributes device) {
+ sendILMsgNoDelay(
+ MSG_IL_SAVE_REMOVE_NDEF_DEVICE_FOR_STRATEGY, SENDMSG_QUEUE, strategy, device);
+ }
+
/*package*/ void postSaveSetPreferredDevicesForCapturePreset(
int capturePreset, List<AudioDeviceAttributes> devices) {
sendILMsgNoDelay(
@@ -1508,6 +1540,16 @@
final int strategy = msg.arg1;
mDeviceInventory.onSaveRemovePreferredDevices(strategy);
} break;
+ case MSG_IL_SAVE_NDEF_DEVICE_FOR_STRATEGY: {
+ final int strategy = msg.arg1;
+ final AudioDeviceAttributes device = (AudioDeviceAttributes) msg.obj;
+ mDeviceInventory.onSaveSetDeviceAsNonDefault(strategy, device);
+ } break;
+ case MSG_IL_SAVE_REMOVE_NDEF_DEVICE_FOR_STRATEGY: {
+ final int strategy = msg.arg1;
+ final AudioDeviceAttributes device = (AudioDeviceAttributes) msg.obj;
+ mDeviceInventory.onSaveRemoveDeviceAsNonDefault(strategy, device);
+ } break;
case MSG_CHECK_MUTE_MUSIC:
checkMessagesMuteMusic(0);
break;
@@ -1593,6 +1635,9 @@
// process set volume for Le Audio, obj is BleVolumeInfo
private static final int MSG_II_SET_LE_AUDIO_OUT_VOLUME = 46;
+ private static final int MSG_IL_SAVE_NDEF_DEVICE_FOR_STRATEGY = 47;
+ private static final int MSG_IL_SAVE_REMOVE_NDEF_DEVICE_FOR_STRATEGY = 48;
+
private static boolean isMessageHandledUnderWakelock(int msgId) {
switch(msgId) {
case MSG_L_SET_WIRED_DEVICE_CONNECTION_STATE:
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index 34457b0..f9270c9 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -31,8 +31,11 @@
import android.media.AudioSystem;
import android.media.IAudioRoutesObserver;
import android.media.ICapturePresetDevicesRoleDispatcher;
+import android.media.IStrategyNonDefaultDevicesDispatcher;
import android.media.IStrategyPreferredDevicesDispatcher;
import android.media.MediaMetrics;
+import android.media.permission.ClearCallingIdentityContext;
+import android.media.permission.SafeCloseable;
import android.os.Binder;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
@@ -142,6 +145,10 @@
private final ArrayMap<Integer, List<AudioDeviceAttributes>> mPreferredDevices =
new ArrayMap<>();
+ // List of non-default devices for strategies
+ private final ArrayMap<Integer, List<AudioDeviceAttributes>> mNonDefaultDevices =
+ new ArrayMap<>();
+
// List of preferred devices of capture preset
private final ArrayMap<Integer, List<AudioDeviceAttributes>> mPreferredDevicesForCapturePreset =
new ArrayMap<>();
@@ -156,10 +163,14 @@
final RemoteCallbackList<IAudioRoutesObserver> mRoutesObservers =
new RemoteCallbackList<IAudioRoutesObserver>();
- // Monitoring of strategy-preferred device
+ // Monitoring of preferred device for strategies
final RemoteCallbackList<IStrategyPreferredDevicesDispatcher> mPrefDevDispatchers =
new RemoteCallbackList<IStrategyPreferredDevicesDispatcher>();
+ // Monitoring of non-default device for strategies
+ final RemoteCallbackList<IStrategyNonDefaultDevicesDispatcher> mNonDefDevDispatchers =
+ new RemoteCallbackList<IStrategyNonDefaultDevicesDispatcher>();
+
// Monitoring of devices for role and capture preset
final RemoteCallbackList<ICapturePresetDevicesRoleDispatcher> mDevRoleCapturePresetDispatchers =
new RemoteCallbackList<ICapturePresetDevicesRoleDispatcher>();
@@ -254,6 +265,9 @@
pw.println("\n" + prefix + "Preferred devices for strategy:");
mPreferredDevices.forEach((strategy, device) -> {
pw.println(" " + prefix + "strategy:" + strategy + " device:" + device); });
+ pw.println("\n" + prefix + "Non-default devices for strategy:");
+ mNonDefaultDevices.forEach((strategy, device) -> {
+ pw.println(" " + prefix + "strategy:" + strategy + " device:" + device); });
pw.println("\n" + prefix + "Connected devices:");
mConnectedDevices.forEach((key, deviceInfo) -> {
pw.println(" " + prefix + deviceInfo.toString()); });
@@ -291,6 +305,11 @@
mAudioSystem.setDevicesRoleForStrategy(
strategy, AudioSystem.DEVICE_ROLE_PREFERRED, devices); });
}
+ synchronized (mNonDefaultDevices) {
+ mNonDefaultDevices.forEach((strategy, devices) -> {
+ mAudioSystem.setDevicesRoleForStrategy(
+ strategy, AudioSystem.DEVICE_ROLE_DISABLED, devices); });
+ }
synchronized (mPreferredDevicesForCapturePreset) {
// TODO: call audiosystem to restore
}
@@ -608,6 +627,18 @@
/*package*/ void onSaveSetPreferredDevices(int strategy,
@NonNull List<AudioDeviceAttributes> devices) {
mPreferredDevices.put(strategy, devices);
+ List<AudioDeviceAttributes> nonDefaultDevices = mNonDefaultDevices.get(strategy);
+ if (nonDefaultDevices != null) {
+ nonDefaultDevices.removeAll(devices);
+
+ if (nonDefaultDevices.isEmpty()) {
+ mNonDefaultDevices.remove(strategy);
+ } else {
+ mNonDefaultDevices.put(strategy, nonDefaultDevices);
+ }
+ dispatchNonDefaultDevice(strategy, nonDefaultDevices);
+ }
+
dispatchPreferredDevice(strategy, devices);
}
@@ -616,6 +647,40 @@
dispatchPreferredDevice(strategy, new ArrayList<AudioDeviceAttributes>());
}
+ /*package*/ void onSaveSetDeviceAsNonDefault(int strategy,
+ @NonNull AudioDeviceAttributes device) {
+ List<AudioDeviceAttributes> nonDefaultDevices = mNonDefaultDevices.get(strategy);
+ if (nonDefaultDevices == null) {
+ nonDefaultDevices = new ArrayList<>();
+ }
+
+ if (!nonDefaultDevices.contains(device)) {
+ nonDefaultDevices.add(device);
+ }
+
+ mNonDefaultDevices.put(strategy, nonDefaultDevices);
+ dispatchNonDefaultDevice(strategy, nonDefaultDevices);
+
+ List<AudioDeviceAttributes> preferredDevices = mPreferredDevices.get(strategy);
+
+ if (preferredDevices != null) {
+ preferredDevices.remove(device);
+ mPreferredDevices.put(strategy, preferredDevices);
+
+ dispatchPreferredDevice(strategy, preferredDevices);
+ }
+ }
+
+ /*package*/ void onSaveRemoveDeviceAsNonDefault(int strategy,
+ @NonNull AudioDeviceAttributes device) {
+ List<AudioDeviceAttributes> nonDefaultDevices = mNonDefaultDevices.get(strategy);
+ if (nonDefaultDevices != null) {
+ nonDefaultDevices.remove(device);
+ mNonDefaultDevices.put(strategy, nonDefaultDevices);
+ dispatchNonDefaultDevice(strategy, nonDefaultDevices);
+ }
+ }
+
/*package*/ void onSaveSetPreferredDevicesForCapturePreset(
int capturePreset, @NonNull List<AudioDeviceAttributes> devices) {
mPreferredDevicesForCapturePreset.put(capturePreset, devices);
@@ -631,18 +696,19 @@
}
//------------------------------------------------------------
- // preferred device(s)
+ // preferred/non-default device(s)
/*package*/ int setPreferredDevicesForStrategySync(int strategy,
@NonNull List<AudioDeviceAttributes> devices) {
- final long identity = Binder.clearCallingIdentity();
+ int status = AudioSystem.ERROR;
- AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
- "setPreferredDevicesForStrategySync, strategy: " + strategy
- + " devices: " + devices)).printLog(TAG));
- final int status = mAudioSystem.setDevicesRoleForStrategy(
- strategy, AudioSystem.DEVICE_ROLE_PREFERRED, devices);
- Binder.restoreCallingIdentity(identity);
+ try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+ AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
+ "setPreferredDevicesForStrategySync, strategy: " + strategy
+ + " devices: " + devices)).printLog(TAG));
+ status = mAudioSystem.setDevicesRoleForStrategy(
+ strategy, AudioSystem.DEVICE_ROLE_PREFERRED, devices);
+ }
if (status == AudioSystem.SUCCESS) {
mDeviceBroker.postSaveSetPreferredDevicesForStrategy(strategy, devices);
@@ -651,15 +717,16 @@
}
/*package*/ int removePreferredDevicesForStrategySync(int strategy) {
- final long identity = Binder.clearCallingIdentity();
+ int status = AudioSystem.ERROR;
- AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
- "removePreferredDevicesForStrategySync, strategy: "
- + strategy)).printLog(TAG));
+ try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+ AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
+ "removePreferredDevicesForStrategySync, strategy: "
+ + strategy)).printLog(TAG));
- final int status = mAudioSystem.removeDevicesRoleForStrategy(
- strategy, AudioSystem.DEVICE_ROLE_PREFERRED);
- Binder.restoreCallingIdentity(identity);
+ status = mAudioSystem.clearDevicesRoleForStrategy(
+ strategy, AudioSystem.DEVICE_ROLE_PREFERRED);
+ }
if (status == AudioSystem.SUCCESS) {
mDeviceBroker.postSaveRemovePreferredDevicesForStrategy(strategy);
@@ -667,6 +734,50 @@
return status;
}
+ /*package*/ int setDeviceAsNonDefaultForStrategySync(int strategy,
+ @NonNull AudioDeviceAttributes device) {
+ int status = AudioSystem.ERROR;
+
+ try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+ List<AudioDeviceAttributes> devices = new ArrayList<>();
+ devices.add(device);
+
+ AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
+ "setDeviceAsNonDefaultForStrategySync, strategy: " + strategy
+ + " device: " + device)).printLog(TAG));
+ status = mAudioSystem.setDevicesRoleForStrategy(
+ strategy, AudioSystem.DEVICE_ROLE_DISABLED, devices);
+ }
+
+ if (status == AudioSystem.SUCCESS) {
+ mDeviceBroker.postSaveSetDeviceAsNonDefaultForStrategy(strategy, device);
+ }
+ return status;
+ }
+
+ /*package*/ int removeDeviceAsNonDefaultForStrategySync(int strategy,
+ @NonNull AudioDeviceAttributes device) {
+ int status = AudioSystem.ERROR;
+
+ try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+ List<AudioDeviceAttributes> devices = new ArrayList<>();
+ devices.add(device);
+
+ AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
+ "removeDeviceAsNonDefaultForStrategySync, strategy: "
+ + strategy + " devices: " + device)).printLog(TAG));
+
+ status = mAudioSystem.removeDevicesRoleForStrategy(
+ strategy, AudioSystem.DEVICE_ROLE_DISABLED, devices);
+ }
+
+ if (status == AudioSystem.SUCCESS) {
+ mDeviceBroker.postSaveRemoveDeviceAsNonDefaultForStrategy(strategy, device);
+ }
+ return status;
+ }
+
+
/*package*/ void registerStrategyPreferredDevicesDispatcher(
@NonNull IStrategyPreferredDevicesDispatcher dispatcher) {
mPrefDevDispatchers.register(dispatcher);
@@ -677,12 +788,24 @@
mPrefDevDispatchers.unregister(dispatcher);
}
+ /*package*/ void registerStrategyNonDefaultDevicesDispatcher(
+ @NonNull IStrategyNonDefaultDevicesDispatcher dispatcher) {
+ mNonDefDevDispatchers.register(dispatcher);
+ }
+
+ /*package*/ void unregisterStrategyNonDefaultDevicesDispatcher(
+ @NonNull IStrategyNonDefaultDevicesDispatcher dispatcher) {
+ mNonDefDevDispatchers.unregister(dispatcher);
+ }
+
/*package*/ int setPreferredDevicesForCapturePresetSync(
int capturePreset, @NonNull List<AudioDeviceAttributes> devices) {
- final long identity = Binder.clearCallingIdentity();
- final int status = mAudioSystem.setDevicesRoleForCapturePreset(
- capturePreset, AudioSystem.DEVICE_ROLE_PREFERRED, devices);
- Binder.restoreCallingIdentity(identity);
+ int status = AudioSystem.ERROR;
+
+ try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+ status = mAudioSystem.setDevicesRoleForCapturePreset(
+ capturePreset, AudioSystem.DEVICE_ROLE_PREFERRED, devices);
+ }
if (status == AudioSystem.SUCCESS) {
mDeviceBroker.postSaveSetPreferredDevicesForCapturePreset(capturePreset, devices);
@@ -691,10 +814,12 @@
}
/*package*/ int clearPreferredDevicesForCapturePresetSync(int capturePreset) {
- final long identity = Binder.clearCallingIdentity();
- final int status = mAudioSystem.clearDevicesRoleForCapturePreset(
- capturePreset, AudioSystem.DEVICE_ROLE_PREFERRED);
- Binder.restoreCallingIdentity(identity);
+ int status = AudioSystem.ERROR;
+
+ try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+ status = mAudioSystem.clearDevicesRoleForCapturePreset(
+ capturePreset, AudioSystem.DEVICE_ROLE_PREFERRED);
+ }
if (status == AudioSystem.SUCCESS) {
mDeviceBroker.postSaveClearPreferredDevicesForCapturePreset(capturePreset);
@@ -1523,6 +1648,19 @@
mPrefDevDispatchers.finishBroadcast();
}
+ private void dispatchNonDefaultDevice(int strategy,
+ @NonNull List<AudioDeviceAttributes> devices) {
+ final int nbDispatchers = mNonDefDevDispatchers.beginBroadcast();
+ for (int i = 0; i < nbDispatchers; i++) {
+ try {
+ mNonDefDevDispatchers.getBroadcastItem(i).dispatchNonDefDevicesChanged(
+ strategy, devices);
+ } catch (RemoteException e) {
+ }
+ }
+ mNonDefDevDispatchers.finishBroadcast();
+ }
+
private void dispatchDevicesRoleForCapturePreset(
int capturePreset, int role, @NonNull List<AudioDeviceAttributes> devices) {
final int nbDispatchers = mDevRoleCapturePresetDispatchers.beginBroadcast();
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 8c3c0c9..24c7d2c 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -116,6 +116,7 @@
import android.media.ISpatializerHeadTrackerAvailableCallback;
import android.media.ISpatializerHeadTrackingModeCallback;
import android.media.ISpatializerOutputCallback;
+import android.media.IStrategyNonDefaultDevicesDispatcher;
import android.media.IStrategyPreferredDevicesDispatcher;
import android.media.IVolumeController;
import android.media.MediaMetrics;
@@ -131,6 +132,8 @@
import android.media.audiopolicy.AudioProductStrategy;
import android.media.audiopolicy.AudioVolumeGroup;
import android.media.audiopolicy.IAudioPolicyCallback;
+import android.media.permission.ClearCallingIdentityContext;
+import android.media.permission.SafeCloseable;
import android.media.projection.IMediaProjection;
import android.media.projection.IMediaProjectionCallback;
import android.media.projection.IMediaProjectionManager;
@@ -2798,11 +2801,12 @@
* @see AudioManager#setPreferredDevicesForStrategy(AudioProductStrategy,
* List<AudioDeviceAttributes>)
*/
+ @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
public int setPreferredDevicesForStrategy(int strategy, List<AudioDeviceAttributes> devices) {
+ super.setPreferredDevicesForStrategy_enforcePermission();
if (devices == null) {
return AudioSystem.ERROR;
}
- enforceModifyAudioRoutingPermission();
final String logString = String.format(
"setPreferredDeviceForStrategy u/pid:%d/%d strat:%d dev:%s",
Binder.getCallingUid(), Binder.getCallingPid(), strategy,
@@ -2860,6 +2864,81 @@
}
}
+ /**
+ * @see AudioManager#setDeviceAsNonDefaultForStrategy(AudioProductStrategy,
+ * AudioDeviceAttributes)
+ * @see AudioManager#setDeviceAsNonDefaultForStrategy(AudioProductStrategy,
+ * List<AudioDeviceAttributes>)
+ */
+ @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+ public int setDeviceAsNonDefaultForStrategy(int strategy,
+ @NonNull AudioDeviceAttributes device) {
+ super.setDeviceAsNonDefaultForStrategy_enforcePermission();
+ Objects.requireNonNull(device);
+ final String logString = String.format(
+ "setDeviceAsNonDefaultForStrategy u/pid:%d/%d strat:%d dev:%s",
+ Binder.getCallingUid(), Binder.getCallingPid(), strategy, device.toString());
+ sDeviceLogger.enqueue(new EventLogger.StringEvent(logString).printLog(TAG));
+ if (device.getRole() == AudioDeviceAttributes.ROLE_INPUT) {
+ Log.e(TAG, "Unsupported input routing in " + logString);
+ return AudioSystem.ERROR;
+ }
+
+ final int status = mDeviceBroker.setDeviceAsNonDefaultForStrategySync(strategy, device);
+ if (status != AudioSystem.SUCCESS) {
+ Log.e(TAG, String.format("Error %d in %s)", status, logString));
+ }
+
+ return status;
+ }
+
+ /**
+ * @see AudioManager#removeDeviceAsNonDefaultForStrategy(AudioProductStrategy,
+ * AudioDeviceAttributes)
+ */
+ @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+ public int removeDeviceAsNonDefaultForStrategy(int strategy,
+ AudioDeviceAttributes device) {
+ super.removeDeviceAsNonDefaultForStrategy_enforcePermission();
+ Objects.requireNonNull(device);
+ final String logString = String.format(
+ "removeDeviceAsNonDefaultForStrategy strat:%d dev:%s", strategy, device.toString());
+ sDeviceLogger.enqueue(new EventLogger.StringEvent(logString).printLog(TAG));
+ if (device.getRole() == AudioDeviceAttributes.ROLE_INPUT) {
+ Log.e(TAG, "Unsupported input routing in " + logString);
+ return AudioSystem.ERROR;
+ }
+
+ final int status = mDeviceBroker.removeDeviceAsNonDefaultForStrategySync(strategy, device);
+ if (status != AudioSystem.SUCCESS) {
+ Log.e(TAG, String.format("Error %d in %s)", status, logString));
+ }
+ return status;
+ }
+
+ /**
+ * @see AudioManager#getNonDefaultDevicesForStrategy(AudioProductStrategy)
+ */
+ @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+ public List<AudioDeviceAttributes> getNonDefaultDevicesForStrategy(int strategy) {
+ super.getNonDefaultDevicesForStrategy_enforcePermission();
+ List<AudioDeviceAttributes> devices = new ArrayList<>();
+ int status = AudioSystem.ERROR;
+
+ try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+ status = AudioSystem.getDevicesForRoleAndStrategy(
+ strategy, AudioSystem.DEVICE_ROLE_DISABLED, devices);
+ }
+
+ if (status != AudioSystem.SUCCESS) {
+ Log.e(TAG, String.format("Error %d in getNonDefaultDeviceForStrategy(%d)",
+ status, strategy));
+ return new ArrayList<AudioDeviceAttributes>();
+ } else {
+ return devices;
+ }
+ }
+
/** @see AudioManager#addOnPreferredDevicesForStrategyChangedListener(
* Executor, AudioManager.OnPreferredDevicesForStrategyChangedListener)
*/
@@ -2884,6 +2963,30 @@
mDeviceBroker.unregisterStrategyPreferredDevicesDispatcher(dispatcher);
}
+ /** @see AudioManager#addOnNonDefaultDevicesForStrategyChangedListener(
+ * Executor, AudioManager.OnNonDefaultDevicesForStrategyChangedListener)
+ */
+ public void registerStrategyNonDefaultDevicesDispatcher(
+ @Nullable IStrategyNonDefaultDevicesDispatcher dispatcher) {
+ if (dispatcher == null) {
+ return;
+ }
+ enforceModifyAudioRoutingPermission();
+ mDeviceBroker.registerStrategyNonDefaultDevicesDispatcher(dispatcher);
+ }
+
+ /** @see AudioManager#removeOnNonDefaultDevicesForStrategyChangedListener(
+ * AudioManager.OnNonDefaultDevicesForStrategyChangedListener)
+ */
+ public void unregisterStrategyNonDefaultDevicesDispatcher(
+ @Nullable IStrategyNonDefaultDevicesDispatcher dispatcher) {
+ if (dispatcher == null) {
+ return;
+ }
+ enforceModifyAudioRoutingPermission();
+ mDeviceBroker.unregisterStrategyNonDefaultDevicesDispatcher(dispatcher);
+ }
+
/**
* @see AudioManager#setPreferredDeviceForCapturePreset(int, AudioDeviceAttributes)
*/
@@ -11219,6 +11322,34 @@
mPrefMixerAttrDispatcher.finishBroadcast();
}
+
+ /** @see AudioManager#supportsBluetoothVariableLatency() */
+ @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+ public boolean supportsBluetoothVariableLatency() {
+ super.supportsBluetoothVariableLatency_enforcePermission();
+ try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+ return AudioSystem.supportsBluetoothVariableLatency();
+ }
+ }
+
+ /** @see AudioManager#setBluetoothVariableLatencyEnabled(boolean) */
+ @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+ public void setBluetoothVariableLatencyEnabled(boolean enabled) {
+ super.setBluetoothVariableLatencyEnabled_enforcePermission();
+ try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+ AudioSystem.setBluetoothVariableLatencyEnabled(enabled);
+ }
+ }
+
+ /** @see AudioManager#isBluetoothVariableLatencyEnabled(boolean) */
+ @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+ public boolean isBluetoothVariableLatencyEnabled() {
+ super.isBluetoothVariableLatencyEnabled_enforcePermission();
+ try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+ return AudioSystem.isBluetoothVariableLatencyEnabled();
+ }
+ }
+
private final Object mExtVolumeControllerLock = new Object();
private IAudioPolicyCallback mExtVolumeController;
private void setExtVolumeController(IAudioPolicyCallback apc) {
@@ -11480,6 +11611,11 @@
public void onCapturedContentResize(int width, int height) {
// Ignore resize of the captured content.
}
+
+ @Override
+ public void onCapturedContentVisibilityChanged(boolean isVisible) {
+ // Ignore visibility changes of the captured content.
+ }
};
UnregisterOnStopCallback mProjectionCallback;
diff --git a/services/core/java/com/android/server/audio/AudioSystemAdapter.java b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
index c176f29..7fefc55 100644
--- a/services/core/java/com/android/server/audio/AudioSystemAdapter.java
+++ b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
@@ -279,14 +279,27 @@
}
/**
- * Same as {@link AudioSystem#removeDevicesRoleForStrategy(int, int)}
+ * Same as {@link AudioSystem#removeDevicesRoleForStrategy(int, int, List)}
+ * @param strategy
+ * @param role
+ * @param devices
+ * @return
+ */
+ public int removeDevicesRoleForStrategy(int strategy, int role,
+ @NonNull List<AudioDeviceAttributes> devices) {
+ invalidateRoutingCache();
+ return AudioSystem.removeDevicesRoleForStrategy(strategy, role, devices);
+ }
+
+ /**
+ * Same as {@link AudioSystem#clearDevicesRoleForStrategy(int, int)}
* @param strategy
* @param role
* @return
*/
- public int removeDevicesRoleForStrategy(int strategy, int role) {
+ public int clearDevicesRoleForStrategy(int strategy, int role) {
invalidateRoutingCache();
- return AudioSystem.removeDevicesRoleForStrategy(strategy, role);
+ return AudioSystem.clearDevicesRoleForStrategy(strategy, role);
}
/**
diff --git a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
index f35931ca..99d6228 100644
--- a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
+++ b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
@@ -35,6 +35,7 @@
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.media.AudioPlaybackConfiguration;
+import android.media.AudioPlaybackConfiguration.FormatInfo;
import android.media.AudioPlaybackConfiguration.PlayerMuteEvent;
import android.media.AudioSystem;
import android.media.IPlaybackConfigDispatcher;
@@ -75,7 +76,7 @@
public final class PlaybackActivityMonitor
implements AudioPlaybackConfiguration.PlayerDeathMonitor, PlayerFocusEnforcer {
- public static final String TAG = "AudioService.PlaybackActivityMonitor";
+ public static final String TAG = "AS.PlaybackActivityMon";
/*package*/ static final boolean DEBUG = false;
/*package*/ static final int VOLUME_SHAPER_SYSTEM_DUCK_ID = 1;
@@ -343,7 +344,7 @@
if (event == AudioPlaybackConfiguration.PLAYER_UPDATE_PORT_ID) {
mEventHandler.sendMessage(
- mEventHandler.obtainMessage(MSG_L_UPDATE_PORT_EVENT, eventValue, piid));
+ mEventHandler.obtainMessage(MSG_II_UPDATE_PORT_EVENT, eventValue, piid));
return;
} else if (event == AudioPlaybackConfiguration.PLAYER_STATE_STARTED) {
for (Integer uidInteger: mBannedUids) {
@@ -399,7 +400,7 @@
}
if (DEBUG) {
- Log.v(TAG, TextUtils.formatSimple("portEvent(portId=%d, event=%s, extras=%s)",
+ Log.v(TAG, TextUtils.formatSimple("BLA portEvent(portId=%d, event=%s, extras=%s)",
portId, AudioPlaybackConfiguration.playerStateToString(event), extras));
}
@@ -427,7 +428,12 @@
if (event == AudioPlaybackConfiguration.PLAYER_UPDATE_MUTED) {
mEventHandler.sendMessage(
- mEventHandler.obtainMessage(MSG_L_UPDATE_PLAYER_MUTED_EVENT, piid,
+ mEventHandler.obtainMessage(MSG_IIL_UPDATE_PLAYER_MUTED_EVENT, piid,
+ portId,
+ extras));
+ } else if (event == AudioPlaybackConfiguration.PLAYER_UPDATE_FORMAT) {
+ mEventHandler.sendMessage(
+ mEventHandler.obtainMessage(MSG_IIL_UPDATE_PLAYER_FORMAT, piid,
portId,
extras));
}
@@ -457,7 +463,7 @@
// remove all port ids mapped to the released player
mEventHandler.sendMessage(
- mEventHandler.obtainMessage(MSG_L_CLEAR_PORTS_FOR_PIID, piid, /*arg2=*/0));
+ mEventHandler.obtainMessage(MSG_I_CLEAR_PORTS_FOR_PIID, piid, /*arg2=*/0));
if (change && mDoNotLogPiidList.contains(piid)) {
// do not dispatch a change for a "do not log" player
@@ -1352,6 +1358,21 @@
}
}
+ private static final class PlayerFormatEvent extends EventLogger.Event {
+ private final int mPlayerIId;
+ private final AudioPlaybackConfiguration.FormatInfo mFormat;
+
+ PlayerFormatEvent(int piid, AudioPlaybackConfiguration.FormatInfo format) {
+ mPlayerIId = piid;
+ mFormat = format;
+ }
+
+ @Override
+ public String eventToString() {
+ return new String("player piid:" + mPlayerIId + " format update:" + mFormat);
+ }
+ }
+
static final EventLogger
sEventLogger = new EventLogger(100,
"playback activity as reported through PlayerBase");
@@ -1478,7 +1499,7 @@
* msg.arg1: port id
* msg.arg2: piid
*/
- private static final int MSG_L_UPDATE_PORT_EVENT = 2;
+ private static final int MSG_II_UPDATE_PORT_EVENT = 2;
/**
* event for player getting muted
@@ -1488,14 +1509,24 @@
* msg.obj: extras describing the mute reason
* type: PersistableBundle
*/
- private static final int MSG_L_UPDATE_PLAYER_MUTED_EVENT = 3;
+ private static final int MSG_IIL_UPDATE_PLAYER_MUTED_EVENT = 3;
/**
* clear all ports assigned to a given piid
* args:
* msg.arg1: the piid
*/
- private static final int MSG_L_CLEAR_PORTS_FOR_PIID = 4;
+ private static final int MSG_I_CLEAR_PORTS_FOR_PIID = 4;
+
+ /**
+ * event for player reporting playback format and spatialization status
+ * args:
+ * msg.arg1: piid
+ * msg.arg2: port id
+ * msg.obj: extras describing the sample rate, channel mask, spatialized
+ * type: PersistableBundle
+ */
+ private static final int MSG_IIL_UPDATE_PLAYER_FORMAT = 5;
private void initEventHandler() {
mEventThread = new HandlerThread(TAG);
@@ -1513,12 +1544,13 @@
}
mMuteAwaitConnectionTimeoutCb.accept((AudioDeviceAttributes) msg.obj);
break;
- case MSG_L_UPDATE_PORT_EVENT:
+
+ case MSG_II_UPDATE_PORT_EVENT:
synchronized (mPlayerLock) {
mPortIdToPiid.put(/*portId*/msg.arg1, /*piid*/msg.arg2);
}
break;
- case MSG_L_UPDATE_PLAYER_MUTED_EVENT:
+ case MSG_IIL_UPDATE_PLAYER_MUTED_EVENT:
// TODO: replace PersistableBundle with own struct
PersistableBundle extras = (PersistableBundle) msg.obj;
if (extras == null) {
@@ -1533,14 +1565,18 @@
sEventLogger.enqueue(
new PlayerEvent(piid, PLAYER_UPDATE_MUTED, eventValue));
- final AudioPlaybackConfiguration apc = mPlayers.get(piid);
+ final AudioPlaybackConfiguration apc;
+ synchronized (mPlayerLock) {
+ apc = mPlayers.get(piid);
+ }
if (apc == null || !apc.handleMutedEvent(eventValue)) {
break; // do not dispatch
}
dispatchPlaybackChange(/* iplayerReleased= */false);
}
break;
- case MSG_L_CLEAR_PORTS_FOR_PIID:
+
+ case MSG_I_CLEAR_PORTS_FOR_PIID:
int piid = msg.arg1;
if (piid == AudioPlaybackConfiguration.PLAYER_PIID_INVALID) {
Log.w(TAG, "Received clear ports with invalid piid");
@@ -1554,6 +1590,34 @@
}
}
break;
+
+ case MSG_IIL_UPDATE_PLAYER_FORMAT:
+ final PersistableBundle formatExtras = (PersistableBundle) msg.obj;
+ if (formatExtras == null) {
+ Log.w(TAG, "Received format event with no extras");
+ break;
+ }
+ final boolean spatialized = formatExtras.getBoolean(
+ AudioPlaybackConfiguration.EXTRA_PLAYER_EVENT_SPATIALIZED, false);
+ final int sampleRate = formatExtras.getInt(
+ AudioPlaybackConfiguration.EXTRA_PLAYER_EVENT_SAMPLE_RATE, 0);
+ final int nativeChannelMask = formatExtras.getInt(
+ AudioPlaybackConfiguration.EXTRA_PLAYER_EVENT_CHANNEL_MASK, 0);
+ final FormatInfo format =
+ new FormatInfo(spatialized, nativeChannelMask, sampleRate);
+
+ sEventLogger.enqueue(new PlayerFormatEvent(msg.arg1, format));
+
+ final AudioPlaybackConfiguration apc;
+ synchronized (mPlayerLock) {
+ apc = mPlayers.get(msg.arg1);
+ }
+ if (apc == null || !apc.handleFormatEvent(format)) {
+ break; // do not dispatch
+ }
+ // TODO optimize for no dispatch to non-privileged listeners
+ dispatchPlaybackChange(/* iplayerReleased= */false);
+ break;
default:
break;
}
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 3c0fda8..c0a238f 100644
--- a/services/core/java/com/android/server/broadcastradio/aidl/ConversionUtils.java
+++ b/services/core/java/com/android/server/broadcastradio/aidl/ConversionUtils.java
@@ -307,6 +307,9 @@
static ProgramIdentifier identifierToHalProgramIdentifier(ProgramSelector.Identifier id) {
ProgramIdentifier hwId = new ProgramIdentifier();
hwId.type = id.getType();
+ if (hwId.type == ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT) {
+ hwId.type = IdentifierType.DAB_SID_EXT;
+ }
hwId.value = id.getValue();
return hwId;
}
@@ -317,9 +320,49 @@
if (id.type == IdentifierType.INVALID) {
return null;
}
- return new ProgramSelector.Identifier(id.type, id.value);
+ int idType;
+ if (id.type == IdentifierType.DAB_SID_EXT) {
+ idType = ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT;
+ } else {
+ idType = id.type;
+ }
+ return new ProgramSelector.Identifier(idType, id.value);
}
+ private static boolean isVendorIdentifierType(int idType) {
+ return idType >= IdentifierType.VENDOR_START && idType <= IdentifierType.VENDOR_END;
+ }
+
+ private static boolean isValidHalProgramSelector(
+ android.hardware.broadcastradio.ProgramSelector sel) {
+ if (sel.primaryId.type != IdentifierType.AMFM_FREQUENCY_KHZ
+ && sel.primaryId.type != IdentifierType.RDS_PI
+ && sel.primaryId.type != IdentifierType.HD_STATION_ID_EXT
+ && sel.primaryId.type != IdentifierType.DAB_SID_EXT
+ && sel.primaryId.type != IdentifierType.DRMO_SERVICE_ID
+ && sel.primaryId.type != IdentifierType.SXM_SERVICE_ID
+ && !isVendorIdentifierType(sel.primaryId.type)) {
+ return false;
+ }
+ if (sel.primaryId.type == IdentifierType.DAB_SID_EXT) {
+ boolean hasEnsemble = false;
+ boolean hasFrequency = false;
+ for (int i = 0; i < sel.secondaryIds.length; i++) {
+ if (sel.secondaryIds[i].type == IdentifierType.DAB_ENSEMBLE) {
+ hasEnsemble = true;
+ } else if (sel.secondaryIds[i].type == IdentifierType.DAB_FREQUENCY_KHZ) {
+ hasFrequency = true;
+ }
+ if (hasEnsemble && hasFrequency) {
+ return true;
+ }
+ }
+ return false;
+ }
+ return true;
+ }
+
+ @Nullable
static android.hardware.broadcastradio.ProgramSelector programSelectorToHalProgramSelector(
ProgramSelector sel) {
android.hardware.broadcastradio.ProgramSelector hwSel =
@@ -332,6 +375,9 @@
secondaryIdList.add(identifierToHalProgramIdentifier(secondaryIds[i]));
}
hwSel.secondaryIds = secondaryIdList.toArray(ProgramIdentifier[]::new);
+ if (!isValidHalProgramSelector(hwSel)) {
+ return null;
+ }
return hwSel;
}
@@ -344,7 +390,7 @@
@Nullable
static ProgramSelector programSelectorFromHalProgramSelector(
android.hardware.broadcastradio.ProgramSelector sel) {
- if (isEmpty(sel)) {
+ if (isEmpty(sel) || !isValidHalProgramSelector(sel)) {
return null;
}
@@ -432,7 +478,34 @@
return builder.build();
}
+ private static boolean isValidLogicallyTunedTo(ProgramIdentifier id) {
+ return id.type == IdentifierType.AMFM_FREQUENCY_KHZ || id.type == IdentifierType.RDS_PI
+ || id.type == IdentifierType.HD_STATION_ID_EXT
+ || id.type == IdentifierType.DAB_SID_EXT
+ || id.type == IdentifierType.DRMO_SERVICE_ID
+ || id.type == IdentifierType.SXM_SERVICE_ID
+ || isVendorIdentifierType(id.type);
+ }
+
+ private static boolean isValidPhysicallyTunedTo(ProgramIdentifier id) {
+ return id.type == IdentifierType.AMFM_FREQUENCY_KHZ
+ || id.type == IdentifierType.DAB_FREQUENCY_KHZ
+ || id.type == IdentifierType.DRMO_FREQUENCY_KHZ
+ || id.type == IdentifierType.SXM_CHANNEL
+ || isVendorIdentifierType(id.type);
+ }
+
+ private static boolean isValidHalProgramInfo(ProgramInfo info) {
+ return isValidHalProgramSelector(info.selector)
+ && isValidLogicallyTunedTo(info.logicallyTunedTo)
+ && isValidPhysicallyTunedTo(info.physicallyTunedTo);
+ }
+
+ @Nullable
static RadioManager.ProgramInfo programInfoFromHalProgramInfo(ProgramInfo info) {
+ if (!isValidHalProgramInfo(info)) {
+ return null;
+ }
Collection<ProgramSelector.Identifier> relatedContent = new ArrayList<>();
if (info.relatedContent != null) {
for (int i = 0; i < info.relatedContent.length; i++) {
@@ -485,7 +558,14 @@
static ProgramList.Chunk chunkFromHalProgramListChunk(ProgramListChunk chunk) {
Set<RadioManager.ProgramInfo> modified = new ArraySet<>(chunk.modified.length);
for (int i = 0; i < chunk.modified.length; i++) {
- modified.add(programInfoFromHalProgramInfo(chunk.modified[i]));
+ RadioManager.ProgramInfo modifiedInfo =
+ programInfoFromHalProgramInfo(chunk.modified[i]);
+ if (modifiedInfo == null) {
+ Slogf.w(TAG, "Program info %s in program list chunk is not valid",
+ chunk.modified[i]);
+ continue;
+ }
+ modified.add(modifiedInfo);
}
Set<ProgramSelector.Identifier> removed = new ArraySet<>();
if (chunk.removed != null) {
@@ -547,10 +627,22 @@
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));
+ Set<RadioManager.ProgramInfo> modified = new ArraySet<>();
+ Iterator<RadioManager.ProgramInfo> modifiedIterator = chunk.getModified().iterator();
+ while (modifiedIterator.hasNext()) {
+ RadioManager.ProgramInfo info = modifiedIterator.next();
+ if (programInfoMeetsSdkVersionRequirement(info, targetSdkVersion)) {
+ modified.add(info);
+ }
+ }
+ Set<ProgramSelector.Identifier> removed = new ArraySet<>();
+ Iterator<ProgramSelector.Identifier> removedIterator = chunk.getRemoved().iterator();
+ while (removedIterator.hasNext()) {
+ ProgramSelector.Identifier id = removedIterator.next();
+ if (!isNewIdentifierInU(id)) {
+ removed.add(id);
+ }
+ }
return new ProgramList.Chunk(chunk.isPurge(), chunk.isComplete(), modified, removed);
}
@@ -558,7 +650,7 @@
Announcement hwAnnouncement) {
return new android.hardware.radio.Announcement(
Objects.requireNonNull(programSelectorFromHalProgramSelector(
- hwAnnouncement.selector)),
+ hwAnnouncement.selector), "Program selector can not be null"),
hwAnnouncement.type,
vendorInfoFromHalVendorKeyValues(hwAnnouncement.vendorInfo)
);
diff --git a/services/core/java/com/android/server/broadcastradio/aidl/ProgramInfoCache.java b/services/core/java/com/android/server/broadcastradio/aidl/ProgramInfoCache.java
index 095a5fa..39b1354 100644
--- a/services/core/java/com/android/server/broadcastradio/aidl/ProgramInfoCache.java
+++ b/services/core/java/com/android/server/broadcastradio/aidl/ProgramInfoCache.java
@@ -24,6 +24,7 @@
import android.util.ArraySet;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.utils.Slogf;
import java.util.ArrayList;
import java.util.Collection;
@@ -37,6 +38,7 @@
*/
final class ProgramInfoCache {
+ private static final String TAG = "BcRadioAidlSrv.cache";
/**
* Maximum number of {@link RadioManager#ProgramInfo} elements that will be put into a
* ProgramList.Chunk.mModified array. Used to try to ensure a single ProgramList.Chunk
@@ -124,6 +126,10 @@
for (int i = 0; i < chunk.modified.length; i++) {
RadioManager.ProgramInfo programInfo =
ConversionUtils.programInfoFromHalProgramInfo(chunk.modified[i]);
+ if (programInfo == null) {
+ Slogf.e(TAG, "Program info in program info %s in chunk is not valid",
+ chunk.modified[i]);
+ }
mProgramInfoMap.put(programInfo.getSelector().getPrimaryId(), programInfo);
}
if (chunk.removed != null) {
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 6193f23..f8c19ae 100644
--- a/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java
+++ b/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java
@@ -97,10 +97,10 @@
public void onTuneFailed(int result, ProgramSelector programSelector) {
fireLater(() -> {
+ android.hardware.radio.ProgramSelector csel =
+ ConversionUtils.programSelectorFromHalProgramSelector(programSelector);
+ int tunerResult = ConversionUtils.halResultToTunerResult(result);
synchronized (mLock) {
- android.hardware.radio.ProgramSelector csel =
- ConversionUtils.programSelectorFromHalProgramSelector(programSelector);
- int tunerResult = ConversionUtils.halResultToTunerResult(result);
fanoutAidlCallbackLocked((cb, sdkVersion) -> {
if (csel != null && !ConversionUtils
.programSelectorMeetsSdkVersionRequirement(csel, sdkVersion)) {
@@ -117,10 +117,12 @@
@Override
public void onCurrentProgramInfoChanged(ProgramInfo halProgramInfo) {
fireLater(() -> {
+ RadioManager.ProgramInfo currentProgramInfo =
+ ConversionUtils.programInfoFromHalProgramInfo(halProgramInfo);
+ Objects.requireNonNull(currentProgramInfo,
+ "Program info from AIDL HAL is invalid");
synchronized (mLock) {
- mCurrentProgramInfo =
- ConversionUtils.programInfoFromHalProgramInfo(halProgramInfo);
- RadioManager.ProgramInfo currentProgramInfo = mCurrentProgramInfo;
+ mCurrentProgramInfo = currentProgramInfo;
fanoutAidlCallbackLocked((cb, sdkVersion) -> {
if (!ConversionUtils.programInfoMeetsSdkVersionRequirement(
currentProgramInfo, sdkVersion)) {
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 d700ed0..fe8c238 100644
--- a/services/core/java/com/android/server/broadcastradio/aidl/TunerSession.java
+++ b/services/core/java/com/android/server/broadcastradio/aidl/TunerSession.java
@@ -203,10 +203,15 @@
Slogf.w(TAG, "Cannot tune on AIDL HAL client from non-current user");
return;
}
+ android.hardware.broadcastradio.ProgramSelector hwSel =
+ ConversionUtils.programSelectorToHalProgramSelector(selector);
+ if (hwSel == null) {
+ throw new IllegalArgumentException("tune: INVALID_ARGUMENTS for program selector");
+ }
synchronized (mLock) {
checkNotClosedLocked();
try {
- mService.tune(ConversionUtils.programSelectorToHalProgramSelector(selector));
+ mService.tune(hwSel);
} catch (RuntimeException ex) {
throw ConversionUtils.throwOnError(ex, /* action= */ "tune");
}
diff --git a/services/core/java/com/android/server/camera/CameraServiceProxy.java b/services/core/java/com/android/server/camera/CameraServiceProxy.java
index e16ca0b..0b03005 100644
--- a/services/core/java/com/android/server/camera/CameraServiceProxy.java
+++ b/services/core/java/com/android/server/camera/CameraServiceProxy.java
@@ -74,6 +74,7 @@
import android.view.WindowManagerGlobal;
import com.android.framework.protobuf.nano.MessageNano;
+import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.FrameworkStatsLog;
import com.android.server.LocalServices;
@@ -389,6 +390,16 @@
return CaptureRequest.SCALER_ROTATE_AND_CROP_NONE;
}
+ // When config_isWindowManagerCameraCompatTreatmentEnabled is true,
+ // DisplayRotationCompatPolicy in WindowManager force rotates fullscreen activities with
+ // fixed orientation to align them with the natural orientation of the device.
+ if (ctx.getResources().getBoolean(
+ R.bool.config_isWindowManagerCameraCompatTreatmentEnabled)) {
+ Slog.v(TAG, "Disable Rotate and Crop to avoid conflicts with"
+ + " WM force rotation treatment.");
+ return CaptureRequest.SCALER_ROTATE_AND_CROP_NONE;
+ }
+
// External cameras do not need crop-rotate-scale.
if (lensFacing != CameraMetadata.LENS_FACING_FRONT
&& lensFacing != CameraMetadata.LENS_FACING_BACK) {
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index 1217e74..e557b50 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -2630,6 +2630,9 @@
}
}
+ /**
+ * Uniquely identifies a Sensor, with the combination of Type and Name.
+ */
static class SensorData {
public String type;
public String name;
diff --git a/services/core/java/com/android/server/display/DisplayDeviceInfo.java b/services/core/java/com/android/server/display/DisplayDeviceInfo.java
index fe1d1a6..b7b7031 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceInfo.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceInfo.java
@@ -180,6 +180,15 @@
public static final int FLAG_DEVICE_DISPLAY_GROUP = 1 << 18;
/**
+ * Flag: Indicates that the display should not become the top focused display by stealing the
+ * top focus from another display.
+ *
+ * @see Display#FLAG_STEAL_TOP_FOCUS_DISABLED
+ * @hide
+ */
+ public static final int FLAG_STEAL_TOP_FOCUS_DISABLED = 1 << 19;
+
+ /**
* Touch attachment: Display does not receive touch.
*/
public static final int TOUCH_NONE = 0;
@@ -649,6 +658,9 @@
if ((flags & FLAG_OWN_FOCUS) != 0) {
msg.append(", FLAG_OWN_FOCUS");
}
+ if ((flags & FLAG_STEAL_TOP_FOCUS_DISABLED) != 0) {
+ msg.append(", FLAG_STEAL_TOP_FOCUS_DISABLED");
+ }
return msg.toString();
}
}
diff --git a/services/core/java/com/android/server/display/DisplayModeDirector.java b/services/core/java/com/android/server/display/DisplayModeDirector.java
index 5dba015..f8d6c5f 100644
--- a/services/core/java/com/android/server/display/DisplayModeDirector.java
+++ b/services/core/java/com/android/server/display/DisplayModeDirector.java
@@ -50,7 +50,6 @@
import android.provider.DeviceConfigInterface;
import android.provider.Settings;
import android.sysprop.SurfaceFlingerProperties;
-import android.text.TextUtils;
import android.util.IndentingPrintWriter;
import android.util.Pair;
import android.util.Slog;
@@ -70,6 +69,7 @@
import com.android.server.LocalServices;
import com.android.server.display.utils.AmbientFilter;
import com.android.server.display.utils.AmbientFilterFactory;
+import com.android.server.display.utils.SensorUtils;
import com.android.server.sensors.SensorManagerInternal;
import com.android.server.sensors.SensorManagerInternal.ProximityActiveListener;
import com.android.server.statusbar.StatusBarManagerInternal;
@@ -683,14 +683,20 @@
}
/**
- * A utility to make this class aware of the new display configs whenever the default display is
- * changed
+ * Called when the underlying display device of the default display is changed.
+ * Some data in this class relates to the physical display of the device, and so we need to
+ * reload the configurations based on this.
+ * E.g. the brightness sensors and refresh rate capabilities depend on the physical display
+ * device that is being used, so will be reloaded.
+ *
+ * @param displayDeviceConfig configurations relating to the underlying display device.
*/
public void defaultDisplayDeviceUpdated(DisplayDeviceConfig displayDeviceConfig) {
mSettingsObserver.setRefreshRates(displayDeviceConfig,
/* attemptLoadingFromDeviceConfig= */ true);
mBrightnessObserver.updateBlockingZoneThresholds(displayDeviceConfig,
/* attemptLoadingFromDeviceConfig= */ true);
+ mBrightnessObserver.reloadLightSensor(displayDeviceConfig);
}
/**
@@ -1739,6 +1745,9 @@
private SensorManager mSensorManager;
private Sensor mLightSensor;
+ private Sensor mRegisteredLightSensor;
+ private String mLightSensorType;
+ private String mLightSensorName;
private final LightSensorEventListener mLightSensorListener =
new LightSensorEventListener();
// Take it as low brightness before valid sensor data comes
@@ -1899,17 +1908,8 @@
return mLowAmbientBrightnessThresholds;
}
- public void registerLightSensor(SensorManager sensorManager, Sensor lightSensor) {
- mSensorManager = sensorManager;
- mLightSensor = lightSensor;
-
- mSensorManager.registerListener(mLightSensorListener,
- mLightSensor, LIGHT_SENSOR_RATE_MS * 1000, mHandler);
- }
-
public void observe(SensorManager sensorManager) {
mSensorManager = sensorManager;
- final ContentResolver cr = mContext.getContentResolver();
mBrightness = getBrightness(Display.DEFAULT_DISPLAY);
// DeviceConfig is accessible after system ready.
@@ -2053,6 +2053,10 @@
pw.println(" mAmbientHighBrightnessThresholds: " + d);
}
+ pw.println(" mRegisteredLightSensor: " + mRegisteredLightSensor);
+ pw.println(" mLightSensor: " + mLightSensor);
+ pw.println(" mLightSensorName: " + mLightSensorName);
+ pw.println(" mLightSensorType: " + mLightSensorType);
mLightSensorListener.dumpLocked(pw);
if (mAmbientFilter != null) {
@@ -2106,27 +2110,9 @@
}
if (mShouldObserveAmbientLowChange || mShouldObserveAmbientHighChange) {
- Resources resources = mContext.getResources();
- String lightSensorType = resources.getString(
- com.android.internal.R.string.config_displayLightSensorType);
+ Sensor lightSensor = getLightSensor();
- Sensor lightSensor = null;
- if (!TextUtils.isEmpty(lightSensorType)) {
- List<Sensor> sensors = mSensorManager.getSensorList(Sensor.TYPE_ALL);
- for (int i = 0; i < sensors.size(); i++) {
- Sensor sensor = sensors.get(i);
- if (lightSensorType.equals(sensor.getStringType())) {
- lightSensor = sensor;
- break;
- }
- }
- }
-
- if (lightSensor == null) {
- lightSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_LIGHT);
- }
-
- if (lightSensor != null) {
+ if (lightSensor != null && lightSensor != mLightSensor) {
final Resources res = mContext.getResources();
mAmbientFilter = AmbientFilterFactory.createBrightnessFilter(TAG, res);
@@ -2137,14 +2123,40 @@
mLightSensor = null;
}
+ updateSensorStatus();
if (mRefreshRateChangeable) {
- updateSensorStatus();
synchronized (mLock) {
onBrightnessChangedLocked();
}
}
}
+ private void reloadLightSensor(DisplayDeviceConfig displayDeviceConfig) {
+ reloadLightSensorData(displayDeviceConfig);
+ restartObserver();
+ }
+
+ private void reloadLightSensorData(DisplayDeviceConfig displayDeviceConfig) {
+ // The displayDeviceConfig (ddc) contains display specific preferences. When loaded,
+ // it naturally falls back to the global config.xml.
+ if (displayDeviceConfig != null
+ && displayDeviceConfig.getAmbientLightSensor() != null) {
+ // This covers both the ddc and the config.xml fallback
+ mLightSensorType = displayDeviceConfig.getAmbientLightSensor().type;
+ mLightSensorName = displayDeviceConfig.getAmbientLightSensor().name;
+ } else if (mLightSensorName == null && mLightSensorType == null) {
+ Resources resources = mContext.getResources();
+ mLightSensorType = resources.getString(
+ com.android.internal.R.string.config_displayLightSensorType);
+ mLightSensorName = "";
+ }
+ }
+
+ private Sensor getLightSensor() {
+ return SensorUtils.findSensor(mSensorManager, mLightSensorType,
+ mLightSensorName, Sensor.TYPE_LIGHT);
+ }
+
/**
* Checks to see if at least one value is positive, in which case it is necessary to listen
* to value changes.
@@ -2288,17 +2300,36 @@
if ((mShouldObserveAmbientLowChange || mShouldObserveAmbientHighChange)
&& isDeviceActive() && !mLowPowerModeEnabled && mRefreshRateChangeable) {
- mSensorManager.registerListener(mLightSensorListener,
- mLightSensor, LIGHT_SENSOR_RATE_MS * 1000, mHandler);
- if (mLoggingEnabled) {
- Slog.d(TAG, "updateSensorStatus: registerListener");
- }
+ registerLightSensor();
+
} else {
- mLightSensorListener.removeCallbacks();
- mSensorManager.unregisterListener(mLightSensorListener);
- if (mLoggingEnabled) {
- Slog.d(TAG, "updateSensorStatus: unregisterListener");
- }
+ unregisterSensorListener();
+ }
+ }
+
+ private void registerLightSensor() {
+ if (mRegisteredLightSensor == mLightSensor) {
+ return;
+ }
+
+ if (mRegisteredLightSensor != null) {
+ unregisterSensorListener();
+ }
+
+ mSensorManager.registerListener(mLightSensorListener,
+ mLightSensor, LIGHT_SENSOR_RATE_MS * 1000, mHandler);
+ mRegisteredLightSensor = mLightSensor;
+ if (mLoggingEnabled) {
+ Slog.d(TAG, "updateSensorStatus: registerListener");
+ }
+ }
+
+ private void unregisterSensorListener() {
+ mLightSensorListener.removeCallbacks();
+ mSensorManager.unregisterListener(mLightSensorListener);
+ mRegisteredLightSensor = null;
+ if (mLoggingEnabled) {
+ Slog.d(TAG, "updateSensorStatus: unregisterListener");
}
}
diff --git a/services/core/java/com/android/server/display/LogicalDisplay.java b/services/core/java/com/android/server/display/LogicalDisplay.java
index ad426b5..dfdbce5 100644
--- a/services/core/java/com/android/server/display/LogicalDisplay.java
+++ b/services/core/java/com/android/server/display/LogicalDisplay.java
@@ -361,6 +361,9 @@
if ((deviceInfo.flags & DisplayDeviceInfo.FLAG_OWN_FOCUS) != 0) {
mBaseDisplayInfo.flags |= Display.FLAG_OWN_FOCUS;
}
+ if ((deviceInfo.flags & DisplayDeviceInfo.FLAG_STEAL_TOP_FOCUS_DISABLED) != 0) {
+ mBaseDisplayInfo.flags |= Display.FLAG_STEAL_TOP_FOCUS_DISABLED;
+ }
Rect maskingInsets = getMaskingInsets(deviceInfo);
int maskedWidth = deviceInfo.width - maskingInsets.left - maskingInsets.right;
int maskedHeight = deviceInfo.height - maskingInsets.top - maskingInsets.bottom;
diff --git a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
index 7c647cf..ddeaa1b 100644
--- a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
@@ -28,6 +28,7 @@
import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT;
import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_SECURE;
import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS;
+import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_STEAL_TOP_FOCUS_DISABLED;
import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH;
import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_TOUCH_FEEDBACK_DISABLED;
import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED;
@@ -35,6 +36,7 @@
import static com.android.server.display.DisplayDeviceInfo.FLAG_ALWAYS_UNLOCKED;
import static com.android.server.display.DisplayDeviceInfo.FLAG_DEVICE_DISPLAY_GROUP;
import static com.android.server.display.DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP;
+import static com.android.server.display.DisplayDeviceInfo.FLAG_STEAL_TOP_FOCUS_DISABLED;
import static com.android.server.display.DisplayDeviceInfo.FLAG_TOUCH_FEEDBACK_DISABLED;
import static com.android.server.display.DisplayDeviceInfo.FLAG_TRUSTED;
@@ -526,6 +528,17 @@
+ "VIRTUAL_DISPLAY_FLAG_TRUSTED.");
}
}
+ if ((mFlags & VIRTUAL_DISPLAY_FLAG_STEAL_TOP_FOCUS_DISABLED) != 0) {
+ if ((mFlags & VIRTUAL_DISPLAY_FLAG_TRUSTED) != 0
+ && (mFlags & VIRTUAL_DISPLAY_FLAG_OWN_FOCUS) != 0) {
+ mInfo.flags |= FLAG_STEAL_TOP_FOCUS_DISABLED;
+ } else {
+ Slog.w(TAG,
+ "Ignoring VIRTUAL_DISPLAY_FLAG_STEAL_TOP_FOCUS_DISABLED as it "
+ + "requires VIRTUAL_DISPLAY_FLAG_OWN_FOCUS which requires "
+ + "VIRTUAL_DISPLAY_FLAG_TRUSTED.");
+ }
+ }
mInfo.type = Display.TYPE_VIRTUAL;
mInfo.touch = ((mFlags & VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH) == 0) ?
@@ -608,6 +621,13 @@
// expect), and there will still be letterboxing on the output content since the
// Surface and VirtualDisplay would then have different aspect ratios.
}
+
+ @Override
+ public void onCapturedContentVisibilityChanged(boolean isVisible) {
+ // Do nothing when we tell the client that the content has a visibility change - it is
+ // up to them to decide to pause recording, and update their own UI, depending on their
+ // use case.
+ }
}
@VisibleForTesting
diff --git a/services/core/java/com/android/server/display/utils/SensorUtils.java b/services/core/java/com/android/server/display/utils/SensorUtils.java
index 4924ad5..48bc46c 100644
--- a/services/core/java/com/android/server/display/utils/SensorUtils.java
+++ b/services/core/java/com/android/server/display/utils/SensorUtils.java
@@ -33,6 +33,10 @@
*/
public static Sensor findSensor(SensorManager sensorManager, String sensorType,
String sensorName, int fallbackType) {
+ if (sensorManager == null) {
+ return null;
+ }
+
if ("".equals(sensorName) && "".equals(sensorType)) {
return null;
}
diff --git a/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java
new file mode 100644
index 0000000..11a4294
--- /dev/null
+++ b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java
@@ -0,0 +1,76 @@
+/*
+ * 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.grammaticalinflection;
+
+import static android.content.res.Configuration.GRAMMATICAL_GENDER_NOT_SPECIFIED;
+
+import android.app.IGrammaticalInflectionManager;
+import android.content.Context;
+import android.os.IBinder;
+
+import com.android.server.LocalServices;
+import com.android.server.SystemService;
+import com.android.server.wm.ActivityTaskManagerInternal;
+
+/**
+ * The implementation of IGrammaticalInflectionManager.aidl.
+ *
+ * <p>This service is API entry point for storing app-specific grammatical inflection.
+ */
+public class GrammaticalInflectionService extends SystemService {
+
+ private final ActivityTaskManagerInternal mActivityTaskManagerInternal;
+
+ /**
+ * Initializes the system service.
+ * <p>
+ * Subclasses must define a single argument constructor that accepts the context
+ * and passes it to super.
+ * </p>
+ *
+ * @param context The system server context.
+ *
+ * @hide
+ */
+ public GrammaticalInflectionService(Context context) {
+ super(context);
+ mActivityTaskManagerInternal = LocalServices.getService(ActivityTaskManagerInternal.class);
+ }
+
+ @Override
+ public void onStart() {
+ publishBinderService(Context.GRAMMATICAL_INFLECTION_SERVICE, mService);
+ }
+
+ private final IBinder mService = new IGrammaticalInflectionManager.Stub() {
+ @Override
+ public void setRequestedApplicationGrammaticalGender(
+ String appPackageName, int userId, int gender) {
+ GrammaticalInflectionService.this.setRequestedApplicationGrammaticalGender(
+ appPackageName, userId, gender);
+ }
+ };
+
+ private void setRequestedApplicationGrammaticalGender(
+ String appPackageName, int userId, int gender) {
+ final ActivityTaskManagerInternal.PackageConfigurationUpdater updater =
+ mActivityTaskManagerInternal.createPackageConfigurationUpdater(appPackageName,
+ userId);
+
+ updater.setGrammaticalGender(gender).commit();
+ }
+}
diff --git a/services/core/java/com/android/server/hdmi/Constants.java b/services/core/java/com/android/server/hdmi/Constants.java
index aaa9ee5..5353092 100644
--- a/services/core/java/com/android/server/hdmi/Constants.java
+++ b/services/core/java/com/android/server/hdmi/Constants.java
@@ -599,6 +599,26 @@
})
@interface RcProfileSource {}
+ static final int HDMI_EARC_STATUS_IDLE = 0; // IDLE1
+ static final int HDMI_EARC_STATUS_EARC_PENDING = 1; // DISC1 and DISC2
+ static final int HDMI_EARC_STATUS_ARC_PENDING = 2; // IDLE2 for ARC
+ static final int HDMI_EARC_STATUS_EARC_CONNECTED = 3; // eARC connected
+ @IntDef({
+ HDMI_EARC_STATUS_IDLE,
+ HDMI_EARC_STATUS_EARC_PENDING,
+ HDMI_EARC_STATUS_ARC_PENDING,
+ HDMI_EARC_STATUS_EARC_CONNECTED
+ })
+ @interface EarcStatus {}
+
+ static final int HDMI_HPD_TYPE_PHYSICAL = 0; // Default. Physical hotplug signal.
+ static final int HDMI_HPD_TYPE_STATUS_BIT = 1; // HDMI_HPD status bit.
+ @IntDef({
+ HDMI_HPD_TYPE_PHYSICAL,
+ HDMI_HPD_TYPE_STATUS_BIT
+ })
+ @interface HpdSignalType {}
+
private Constants() {
/* cannot be instantiated */
}
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecController.java b/services/core/java/com/android/server/hdmi/HdmiCecController.java
index f3c67fb9..2e73a41 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecController.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecController.java
@@ -70,6 +70,10 @@
* <p>It can be created only by {@link HdmiCecController#create}
*
* <p>Declared as package-private, accessed by {@link HdmiControlService} only.
+ *
+ * <p>Also manages HDMI HAL methods that are shared between CEC and eARC. To make eARC
+ * fully independent of the presence of a CEC HAL, we should split this class into HdmiCecController
+ * and HdmiController TODO(b/255751565).
*/
final class HdmiCecController {
private static final String TAG = "HdmiCecController";
@@ -413,6 +417,31 @@
}
/**
+ * Configures the type of HDP signal that the driver and HAL use for actions other than eARC,
+ * such as signaling EDID updates.
+ */
+ @ServiceThreadOnly
+ void setHpdSignalType(@Constants.HpdSignalType int signal, int portId) {
+ assertRunOnServiceThread();
+ // Stub.
+ // TODO: bind to native.
+ // TODO: handle error return values here, with logging.
+ }
+
+ /**
+ * Gets the type of the HDP signal that the driver and HAL use for actions other than eARC,
+ * such as signaling EDID updates.
+ */
+ @ServiceThreadOnly
+ @Constants.HpdSignalType
+ int getHpdSignalType(int portId) {
+ assertRunOnServiceThread();
+ // Stub.
+ // TODO: bind to native.
+ return Constants.HDMI_HPD_TYPE_PHYSICAL;
+ }
+
+ /**
* Informs CEC HAL about the current system language.
*
* @param language Three-letter code defined in ISO/FDIS 639-2. Must be lowercase letters.
@@ -1066,6 +1095,8 @@
HdmiPortInfo[] hdmiPortInfo = new HdmiPortInfo[hdmiPortInfos.length];
int i = 0;
for (android.hardware.tv.hdmi.HdmiPortInfo portInfo : hdmiPortInfos) {
+ // TODO: the earc argument is stubbed for now.
+ // To be replaced by portInfo.earcSupported.
hdmiPortInfo[i] =
new HdmiPortInfo(
portInfo.portId,
@@ -1073,7 +1104,8 @@
portInfo.physicalAddress,
portInfo.cecSupported,
false,
- portInfo.arcSupported);
+ portInfo.arcSupported,
+ false);
i++;
}
return hdmiPortInfo;
@@ -1234,7 +1266,8 @@
portInfo.physicalAddress,
portInfo.cecSupported,
false,
- portInfo.arcSupported);
+ portInfo.arcSupported,
+ false);
i++;
}
return hdmiPortInfo;
@@ -1415,7 +1448,8 @@
portInfo.physicalAddress,
portInfo.cecSupported,
false,
- portInfo.arcSupported);
+ portInfo.arcSupported,
+ false);
i++;
}
return hdmiPortInfo;
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
index b4d7fb9..91f58db 100755
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
@@ -54,7 +54,7 @@
* Class that models a logical CEC device hosted in this system. Handles initialization, CEC
* commands that call for actions customized per device type.
*/
-abstract class HdmiCecLocalDevice {
+abstract class HdmiCecLocalDevice extends HdmiLocalDevice {
private static final String TAG = "HdmiCecLocalDevice";
private static final int MAX_HDMI_ACTIVE_SOURCE_HISTORY = 10;
@@ -67,8 +67,6 @@
// When it expires, we can assume <User Control Release> is received.
private static final int FOLLOWER_SAFETY_TIMEOUT = 550;
- protected final HdmiControlService mService;
- protected final int mDeviceType;
protected int mPreferredAddress;
@GuardedBy("mLock")
private HdmiDeviceInfo mDeviceInfo;
@@ -154,8 +152,6 @@
private int mActiveRoutingPath;
protected final HdmiCecMessageCache mCecMessageCache = new HdmiCecMessageCache();
- @VisibleForTesting
- protected final Object mLock;
// A collection of FeatureAction.
// Note that access to this collection should happen in service thread.
@@ -188,9 +184,7 @@
protected PendingActionClearedCallback mPendingActionClearedCallback;
protected HdmiCecLocalDevice(HdmiControlService service, int deviceType) {
- mService = service;
- mDeviceType = deviceType;
- mLock = service.getServiceLock();
+ super(service, deviceType);
}
// Factory method that returns HdmiCecLocalDevice of corresponding type.
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
index 96e7b03..6303bdc 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
@@ -899,10 +899,16 @@
@ServiceThreadOnly
void startArcAction(boolean enabled) {
+ startArcAction(enabled, null);
+ }
+
+ @ServiceThreadOnly
+ void startArcAction(boolean enabled, IHdmiControlCallback callback) {
assertRunOnServiceThread();
HdmiDeviceInfo info = getAvrDeviceInfo();
if (info == null) {
Slog.w(TAG, "Failed to start arc action; No AVR device.");
+ invokeCallback(callback, HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
return;
}
if (!canStartArcUpdateAction(info.getLogicalAddress(), enabled)) {
@@ -910,19 +916,37 @@
if (enabled && !isConnectedToArcPort(info.getPhysicalAddress())) {
displayOsd(OSD_MESSAGE_ARC_CONNECTED_INVALID_PORT);
}
+ invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE);
+ return;
+ }
+ if (enabled && mService.earcBlocksArcConnection()) {
+ Slog.i(TAG,
+ "ARC connection blocked because eARC connection is established or being "
+ + "established.");
+ invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE);
return;
}
- // Terminate opposite action and start action if not exist.
+ // Terminate opposite action and create an action with callback.
if (enabled) {
removeAction(RequestArcTerminationAction.class);
- if (!hasAction(RequestArcInitiationAction.class)) {
- addAndStartAction(new RequestArcInitiationAction(this, info.getLogicalAddress()));
+ if (hasAction(RequestArcInitiationAction.class)) {
+ RequestArcInitiationAction existingInitiationAction =
+ getActions(RequestArcInitiationAction.class).get(0);
+ existingInitiationAction.addCallback(callback);
+ } else {
+ addAndStartAction(
+ new RequestArcInitiationAction(this, info.getLogicalAddress(), callback));
}
} else {
removeAction(RequestArcInitiationAction.class);
- if (!hasAction(RequestArcTerminationAction.class)) {
- addAndStartAction(new RequestArcTerminationAction(this, info.getLogicalAddress()));
+ if (hasAction(RequestArcTerminationAction.class)) {
+ RequestArcTerminationAction existingTerminationAction =
+ getActions(RequestArcTerminationAction.class).get(0);
+ existingTerminationAction.addCallback(callback);
+ } else {
+ addAndStartAction(
+ new RequestArcTerminationAction(this, info.getLogicalAddress(), callback));
}
}
}
@@ -1010,6 +1034,13 @@
protected int handleInitiateArc(HdmiCecMessage message) {
assertRunOnServiceThread();
+ if (mService.earcBlocksArcConnection()) {
+ Slog.i(TAG,
+ "ARC connection blocked because eARC connection is established or being "
+ + "established.");
+ return Constants.ABORT_NOT_IN_CORRECT_MODE;
+ }
+
if (!canStartArcUpdateAction(message.getSource(), true)) {
HdmiDeviceInfo avrDeviceInfo = getAvrDeviceInfo();
if (avrDeviceInfo == null) {
@@ -1023,9 +1054,8 @@
return Constants.ABORT_REFUSED;
}
- // In case where <Initiate Arc> is started by <Request ARC Initiation>
- // need to clean up RequestArcInitiationAction.
- removeAction(RequestArcInitiationAction.class);
+ // In case where <Initiate Arc> is started by <Request ARC Initiation>, this message is
+ // handled in RequestArcInitiationAction as well.
SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
message.getSource(), true);
addAndStartAction(action);
@@ -1059,9 +1089,8 @@
return Constants.HANDLED;
}
// Do not check ARC configuration since the AVR might have been already removed.
- // Clean up RequestArcTerminationAction in case <Terminate Arc> was started by
- // <Request ARC Termination>.
- removeAction(RequestArcTerminationAction.class);
+ // In case where <Terminate Arc> is started by <Request ARC Termination>, this
+ // message is handled in RequestArcTerminationAction as well.
SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
message.getSource(), false);
addAndStartAction(action);
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecNetwork.java b/services/core/java/com/android/server/hdmi/HdmiCecNetwork.java
index f708941..a57292a 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecNetwork.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecNetwork.java
@@ -470,7 +470,8 @@
for (HdmiPortInfo info : cecPortInfo) {
if (mhlSupportedPorts.contains(info.getId())) {
result.add(new HdmiPortInfo(info.getId(), info.getType(), info.getAddress(),
- info.isCecSupported(), true, info.isArcSupported()));
+ info.isCecSupported(), true, info.isArcSupported(),
+ info.isEarcSupported()));
} else {
result.add(info);
}
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index 2f15e57..f66f8ea 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -18,6 +18,7 @@
import static android.hardware.hdmi.HdmiControlManager.DEVICE_EVENT_ADD_DEVICE;
import static android.hardware.hdmi.HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE;
+import static android.hardware.hdmi.HdmiControlManager.EARC_FEATURE_ENABLED;
import static android.hardware.hdmi.HdmiControlManager.HDMI_CEC_CONTROL_ENABLED;
import static android.hardware.hdmi.HdmiControlManager.SOUNDBAR_MODE_DISABLED;
import static android.hardware.hdmi.HdmiControlManager.SOUNDBAR_MODE_ENABLED;
@@ -25,6 +26,7 @@
import static com.android.server.hdmi.Constants.ADDR_UNREGISTERED;
import static com.android.server.hdmi.Constants.DISABLED;
import static com.android.server.hdmi.Constants.ENABLED;
+import static com.android.server.hdmi.Constants.HDMI_EARC_STATUS_ARC_PENDING;
import static com.android.server.hdmi.Constants.OPTION_MHL_ENABLE;
import static com.android.server.hdmi.Constants.OPTION_MHL_INPUT_SWITCHING;
import static com.android.server.hdmi.Constants.OPTION_MHL_POWER_CHARGE;
@@ -126,6 +128,8 @@
/**
* Provides a service for sending and processing HDMI control messages,
* HDMI-CEC and MHL control command, and providing the information on both standard.
+ *
+ * Additionally takes care of establishing and managing an eARC connection.
*/
public class HdmiControlService extends SystemService {
private static final String TAG = "HdmiControlService";
@@ -184,13 +188,14 @@
static final String PERMISSION = "android.permission.HDMI_CEC";
- // The reason code to initiate initializeCec().
+ // The reason code to initiate initializeCec() and initializeEarc().
static final int INITIATED_BY_ENABLE_CEC = 0;
static final int INITIATED_BY_BOOT_UP = 1;
static final int INITIATED_BY_SCREEN_ON = 2;
static final int INITIATED_BY_WAKE_UP_MESSAGE = 3;
static final int INITIATED_BY_HOTPLUG = 4;
static final int INITIATED_BY_SOUNDBAR_MODE = 5;
+ static final int INITIATED_BY_ENABLE_EARC = 6;
// The reason code representing the intent action that drives the standby
// procedure. The procedure starts either by Intent.ACTION_SCREEN_OFF or
@@ -384,6 +389,18 @@
@HdmiControlManager.HdmiCecControl
private int mHdmiControlEnabled;
+ // Set to true while the eARC feature is supported by the hardware on at least one port
+ // and the eARC HAL is present.
+ @GuardedBy("mLock")
+ @VisibleForTesting
+ private boolean mEarcSupported;
+
+ // Set to true while the eARC feature is enabled.
+ @GuardedBy("mLock")
+ private boolean mEarcEnabled;
+
+ private int mEarcPortId = -1;
+
// Set to true while the service is in normal mode. While set to false, no input change is
// allowed. Used for situations where input change can confuse users such as channel auto-scan,
// system upgrade, etc., a.k.a. "prohibit mode".
@@ -417,6 +434,12 @@
private HdmiCecPowerStatusController mPowerStatusController;
+ @Nullable
+ private HdmiEarcController mEarcController;
+
+ @Nullable
+ private HdmiEarcLocalDevice mEarcLocalDevice;
+
@ServiceThreadOnly
private String mMenuLanguage = localeToMenuLanguage(Locale.getDefault());
@@ -631,9 +654,13 @@
mPowerStatusController = new HdmiCecPowerStatusController(this);
}
mPowerStatusController.setPowerStatus(getInitialPowerStatus());
- mProhibitMode = false;
+ setProhibitMode(false);
mHdmiControlEnabled = mHdmiCecConfig.getIntValue(
HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_ENABLED);
+ synchronized (mLock) {
+ mEarcEnabled = (mHdmiCecConfig.getIntValue(
+ HdmiControlManager.SETTING_NAME_EARC_ENABLED) == EARC_FEATURE_ENABLED);
+ }
setHdmiCecVolumeControlEnabledInternal(getHdmiCecConfig().getIntValue(
HdmiControlManager.CEC_SETTING_NAME_VOLUME_CONTROL_MODE));
mMhlInputChangeEnabled = readBooleanSetting(Global.MHL_INPUT_SWITCHING_ENABLED, true);
@@ -646,7 +673,6 @@
}
if (mCecController == null) {
Slog.i(TAG, "Device does not support HDMI-CEC.");
- return;
}
if (mMhlController == null) {
mMhlController = HdmiMhlControllerStub.create(this);
@@ -654,15 +680,56 @@
if (!mMhlController.isReady()) {
Slog.i(TAG, "Device does not support MHL-control.");
}
+ if (mEarcController == null) {
+ mEarcController = HdmiEarcController.create(this);
+ }
+ if (mEarcController == null) {
+ Slog.i(TAG, "Device does not support eARC.");
+ }
+ if (mCecController == null && mEarcController == null) {
+ return;
+ }
mHdmiCecNetwork = new HdmiCecNetwork(this, mCecController, mMhlController);
- if (mHdmiControlEnabled == HdmiControlManager.HDMI_CEC_CONTROL_ENABLED) {
+ if (isCecControlEnabled()) {
initializeCec(INITIATED_BY_BOOT_UP);
} else {
mCecController.enableCec(false);
}
- mMhlDevices = Collections.emptyList();
+
+ synchronized (mLock) {
+ mMhlDevices = Collections.emptyList();
+ }
mHdmiCecNetwork.initPortInfo();
+ List<HdmiPortInfo> ports = getPortInfo();
+ synchronized (mLock) {
+ mEarcSupported = false;
+ for (HdmiPortInfo port : ports) {
+ boolean earcSupportedOnPort = port.isEarcSupported();
+ if (earcSupportedOnPort && mEarcSupported) {
+ // This means that more than 1 port supports eARC.
+ // The HDMI specification only allows 1 active eARC connection.
+ // Android does not support devices with multiple eARC-enabled ports.
+ // Consider eARC not supported in this case.
+ Slog.e(TAG, "HDMI eARC supported on more than 1 port.");
+ mEarcSupported = false;
+ mEarcPortId = -1;
+ break;
+ } else if (earcSupportedOnPort) {
+ mEarcPortId = port.getId();
+ mEarcSupported = earcSupportedOnPort;
+ }
+ }
+ mEarcSupported &= (mEarcController != null);
+ }
+ if (isEarcSupported()) {
+ if (isEarcEnabled()) {
+ initializeEarc(INITIATED_BY_BOOT_UP);
+ } else {
+ setEarcEnabledInHal(false, false);
+ }
+ }
+
mHdmiCecConfig.registerChangeListener(HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_ENABLED,
new HdmiCecConfig.SettingChangeListener() {
@Override
@@ -743,8 +810,16 @@
mCecController.enableWakeupByOtp(tv().getAutoWakeup());
}
}
- },
- mServiceThreadExecutor);
+ }, mServiceThreadExecutor);
+ mHdmiCecConfig.registerChangeListener(HdmiControlManager.SETTING_NAME_EARC_ENABLED,
+ new HdmiCecConfig.SettingChangeListener() {
+ @Override
+ public void onChange(String setting) {
+ @HdmiControlManager.HdmiCecControl int enabled = mHdmiCecConfig.getIntValue(
+ HdmiControlManager.SETTING_NAME_EARC_ENABLED);
+ setEarcEnabled(enabled);
+ }
+ }, mServiceThreadExecutor);
}
/** Returns true if the device screen is off */
@@ -1083,7 +1158,8 @@
}
@ServiceThreadOnly
- private void initializeCecLocalDevices(final int initiatedBy) {
+ @VisibleForTesting
+ protected void initializeCecLocalDevices(final int initiatedBy) {
assertRunOnServiceThread();
// A container for [Device type, Local device info].
ArrayList<HdmiCecLocalDevice> localDevices = new ArrayList<>();
@@ -1245,7 +1321,6 @@
* Returns {@link Looper} of main thread. Use this {@link Looper} instance
* for tasks that are running on main service thread.
*/
- @VisibleForTesting
protected Looper getServiceLooper() {
return mHandler.getLooper();
}
@@ -2568,7 +2643,9 @@
if (!DumpUtils.checkDumpPermission(getContext(), TAG, writer)) return;
final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ");
- pw.println("mProhibitMode: " + mProhibitMode);
+ synchronized (mLock) {
+ pw.println("mProhibitMode: " + mProhibitMode);
+ }
pw.println("mPowerStatus: " + mPowerStatusController.getPowerStatus());
pw.println("mIsCecAvailable: " + mIsCecAvailable);
pw.println("mCecVersion: " + mCecVersion);
@@ -2577,9 +2654,9 @@
// System settings
pw.println("System_settings:");
pw.increaseIndent();
- pw.println("mMhlInputChangeEnabled: " + mMhlInputChangeEnabled);
+ pw.println("mMhlInputChangeEnabled: " + isMhlInputChangeEnabled());
pw.println("mSystemAudioActivated: " + isSystemAudioActivated());
- pw.println("mHdmiCecVolumeControlEnabled: " + mHdmiCecVolumeControl);
+ pw.println("mHdmiCecVolumeControlEnabled: " + getHdmiCecVolumeControl());
pw.decreaseIndent();
// CEC settings
@@ -2605,6 +2682,14 @@
pw.increaseIndent();
mMhlController.dump(pw);
pw.decreaseIndent();
+ pw.print("eARC local device: ");
+ pw.increaseIndent();
+ if (mEarcLocalDevice == null) {
+ pw.println("None. eARC is either disabled or not available.");
+ } else {
+ mEarcLocalDevice.dump(pw);
+ }
+ pw.decreaseIndent();
mHdmiCecNetwork.dump(pw);
if (mCecController != null) {
pw.println("mCecController: ");
@@ -3152,6 +3237,9 @@
}
private void invokeCallback(IHdmiControlCallback callback, int result) {
+ if (callback == null) {
+ return;
+ }
try {
callback.onComplete(result);
} catch (RemoteException e) {
@@ -3313,6 +3401,18 @@
}
}
+ private boolean isEarcEnabled() {
+ synchronized (mLock) {
+ return mEarcEnabled;
+ }
+ }
+
+ private boolean isEarcSupported() {
+ synchronized (mLock) {
+ return mEarcSupported;
+ }
+ }
+
@ServiceThreadOnly
int getPowerStatus() {
assertRunOnServiceThread();
@@ -3384,7 +3484,7 @@
mPowerStatusController.setPowerStatus(HdmiControlManager.POWER_STATUS_TRANSIENT_TO_ON,
false);
if (mCecController != null) {
- if (mHdmiControlEnabled == HDMI_CEC_CONTROL_ENABLED) {
+ if (isCecControlEnabled()) {
int startReason = -1;
switch (wakeUpAction) {
case WAKE_UP_SCREEN_ON:
@@ -3406,6 +3506,25 @@
} else {
Slog.i(TAG, "Device does not support HDMI-CEC.");
}
+ if (isEarcSupported()) {
+ if (isEarcEnabled()) {
+ int startReason = -1;
+ switch (wakeUpAction) {
+ case WAKE_UP_SCREEN_ON:
+ startReason = INITIATED_BY_SCREEN_ON;
+ break;
+ case WAKE_UP_BOOT_UP:
+ startReason = INITIATED_BY_BOOT_UP;
+ break;
+ default:
+ Slog.e(TAG, "wakeUpAction " + wakeUpAction + " not defined.");
+ return;
+ }
+ initializeEarc(startReason);
+ } else {
+ setEarcEnabledInHal(false, false);
+ }
+ }
// TODO: Initialize MHL local devices.
}
@@ -4296,4 +4415,176 @@
getAudioManager().setStreamVolume(AudioManager.STREAM_MUSIC,
volume * mStreamMusicMaxVolume / AudioStatus.MAX_VOLUME, flags);
}
+
+ private void initializeEarc(int initiatedBy) {
+ Slog.i(TAG, "eARC initialized, reason = " + initiatedBy);
+ initializeEarcLocalDevice(initiatedBy);
+
+ if (initiatedBy == INITIATED_BY_ENABLE_EARC) {
+ // Since ARC and eARC cannot be connected simultaneously, we need to terminate ARC
+ // before even enabling eARC.
+ setEarcEnabledInHal(true, true);
+ } else {
+ // On boot, wake-up, and hotplug in, eARC will always be attempted before ARC.
+ // So there is no need to explicitly terminate ARC before enabling eARC.
+ setEarcEnabledInHal(true, false);
+ }
+ }
+
+ @ServiceThreadOnly
+ @VisibleForTesting
+ protected void initializeEarcLocalDevice(final int initiatedBy) {
+ // TODO remove initiatedBy argument if it stays unused
+ assertRunOnServiceThread();
+ if (mEarcLocalDevice == null) {
+ mEarcLocalDevice = HdmiEarcLocalDevice.create(this, HdmiDeviceInfo.DEVICE_TV);
+ }
+ // TODO create HdmiEarcLocalDeviceRx if we're an audio system device.
+ }
+
+ @ServiceThreadOnly
+ @VisibleForTesting
+ protected void setEarcEnabled(@HdmiControlManager.EarcFeature int enabled) {
+ assertRunOnServiceThread();
+ synchronized (mLock) {
+ mEarcEnabled = (enabled == EARC_FEATURE_ENABLED);
+
+ if (!isEarcSupported()) {
+ Slog.i(TAG, "Enabled/disabled eARC setting, but the hardware doesn´t support eARC. "
+ + "This settings change doesn´t have an effect.");
+ return;
+ }
+
+ if (mEarcEnabled) {
+ onEnableEarc();
+ return;
+ }
+ }
+ runOnServiceThread(new Runnable() {
+ @Override
+ public void run() {
+ onDisableEarc();
+ }
+ });
+ }
+
+ @VisibleForTesting
+ protected void setEarcSupported(boolean supported) {
+ synchronized (mLock) {
+ mEarcSupported = supported;
+ }
+ }
+
+ @ServiceThreadOnly
+ private void onEnableEarc() {
+ // This will terminate ARC as well.
+ initializeEarc(INITIATED_BY_ENABLE_EARC);
+ }
+
+ @ServiceThreadOnly
+ private void onDisableEarc() {
+ disableEarcLocalDevice();
+ setEarcEnabledInHal(false, false);
+ clearEarcLocalDevice();
+ }
+
+ @ServiceThreadOnly
+ @VisibleForTesting
+ protected void clearEarcLocalDevice() {
+ assertRunOnServiceThread();
+ mEarcLocalDevice = null;
+ }
+
+ @ServiceThreadOnly
+ @VisibleForTesting
+ protected void addEarcLocalDevice(HdmiEarcLocalDevice localDevice) {
+ assertRunOnServiceThread();
+ mEarcLocalDevice = localDevice;
+ }
+
+ @ServiceThreadOnly
+ @VisibleForTesting
+ HdmiEarcLocalDevice getEarcLocalDevice() {
+ assertRunOnServiceThread();
+ return mEarcLocalDevice;
+ }
+
+ private void disableEarcLocalDevice() {
+ if (mEarcLocalDevice == null) {
+ return;
+ }
+ mEarcLocalDevice.disableDevice();
+ }
+
+ @ServiceThreadOnly
+ @VisibleForTesting
+ protected void setEarcEnabledInHal(boolean enabled, boolean terminateArcFirst) {
+ assertRunOnServiceThread();
+ if (terminateArcFirst) {
+ startArcAction(false, new IHdmiControlCallback.Stub() {
+ @Override
+ public void onComplete(int result) throws RemoteException {
+ // Independently of the result (i.e. independently of whether the ARC RX device
+ // responded with <Terminate ARC> or not), we always end up terminating ARC in
+ // the HAL. As soon as we do that, we can enable eARC in the HAL.
+ mEarcController.setEarcEnabled(enabled);
+ mCecController.setHpdSignalType(
+ enabled ? Constants.HDMI_HPD_TYPE_STATUS_BIT
+ : Constants.HDMI_HPD_TYPE_PHYSICAL,
+ mEarcPortId);
+ }
+ });
+ } else {
+ mEarcController.setEarcEnabled(enabled);
+ mCecController.setHpdSignalType(
+ enabled ? Constants.HDMI_HPD_TYPE_STATUS_BIT : Constants.HDMI_HPD_TYPE_PHYSICAL,
+ mEarcPortId);
+ }
+ }
+
+ @ServiceThreadOnly
+ void handleEarcStateChange(int status, int portId) {
+ assertRunOnServiceThread();
+ if (!getPortInfo(portId).isEarcSupported()) {
+ Slog.w(TAG, "Tried to update eARC status on a port that doesn't support eARC.");
+ return;
+ }
+ // If eARC is disabled, the local device is null. In this case, the HAL shouldn't have
+ // reported connection state changes, but even if it did, it won't take effect.
+ if (mEarcLocalDevice != null) {
+ mEarcLocalDevice.handleEarcStateChange(status);
+ }
+ }
+
+ @ServiceThreadOnly
+ void handleEarcCapabilitiesReported(byte[] rawCapabilities, int portId) {
+ assertRunOnServiceThread();
+ if (!getPortInfo(portId).isEarcSupported()) {
+ Slog.w(TAG,
+ "Tried to process eARC capabilities from a port that doesn't support eARC.");
+ return;
+ }
+ // If eARC is disabled, the local device is null. In this case, the HAL shouldn't have
+ // reported eARC capabilities, but even if it did, it won't take effect.
+ if (mEarcLocalDevice != null) {
+ mEarcLocalDevice.handleEarcCapabilitiesReported(rawCapabilities);
+ }
+ }
+
+ protected boolean earcBlocksArcConnection() {
+ if (mEarcLocalDevice == null) {
+ return false;
+ }
+ synchronized (mLock) {
+ return mEarcLocalDevice.mEarcStatus != HDMI_EARC_STATUS_ARC_PENDING;
+ }
+ }
+
+ protected void startArcAction(boolean enabled, IHdmiControlCallback callback) {
+ if (!isTvDeviceEnabled()) {
+ invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE);
+ } else {
+ tv().startArcAction(enabled, callback);
+ }
+ }
}
diff --git a/services/core/java/com/android/server/hdmi/HdmiEarcController.java b/services/core/java/com/android/server/hdmi/HdmiEarcController.java
new file mode 100644
index 0000000..8522509
--- /dev/null
+++ b/services/core/java/com/android/server/hdmi/HdmiEarcController.java
@@ -0,0 +1,117 @@
+/*
+ * 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.hdmi;
+
+import android.os.Handler;
+import android.os.Looper;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+final class HdmiEarcController {
+ private static final String TAG = "HdmiEarcController";
+
+ // Handler instance to process HAL calls.
+ private Handler mControlHandler;
+
+ private final HdmiControlService mService;
+
+ // Private constructor. Use HdmiEarcController.create().
+ private HdmiEarcController(HdmiControlService service) {
+ mService = service;
+ }
+
+ /**
+ * A factory method to get {@link HdmiEarcController}. If it fails to initialize
+ * inner device or has no device it will return {@code null}.
+ *
+ * <p>Declared as package-private, accessed by {@link HdmiControlService} only.
+ * @param service {@link HdmiControlService} instance used to create internal handler
+ * and to pass callback for incoming message or event.
+ * @return {@link HdmiEarcController} if device is initialized successfully. Otherwise,
+ * returns {@code null}.
+ */
+ static HdmiEarcController create(HdmiControlService service) {
+ // TODO add the native wrapper and return null if eARC HAL is not present.
+ HdmiEarcController controller = new HdmiEarcController(service);
+ controller.init();
+ return controller;
+ }
+
+ private void init() {
+ mControlHandler = new Handler(mService.getServiceLooper());
+ }
+
+ private void assertRunOnServiceThread() {
+ if (Looper.myLooper() != mControlHandler.getLooper()) {
+ throw new IllegalStateException("Should run on service thread.");
+ }
+ }
+
+ @VisibleForTesting
+ void runOnServiceThread(Runnable runnable) {
+ mControlHandler.post(new WorkSourceUidPreservingRunnable(runnable));
+ }
+
+ /**
+ * Enable eARC in the HAL
+ * @param enabled
+ */
+ @HdmiAnnotations.ServiceThreadOnly
+ void setEarcEnabled(boolean enabled) {
+ assertRunOnServiceThread();
+ // Stub.
+ // TODO: bind to native.
+ // TODO: handle error return values here, with logging.
+ }
+
+ /**
+ * Getter for the current eARC state.
+ * @param portId the ID of the port on which to get the connection state
+ * @return the current eARC state
+ */
+ @HdmiAnnotations.ServiceThreadOnly
+ @Constants.EarcStatus
+ int getState(int portId) {
+ // Stub.
+ // TODO: bind to native.
+ return Constants.HDMI_EARC_STATUS_IDLE;
+ }
+
+ /**
+ * Ask the HAL to report the last eARC capabilities that the connected audio system reported.
+ * @return the raw eARC capabilities
+ */
+ @HdmiAnnotations.ServiceThreadOnly
+ byte[] getLastReportedCaps() {
+ // Stub. TODO: bind to native.
+ return new byte[] {};
+ }
+
+ final class EarcCallback {
+ public void onStateChange(@Constants.EarcStatus int status, int portId) {
+ runOnServiceThread(
+ () -> mService.handleEarcStateChange(status, portId));
+ }
+
+ public void onCapabilitiesReported(byte[] rawCapabilities, int portId) {
+ runOnServiceThread(
+ () -> mService.handleEarcCapabilitiesReported(rawCapabilities, portId));
+ }
+ }
+
+ // TODO: bind to native.
+}
diff --git a/services/core/java/com/android/server/hdmi/HdmiEarcLocalDevice.java b/services/core/java/com/android/server/hdmi/HdmiEarcLocalDevice.java
new file mode 100644
index 0000000..e95ecd2
--- /dev/null
+++ b/services/core/java/com/android/server/hdmi/HdmiEarcLocalDevice.java
@@ -0,0 +1,61 @@
+/*
+ * 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.hdmi;
+
+import android.hardware.hdmi.HdmiDeviceInfo;
+import android.util.IndentingPrintWriter;
+
+import com.android.internal.annotations.GuardedBy;
+
+/**
+ * Class that models a local eARC device hosted in this system.
+ * The class contains methods that are common between eARC TX and eARC RX devices.
+ */
+abstract class HdmiEarcLocalDevice extends HdmiLocalDevice {
+ private static final String TAG = "HdmiEarcLocalDevice";
+
+ // The current status of the eARC connection, as reported by the HAL
+ @GuardedBy("mLock")
+ @Constants.EarcStatus
+ protected int mEarcStatus;
+
+ protected HdmiEarcLocalDevice(HdmiControlService service, int deviceType) {
+ super(service, deviceType);
+ }
+
+ // Factory method that returns HdmiCecLocalDevice of corresponding type.
+ static HdmiEarcLocalDevice create(HdmiControlService service, int deviceType) {
+ switch (deviceType) {
+ case HdmiDeviceInfo.DEVICE_TV:
+ return new HdmiEarcLocalDeviceTx(service);
+ default:
+ return null;
+ }
+ }
+
+ protected abstract void handleEarcStateChange(@Constants.EarcStatus int status);
+
+ protected abstract void handleEarcCapabilitiesReported(byte[] rawCapabilities);
+
+ protected void disableDevice() {
+ }
+
+ /** Dump internal status of HdmiEarcLocalDevice object */
+ protected void dump(final IndentingPrintWriter pw) {
+ // Should be overridden in the more specific classes
+ }
+}
diff --git a/services/core/java/com/android/server/hdmi/HdmiEarcLocalDeviceTx.java b/services/core/java/com/android/server/hdmi/HdmiEarcLocalDeviceTx.java
new file mode 100644
index 0000000..abb8439
--- /dev/null
+++ b/services/core/java/com/android/server/hdmi/HdmiEarcLocalDeviceTx.java
@@ -0,0 +1,224 @@
+/*
+ * 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.hdmi;
+
+import static com.android.server.hdmi.Constants.HDMI_EARC_STATUS_ARC_PENDING;
+import static com.android.server.hdmi.Constants.HDMI_EARC_STATUS_EARC_CONNECTED;
+import static com.android.server.hdmi.Constants.HDMI_EARC_STATUS_EARC_PENDING;
+import static com.android.server.hdmi.Constants.HDMI_EARC_STATUS_IDLE;
+
+import android.hardware.hdmi.HdmiDeviceInfo;
+import android.media.AudioDescriptor;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioDeviceInfo;
+import android.media.AudioProfile;
+import android.os.Handler;
+import android.util.IndentingPrintWriter;
+import android.util.Slog;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Represents a local eARC device of type TX residing in the Android system.
+ * Only TV panel devices can have a local eARC TX device.
+ */
+public class HdmiEarcLocalDeviceTx extends HdmiEarcLocalDevice {
+ private static final String TAG = "HdmiEarcLocalDeviceTx";
+
+ // How long to wait for the audio system to report its capabilities after eARC was connected
+ static final long REPORT_CAPS_MAX_DELAY_MS = 2_000;
+
+ // eARC Capability Data Structure parameters
+ private static final int EARC_CAPS_PAYLOAD_LENGTH = 0x02;
+ private static final int EARC_CAPS_DATA_START = 0x03;
+
+ // Table 55 CTA Data Block Tag Codes
+ private static final int TAGCODE_AUDIO_DATA_BLOCK = 0x01; // Includes one or more Short Audio
+ // Descriptors
+ private static final int TAGCODE_SADB_DATA_BLOCK = 0x04; // Speaker Allocation Data Block
+ private static final int TAGCODE_USE_EXTENDED_TAG = 0x07; // Use Extended Tag
+
+ // Table 56 Extended Tag Format (2nd byte of Data Block)
+ private static final int EXTENDED_TAGCODE_VSADB = 0x11; // Vendor-Specific Audio Data Block
+
+ // eARC capability mask and shift
+ private static final int EARC_CAPS_TAGCODE_MASK = 0xE0;
+ private static final int EARC_CAPS_TAGCODE_SHIFT = 0x05;
+ private static final int EARC_CAPS_LENGTH_MASK = 0x1F;
+
+ // Handler and runnable for waiting for the audio system to report its capabilities after eARC
+ // was connected
+ private Handler mReportCapsHandler;
+ private ReportCapsRunnable mReportCapsRunnable;
+
+ HdmiEarcLocalDeviceTx(HdmiControlService service) {
+ super(service, HdmiDeviceInfo.DEVICE_TV);
+
+ synchronized (mLock) {
+ mEarcStatus = HDMI_EARC_STATUS_EARC_PENDING;
+ }
+ mReportCapsHandler = new Handler(service.getServiceLooper());
+ mReportCapsRunnable = new ReportCapsRunnable();
+ }
+
+ protected void handleEarcStateChange(@Constants.EarcStatus int status) {
+ int oldEarcStatus;
+ synchronized (mLock) {
+ HdmiLogger.debug(TAG, "eARC state change [old:%b new %b]", mEarcStatus,
+ status);
+ oldEarcStatus = mEarcStatus;
+ mEarcStatus = status;
+ }
+
+ mReportCapsHandler.removeCallbacksAndMessages(null);
+ if (status == HDMI_EARC_STATUS_IDLE) {
+ notifyEarcStatusToAudioService(false, new ArrayList<>());
+ mService.startArcAction(false, null);
+ } else if (status == HDMI_EARC_STATUS_ARC_PENDING) {
+ notifyEarcStatusToAudioService(false, new ArrayList<>());
+ mService.startArcAction(true, null);
+ } else if (status == HDMI_EARC_STATUS_EARC_PENDING
+ && oldEarcStatus == HDMI_EARC_STATUS_ARC_PENDING) {
+ mService.startArcAction(false, null);
+ } else if (status == HDMI_EARC_STATUS_EARC_CONNECTED) {
+ if (oldEarcStatus == HDMI_EARC_STATUS_ARC_PENDING) {
+ mService.startArcAction(false, null);
+ }
+ mReportCapsHandler.postDelayed(mReportCapsRunnable, REPORT_CAPS_MAX_DELAY_MS);
+ }
+ }
+
+ protected void handleEarcCapabilitiesReported(byte[] rawCapabilities) {
+ synchronized (mLock) {
+ if (mEarcStatus == HDMI_EARC_STATUS_EARC_CONNECTED
+ && mReportCapsHandler.hasCallbacks(mReportCapsRunnable)) {
+ mReportCapsHandler.removeCallbacksAndMessages(null);
+ List<AudioDescriptor> audioDescriptors = parseCapabilities(rawCapabilities);
+ notifyEarcStatusToAudioService(true, audioDescriptors);
+ }
+ }
+ }
+
+ private void notifyEarcStatusToAudioService(
+ boolean enabled, List<AudioDescriptor> audioDescriptors) {
+ AudioDeviceAttributes attributes = new AudioDeviceAttributes(
+ AudioDeviceAttributes.ROLE_OUTPUT, AudioDeviceInfo.TYPE_HDMI_EARC, "", "",
+ new ArrayList<AudioProfile>(), audioDescriptors);
+ mService.getAudioManager().setWiredDeviceConnectionState(attributes, enabled ? 1 : 0);
+ }
+
+ /**
+ * Runnable for waiting for a certain amount of time for the audio system to report its
+ * capabilities after eARC was connected. If the audio system doesn´t report its capabilities in
+ * this time, we inform AudioService about the connection state only, without any specified
+ * capabilities.
+ */
+ private class ReportCapsRunnable implements Runnable {
+ @Override
+ public void run() {
+ synchronized (mLock) {
+ if (mEarcStatus == HDMI_EARC_STATUS_EARC_CONNECTED) {
+ notifyEarcStatusToAudioService(true, new ArrayList<>());
+ }
+ }
+ }
+ }
+
+ private List<AudioDescriptor> parseCapabilities(byte[] rawCapabilities) {
+ List<AudioDescriptor> audioDescriptors = new ArrayList<>();
+ if (rawCapabilities.length < EARC_CAPS_DATA_START + 1) {
+ Slog.i(TAG, "Raw eARC capabilities array doesn´t contain any blocks.");
+ return audioDescriptors;
+ }
+ int earcCapsSize = rawCapabilities[EARC_CAPS_PAYLOAD_LENGTH];
+ if (rawCapabilities.length < earcCapsSize) {
+ Slog.i(TAG, "Raw eARC capabilities array is shorter than the reported payload length.");
+ return audioDescriptors;
+ }
+ int firstByteOfBlock = EARC_CAPS_DATA_START;
+ while (firstByteOfBlock < earcCapsSize) {
+ // Tag Code: Bit 5-7
+ int tagCode =
+ (rawCapabilities[firstByteOfBlock] & EARC_CAPS_TAGCODE_MASK)
+ >> EARC_CAPS_TAGCODE_SHIFT;
+ // Length: Bit 0-4
+ int length = rawCapabilities[firstByteOfBlock] & EARC_CAPS_LENGTH_MASK;
+ if (length == 0) {
+ // End Marker of eARC capability.
+ break;
+ }
+ AudioDescriptor descriptor;
+ switch (tagCode) {
+ case TAGCODE_AUDIO_DATA_BLOCK:
+ int earcSadLen = length;
+ if (length % 3 != 0) {
+ Slog.e(TAG, "Invalid length of SAD block: expected a factor of 3 but got "
+ + length % 3);
+ break;
+ }
+ byte[] earcSad = new byte[earcSadLen];
+ System.arraycopy(rawCapabilities, firstByteOfBlock + 1, earcSad, 0, earcSadLen);
+ for (int i = 0; i < earcSadLen; i += 3) {
+ descriptor = new AudioDescriptor(
+ AudioDescriptor.STANDARD_EDID,
+ AudioProfile.AUDIO_ENCAPSULATION_TYPE_NONE,
+ Arrays.copyOfRange(earcSad, i, i + 3));
+ audioDescriptors.add(descriptor);
+ }
+ break;
+ case TAGCODE_SADB_DATA_BLOCK:
+ //Include Tag code size
+ int earcSadbLen = length + 1;
+ byte[] earcSadb = new byte[earcSadbLen];
+ System.arraycopy(rawCapabilities, firstByteOfBlock, earcSadb, 0, earcSadbLen);
+ descriptor = new AudioDescriptor(
+ AudioDescriptor.STANDARD_SADB,
+ AudioProfile.AUDIO_ENCAPSULATION_TYPE_NONE,
+ earcSadb);
+ audioDescriptors.add(descriptor);
+ break;
+ case TAGCODE_USE_EXTENDED_TAG:
+ if (rawCapabilities[firstByteOfBlock + 1] == EXTENDED_TAGCODE_VSADB) {
+ int earcVsadbLen = length + 1; //Include Tag code size
+ byte[] earcVsadb = new byte[earcVsadbLen];
+ System.arraycopy(rawCapabilities, firstByteOfBlock, earcVsadb, 0,
+ earcVsadbLen);
+ descriptor = new AudioDescriptor(
+ AudioDescriptor.STANDARD_VSADB,
+ AudioProfile.AUDIO_ENCAPSULATION_TYPE_NONE,
+ earcVsadb);
+ audioDescriptors.add(descriptor);
+ }
+ break;
+ default:
+ Slog.w(TAG, "This tagcode was not handled: " + tagCode);
+ break;
+ }
+ firstByteOfBlock += (length + 1);
+ }
+ return audioDescriptors;
+ }
+
+ /** Dump internal status of HdmiEarcLocalDeviceTx object */
+ protected void dump(final IndentingPrintWriter pw) {
+ synchronized (mLock) {
+ pw.println("TX, mEarcStatus: " + mEarcStatus);
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/hdmi/HdmiLocalDevice.java b/services/core/java/com/android/server/hdmi/HdmiLocalDevice.java
new file mode 100644
index 0000000..f661e40
--- /dev/null
+++ b/services/core/java/com/android/server/hdmi/HdmiLocalDevice.java
@@ -0,0 +1,37 @@
+/*
+ * 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.hdmi;
+
+/**
+ * Class that models an HDMI device hosted in this system.
+ * Can be used to share methods between CEC and eARC local devices.
+ * Currently just a placeholder.
+ */
+abstract class HdmiLocalDevice {
+ private static final String TAG = "HdmiLocalDevice";
+
+ protected final HdmiControlService mService;
+ protected final int mDeviceType;
+
+ protected final Object mLock;
+
+ protected HdmiLocalDevice(HdmiControlService service, int deviceType) {
+ mService = service;
+ mDeviceType = deviceType;
+ mLock = service.getServiceLock();
+ }
+}
diff --git a/services/core/java/com/android/server/hdmi/RequestArcAction.java b/services/core/java/com/android/server/hdmi/RequestArcAction.java
index 3d9a290..54c8c00 100644
--- a/services/core/java/com/android/server/hdmi/RequestArcAction.java
+++ b/services/core/java/com/android/server/hdmi/RequestArcAction.java
@@ -16,7 +16,9 @@
package com.android.server.hdmi;
+import android.hardware.hdmi.HdmiControlManager;
import android.hardware.hdmi.HdmiDeviceInfo;
+import android.hardware.hdmi.IHdmiControlCallback;
/**
* Base feature action class for <Request ARC Initiation>/<Request ARC Termination>.
@@ -35,41 +37,19 @@
*
* @param source {@link HdmiCecLocalDevice} instance
* @param avrAddress address of AV receiver. It should be AUDIO_SYSTEM type
+ * @param callback callback to inform about the status of the action
* @throws IllegalArgumentException if device type of sourceAddress and avrAddress
* is invalid
*/
- RequestArcAction(HdmiCecLocalDevice source, int avrAddress) {
- super(source);
+ RequestArcAction(HdmiCecLocalDevice source, int avrAddress, IHdmiControlCallback callback) {
+ super(source, callback);
HdmiUtils.verifyAddressType(getSourceAddress(), HdmiDeviceInfo.DEVICE_TV);
HdmiUtils.verifyAddressType(avrAddress, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
mAvrAddress = avrAddress;
}
- @Override
- boolean processCommand(HdmiCecMessage cmd) {
- if (mState != STATE_WATING_FOR_REQUEST_ARC_REQUEST_RESPONSE
- || !HdmiUtils.checkCommandSource(cmd, mAvrAddress, TAG)) {
- return false;
- }
- int opcode = cmd.getOpcode();
- switch (opcode) {
- // Handles only <Feature Abort> here and, both <Initiate ARC> and <Terminate ARC>
- // are handled in HdmiControlService itself because both can be
- // received without <Request ARC Initiation> or <Request ARC Termination>.
- case Constants.MESSAGE_FEATURE_ABORT:
- int originalOpcode = cmd.getParams()[0] & 0xFF;
- if (originalOpcode == Constants.MESSAGE_REQUEST_ARC_TERMINATION) {
- disableArcTransmission();
- finish();
- return true;
- } else if (originalOpcode == Constants.MESSAGE_REQUEST_ARC_INITIATION) {
- tv().disableArc();
- finish();
- return true;
- }
- return false;
- }
- return false;
+ RequestArcAction(HdmiCecLocalDevice source, int avrAddress) {
+ this(source, avrAddress, null);
}
protected final void disableArcTransmission() {
@@ -86,6 +66,6 @@
}
HdmiLogger.debug("[T] RequestArcAction.");
disableArcTransmission();
- finish();
+ finishWithCallback(HdmiControlManager.RESULT_TIMEOUT);
}
}
diff --git a/services/core/java/com/android/server/hdmi/RequestArcInitiationAction.java b/services/core/java/com/android/server/hdmi/RequestArcInitiationAction.java
index 3b7f1dd..db21a33 100644
--- a/services/core/java/com/android/server/hdmi/RequestArcInitiationAction.java
+++ b/services/core/java/com/android/server/hdmi/RequestArcInitiationAction.java
@@ -16,6 +16,8 @@
package com.android.server.hdmi;
+import android.hardware.hdmi.HdmiControlManager;
+import android.hardware.hdmi.IHdmiControlCallback;
import android.hardware.tv.cec.V1_0.SendMessageResult;
/**
@@ -35,6 +37,16 @@
super(source, avrAddress);
}
+ /**
+ * @Constructor
+ *
+ * For more details look at {@link RequestArcAction#RequestArcAction}.
+ */
+ RequestArcInitiationAction(HdmiCecLocalDevice source, int avrAddress,
+ IHdmiControlCallback callback) {
+ super(source, avrAddress, callback);
+ }
+
@Override
boolean start() {
// Seq #38
@@ -49,10 +61,34 @@
if (error != SendMessageResult.SUCCESS) {
// Turn off ARC status if <Request ARC Initiation> fails.
tv().disableArc();
- finish();
+ finishWithCallback(HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
}
}
});
return true;
}
+
+ @Override
+ boolean processCommand(HdmiCecMessage cmd) {
+ if (mState != STATE_WATING_FOR_REQUEST_ARC_REQUEST_RESPONSE
+ || !HdmiUtils.checkCommandSource(cmd, mAvrAddress, TAG)) {
+ return false;
+ }
+ int opcode = cmd.getOpcode();
+ switch (opcode) {
+ case Constants.MESSAGE_FEATURE_ABORT:
+ int originalOpcode = cmd.getParams()[0] & 0xFF;
+ if (originalOpcode == Constants.MESSAGE_REQUEST_ARC_INITIATION) {
+ tv().disableArc();
+ finishWithCallback(HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
+ return true;
+ }
+ return false;
+ case Constants.MESSAGE_INITIATE_ARC:
+ finishWithCallback(HdmiControlManager.RESULT_SUCCESS);
+ // This message still needs to be handled in HdmiCecLocalDeviceTv as well.
+ return false;
+ }
+ return false;
+ }
}
diff --git a/services/core/java/com/android/server/hdmi/RequestArcTerminationAction.java b/services/core/java/com/android/server/hdmi/RequestArcTerminationAction.java
index 8b5a2931..85128b6 100644
--- a/services/core/java/com/android/server/hdmi/RequestArcTerminationAction.java
+++ b/services/core/java/com/android/server/hdmi/RequestArcTerminationAction.java
@@ -16,6 +16,8 @@
package com.android.server.hdmi;
+import android.hardware.hdmi.HdmiControlManager;
+import android.hardware.hdmi.IHdmiControlCallback;
import android.hardware.tv.cec.V1_0.SendMessageResult;
/**
@@ -35,6 +37,16 @@
super(source, avrAddress);
}
+ /**
+ * @Constructor
+ *
+ * @see RequestArcAction#RequestArcAction
+ */
+ RequestArcTerminationAction(HdmiCecLocalDevice source, int avrAddress,
+ IHdmiControlCallback callback) {
+ super(source, avrAddress, callback);
+ }
+
@Override
boolean start() {
mState = STATE_WATING_FOR_REQUEST_ARC_REQUEST_RESPONSE;
@@ -49,10 +61,34 @@
// If failed to send <Request ARC Termination>, start "Disabled" ARC
// transmission action.
disableArcTransmission();
- finish();
+ finishWithCallback(HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
}
}
});
return true;
}
+
+ @Override
+ boolean processCommand(HdmiCecMessage cmd) {
+ if (mState != STATE_WATING_FOR_REQUEST_ARC_REQUEST_RESPONSE
+ || !HdmiUtils.checkCommandSource(cmd, mAvrAddress, TAG)) {
+ return false;
+ }
+ int opcode = cmd.getOpcode();
+ switch (opcode) {
+ case Constants.MESSAGE_FEATURE_ABORT:
+ int originalOpcode = cmd.getParams()[0] & 0xFF;
+ if (originalOpcode == Constants.MESSAGE_REQUEST_ARC_TERMINATION) {
+ disableArcTransmission();
+ finishWithCallback(HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
+ return true;
+ }
+ return false;
+ case Constants.MESSAGE_TERMINATE_ARC:
+ finishWithCallback(HdmiControlManager.RESULT_SUCCESS);
+ // This message still needs to be handled in HdmiCecLocalDeviceTv as well.
+ return false;
+ }
+ return false;
+ }
}
diff --git a/services/core/java/com/android/server/infra/AbstractPerUserSystemService.java b/services/core/java/com/android/server/infra/AbstractPerUserSystemService.java
index b8f1db4..ddb19f0 100644
--- a/services/core/java/com/android/server/infra/AbstractPerUserSystemService.java
+++ b/services/core/java/com/android/server/infra/AbstractPerUserSystemService.java
@@ -182,8 +182,12 @@
final String componentName = getComponentNameLocked();
return new ComponentName[] { getServiceComponent(componentName) };
}
+
final String[] componentNames = mMaster.mServiceNameResolver.getServiceNameList(
mUserId);
+ if (componentNames == null) {
+ return null;
+ }
ComponentName[] serviceComponents = new ComponentName[componentNames.length];
for (int i = 0; i < componentNames.length; i++) {
serviceComponents[i] = getServiceComponent(componentNames[i]);
diff --git a/services/core/java/com/android/server/input/KeyboardLayoutManager.java b/services/core/java/com/android/server/input/KeyboardLayoutManager.java
index d76da83..b8eb901 100644
--- a/services/core/java/com/android/server/input/KeyboardLayoutManager.java
+++ b/services/core/java/com/android/server/input/KeyboardLayoutManager.java
@@ -385,8 +385,10 @@
R.styleable.KeyboardLayout_keyboardLayout,
0);
String languageTags = a.getString(
- R.styleable.KeyboardLayout_locale);
+ R.styleable.KeyboardLayout_keyboardLocale);
LocaleList locales = getLocalesFromLanguageTags(languageTags);
+ int layoutType = a.getInt(R.styleable.KeyboardLayout_keyboardLayoutType,
+ 0);
int vid = a.getInt(
R.styleable.KeyboardLayout_vendorId, -1);
int pid = a.getInt(
@@ -403,7 +405,7 @@
if (keyboardName == null || name.equals(keyboardName)) {
KeyboardLayout layout = new KeyboardLayout(
descriptor, label, collection, priority,
- locales, vid, pid);
+ locales, layoutType, vid, pid);
visitor.visitKeyboardLayout(
resources, keyboardLayoutResId, layout);
}
diff --git a/services/core/java/com/android/server/inputmethod/ImeTrackerService.java b/services/core/java/com/android/server/inputmethod/ImeTrackerService.java
new file mode 100644
index 0000000..60167b4
--- /dev/null
+++ b/services/core/java/com/android/server/inputmethod/ImeTrackerService.java
@@ -0,0 +1,338 @@
+/*
+ * 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.inputmethod;
+
+import android.Manifest;
+import android.annotation.EnforcePermission;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.view.inputmethod.ImeTracker;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.inputmethod.InputMethodDebug;
+import com.android.internal.inputmethod.SoftInputShowHideReason;
+import com.android.internal.util.FrameworkStatsLog;
+import com.android.internal.view.IImeTracker;
+
+import java.io.PrintWriter;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayDeque;
+import java.util.Locale;
+import java.util.WeakHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Service for managing and logging {@link ImeTracker.Token} instances.
+ *
+ * @implNote Suppresses {@link GuardedBy} warnings, as linter reports that {@link #mHistory}
+ * interactions are guarded by {@code this} instead of {@code ImeTrackerService.this}, which should
+ * be identical.
+ *
+ * @hide
+ */
+@SuppressWarnings("GuardedBy")
+public final class ImeTrackerService extends IImeTracker.Stub {
+
+ static final String TAG = "ImeTrackerService";
+
+ /** The threshold in milliseconds after which a history entry is considered timed out. */
+ private static final long TIMEOUT_MS = 10_000;
+
+ /** Handler for registering timeouts for live entries. */
+ private final Handler mHandler =
+ new Handler(Looper.myLooper(), null /* callback */, true /* async */);
+
+ /** Singleton instance of the History. */
+ @GuardedBy("ImeTrackerService.this")
+ private final History mHistory = new History();
+
+ @NonNull
+ @Override
+ public synchronized IBinder onRequestShow(int uid, @ImeTracker.Origin int origin,
+ @SoftInputShowHideReason int reason) {
+ final IBinder binder = new Binder();
+ final History.Entry entry = new History.Entry(uid, ImeTracker.TYPE_SHOW,
+ ImeTracker.STATUS_RUN, origin, reason);
+ mHistory.addEntry(binder, entry);
+
+ // Register a delayed task to handle the case where the new entry times out.
+ mHandler.postDelayed(() -> {
+ synchronized (ImeTrackerService.this) {
+ mHistory.setFinished(binder, ImeTracker.STATUS_TIMEOUT, ImeTracker.PHASE_NOT_SET);
+ }
+ }, TIMEOUT_MS);
+
+ return binder;
+ }
+
+ @NonNull
+ @Override
+ public synchronized IBinder onRequestHide(int uid, @ImeTracker.Origin int origin,
+ @SoftInputShowHideReason int reason) {
+ final IBinder binder = new Binder();
+ final History.Entry entry = new History.Entry(uid, ImeTracker.TYPE_HIDE,
+ ImeTracker.STATUS_RUN, origin, reason);
+ mHistory.addEntry(binder, entry);
+
+ // Register a delayed task to handle the case where the new entry times out.
+ mHandler.postDelayed(() -> {
+ synchronized (ImeTrackerService.this) {
+ mHistory.setFinished(binder, ImeTracker.STATUS_TIMEOUT, ImeTracker.PHASE_NOT_SET);
+ }
+ }, TIMEOUT_MS);
+
+ return binder;
+ }
+
+ @Override
+ public synchronized void onProgress(@NonNull IBinder statsToken, @ImeTracker.Phase int phase) {
+ final History.Entry entry = mHistory.getEntry(statsToken);
+ if (entry == null) return;
+
+ entry.mPhase = phase;
+ }
+
+ @Override
+ public synchronized void onFailed(@NonNull IBinder statsToken, @ImeTracker.Phase int phase) {
+ mHistory.setFinished(statsToken, ImeTracker.STATUS_FAIL, phase);
+ }
+
+ @Override
+ public synchronized void onCancelled(@NonNull IBinder statsToken, @ImeTracker.Phase int phase) {
+ mHistory.setFinished(statsToken, ImeTracker.STATUS_CANCEL, phase);
+ }
+
+ @Override
+ public synchronized void onShown(@NonNull IBinder statsToken) {
+ mHistory.setFinished(statsToken, ImeTracker.STATUS_SUCCESS, ImeTracker.PHASE_NOT_SET);
+ }
+
+ @Override
+ public synchronized void onHidden(@NonNull IBinder statsToken) {
+ mHistory.setFinished(statsToken, ImeTracker.STATUS_SUCCESS, ImeTracker.PHASE_NOT_SET);
+ }
+
+ /**
+ * Updates the IME request tracking token with new information available in IMMS.
+ *
+ * @param statsToken the token corresponding to the current IME request.
+ * @param requestWindowName the name of the window that created the IME request.
+ */
+ public synchronized void onImmsUpdate(@NonNull IBinder statsToken,
+ @NonNull String requestWindowName) {
+ final History.Entry entry = mHistory.getEntry(statsToken);
+ if (entry == null) return;
+
+ entry.mRequestWindowName = requestWindowName;
+ }
+
+ /** Dumps the contents of the history. */
+ public synchronized void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
+ mHistory.dump(pw, prefix);
+ }
+
+ @EnforcePermission(Manifest.permission.TEST_INPUT_METHOD)
+ @Override
+ public synchronized boolean hasPendingImeVisibilityRequests() {
+ super.hasPendingImeVisibilityRequests_enforcePermission();
+
+ return !mHistory.mLiveEntries.isEmpty();
+ }
+
+ /**
+ * A circular buffer storing the most recent few {@link ImeTracker.Token} entries information.
+ */
+ private static final class History {
+
+ /** The circular buffer's capacity. */
+ private static final int CAPACITY = 100;
+
+ /** Backing store for the circular buffer. */
+ @GuardedBy("ImeTrackerService.this")
+ private final ArrayDeque<Entry> mEntries = new ArrayDeque<>(CAPACITY);
+
+ /** Backing store for the live entries (i.e. entries that are not finished yet). */
+ @GuardedBy("ImeTrackerService.this")
+ private final WeakHashMap<IBinder, Entry> mLiveEntries = new WeakHashMap<>();
+
+ /** Latest entry sequence number. */
+ private static final AtomicInteger sSequenceNumber = new AtomicInteger(0);
+
+ /** Adds a live entry. */
+ @GuardedBy("ImeTrackerService.this")
+ private void addEntry(@NonNull IBinder statsToken, @NonNull Entry entry) {
+ mLiveEntries.put(statsToken, entry);
+ }
+
+ /** Gets the entry corresponding to the given IME tracking token, if it exists. */
+ @Nullable
+ @GuardedBy("ImeTrackerService.this")
+ private Entry getEntry(@NonNull IBinder statsToken) {
+ return mLiveEntries.get(statsToken);
+ }
+
+ /**
+ * Sets the live entry corresponding to the tracking token, if it exists, as finished,
+ * and uploads the data for metrics.
+ *
+ * @param statsToken the token corresponding to the current IME request.
+ * @param status the finish status of the IME request.
+ * @param phase the phase the IME request finished at, if it exists
+ * (or {@link ImeTracker#PHASE_NOT_SET} otherwise).
+ */
+ @GuardedBy("ImeTrackerService.this")
+ private void setFinished(@NonNull IBinder statsToken, @ImeTracker.Status int status,
+ @ImeTracker.Phase int phase) {
+ final Entry entry = mLiveEntries.remove(statsToken);
+ if (entry == null) return;
+
+ entry.mDuration = System.currentTimeMillis() - entry.mStartTime;
+ entry.mStatus = status;
+
+ if (phase != ImeTracker.PHASE_NOT_SET) {
+ entry.mPhase = phase;
+ }
+
+ // Remove excess entries overflowing capacity (plus one for the new entry).
+ while (mEntries.size() >= CAPACITY) {
+ mEntries.remove();
+ }
+
+ mEntries.offer(entry);
+
+ // Log newly finished entry.
+ FrameworkStatsLog.write(FrameworkStatsLog.IME_REQUEST_FINISHED, entry.mUid,
+ entry.mDuration, entry.mType, entry.mStatus, entry.mReason,
+ entry.mOrigin, entry.mPhase);
+ }
+
+ /** Dumps the contents of the circular buffer. */
+ @GuardedBy("ImeTrackerService.this")
+ private void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
+ final DateTimeFormatter formatter =
+ DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS", Locale.US)
+ .withZone(ZoneId.systemDefault());
+
+ pw.print(prefix);
+ pw.println("ImeTrackerService#History.mLiveEntries:");
+
+ for (final Entry entry: mLiveEntries.values()) {
+ dumpEntry(entry, pw, prefix, formatter);
+ }
+
+ pw.print(prefix);
+ pw.println("ImeTrackerService#History.mEntries:");
+
+ for (final Entry entry: mEntries) {
+ dumpEntry(entry, pw, prefix, formatter);
+ }
+ }
+
+ @GuardedBy("ImeTrackerService.this")
+ private void dumpEntry(@NonNull Entry entry, @NonNull PrintWriter pw,
+ @NonNull String prefix, @NonNull DateTimeFormatter formatter) {
+ pw.print(prefix);
+ pw.println("ImeTrackerService#History #" + entry.mSequenceNumber + ":");
+
+ pw.print(prefix);
+ pw.println(" startTime=" + formatter.format(Instant.ofEpochMilli(entry.mStartTime)));
+
+ pw.print(prefix);
+ pw.println(" duration=" + entry.mDuration + "ms");
+
+ pw.print(prefix);
+ pw.print(" type=" + ImeTracker.Debug.typeToString(entry.mType));
+
+ pw.print(prefix);
+ pw.print(" status=" + ImeTracker.Debug.statusToString(entry.mStatus));
+
+ pw.print(prefix);
+ pw.print(" origin="
+ + ImeTracker.Debug.originToString(entry.mOrigin));
+
+ pw.print(prefix);
+ pw.print(" reason="
+ + InputMethodDebug.softInputDisplayReasonToString(entry.mReason));
+
+ pw.print(prefix);
+ pw.print(" phase="
+ + ImeTracker.Debug.phaseToString(entry.mPhase));
+
+ pw.print(prefix);
+ pw.print(" requestWindowName=" + entry.mRequestWindowName);
+ }
+
+ /** A history entry. */
+ private static final class Entry {
+
+ /** The entry's sequence number in the history. */
+ private final int mSequenceNumber = sSequenceNumber.getAndIncrement();
+
+ /** Uid of the client that requested the IME. */
+ private final int mUid;
+
+ /** Clock time in milliseconds when the IME request was created. */
+ private final long mStartTime = System.currentTimeMillis();
+
+ /** Duration in milliseconds of the IME request from start to end. */
+ private long mDuration = 0;
+
+ /** Type of the IME request. */
+ @ImeTracker.Type
+ private final int mType;
+
+ /** Status of the IME request. */
+ @ImeTracker.Status
+ private int mStatus;
+
+ /** Origin of the IME request. */
+ @ImeTracker.Origin
+ private final int mOrigin;
+
+ /** Reason for creating the IME request. */
+ @SoftInputShowHideReason
+ private final int mReason;
+
+ /** Latest phase of the IME request. */
+ @ImeTracker.Phase
+ private int mPhase = ImeTracker.PHASE_NOT_SET;
+
+ /**
+ * Name of the window that created the IME request.
+ *
+ * Note: This is later set through {@link #onImmsUpdate(IBinder, String)}.
+ */
+ @NonNull
+ private String mRequestWindowName = "not set";
+
+ private Entry(int uid, @ImeTracker.Type int type, @ImeTracker.Status int status,
+ @ImeTracker.Origin int origin, @SoftInputShowHideReason int reason) {
+ mUid = uid;
+ mType = type;
+ mStatus = status;
+ mOrigin = origin;
+ mReason = reason;
+ }
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 5840acf..c15b538 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -176,6 +176,7 @@
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.ConcurrentUtils;
import com.android.internal.util.DumpUtils;
+import com.android.internal.view.IImeTracker;
import com.android.internal.view.IInputMethodManager;
import com.android.server.AccessibilityManagerInternal;
import com.android.server.EventLogTags;
@@ -198,11 +199,12 @@
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.security.InvalidParameterException;
-import java.text.SimpleDateFormat;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
-import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
@@ -919,8 +921,9 @@
}
void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
- final SimpleDateFormat dataFormat =
- new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.US);
+ final DateTimeFormatter formatter =
+ DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS", Locale.US)
+ .withZone(ZoneId.systemDefault());
for (int i = 0; i < mEntries.length; ++i) {
final Entry entry = mEntries[(i + mNextIndex) % mEntries.length];
@@ -931,7 +934,7 @@
pw.println("SoftInputShowHideHistory #" + entry.mSequenceNumber + ":");
pw.print(prefix);
- pw.println(" time=" + dataFormat.format(new Date(entry.mWallTime))
+ pw.println(" time=" + formatter.format(Instant.ofEpochMilli(entry.mWallTime))
+ " (timestamp=" + entry.mTimestamp + ")");
pw.print(prefix);
@@ -999,7 +1002,7 @@
private static final int ENTRY_SIZE_FOR_HIGH_RAM_DEVICE = 32;
/**
- * Entry size for non low-RAM devices.
+ * Entry size for low-RAM devices.
*
* <p>TODO: Consider to follow what other system services have been doing to manage
* constants (e.g. {@link android.provider.Settings.Global#ACTIVITY_MANAGER_CONSTANTS}).</p>
@@ -1015,7 +1018,7 @@
}
/**
- * Backing store for the ring bugger.
+ * Backing store for the ring buffer.
*/
private final Entry[] mEntries = new Entry[getEntrySize()];
@@ -1095,8 +1098,9 @@
}
void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
- final SimpleDateFormat dataFormat =
- new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.US);
+ final DateTimeFormatter formatter =
+ DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS", Locale.US)
+ .withZone(ZoneId.systemDefault());
for (int i = 0; i < mEntries.length; ++i) {
final Entry entry = mEntries[(i + mNextIndex) % mEntries.length];
@@ -1107,7 +1111,7 @@
pw.println("StartInput #" + entry.mSequenceNumber + ":");
pw.print(prefix);
- pw.println(" time=" + dataFormat.format(new Date(entry.mWallTime))
+ pw.println(" time=" + formatter.format(Instant.ofEpochMilli(entry.mWallTime))
+ " (timestamp=" + entry.mTimestamp + ")"
+ " reason="
+ InputMethodDebug.startInputReasonToString(entry.mStartInputReason)
@@ -1149,6 +1153,9 @@
private final SoftInputShowHideHistory mSoftInputShowHideHistory =
new SoftInputShowHideHistory();
+ @NonNull
+ private final ImeTrackerService mImeTrackerService = new ImeTrackerService();
+
class SettingsObserver extends ContentObserver {
int mUserId;
boolean mRegistered = false;
@@ -3405,13 +3412,11 @@
ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
// Create statsToken is none exists.
if (statsToken == null) {
- String packageName = null;
- if (mCurEditorInfo != null) {
- packageName = mCurEditorInfo.packageName;
- }
- statsToken = new ImeTracker.Token(packageName);
- ImeTracker.get().onRequestShow(statsToken, ImeTracker.ORIGIN_SERVER_START_INPUT,
- reason);
+ // TODO(b/261565259): to avoid using null, add package name in ClientState
+ final String packageName = (mCurEditorInfo != null) ? mCurEditorInfo.packageName : null;
+ final int uid = mCurClient != null ? mCurClient.mUid : -1;
+ statsToken = ImeTracker.get().onRequestShow(packageName, uid,
+ ImeTracker.ORIGIN_SERVER_START_INPUT, reason);
}
mShowRequested = true;
@@ -3461,7 +3466,7 @@
InputMethodDebug.softInputDisplayReasonToString(reason),
InputMethodDebug.softInputModeToString(mCurFocusedWindowSoftInputMode));
}
- onShowHideSoftInputRequested(true /* show */, windowToken, reason);
+ onShowHideSoftInputRequested(true /* show */, windowToken, reason, statsToken);
}
mInputShown = true;
return true;
@@ -3508,12 +3513,18 @@
int flags, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
// Create statsToken is none exists.
if (statsToken == null) {
- String packageName = null;
- if (mCurEditorInfo != null) {
- packageName = mCurEditorInfo.packageName;
+ // TODO(b/261565259): to avoid using null, add package name in ClientState
+ final String packageName = (mCurEditorInfo != null) ? mCurEditorInfo.packageName : null;
+ final int uid;
+ if (mCurClient != null) {
+ uid = mCurClient.mUid;
+ } else if (mCurFocusedWindowClient != null) {
+ uid = mCurFocusedWindowClient.mUid;
+ } else {
+ uid = -1;
}
- statsToken = new ImeTracker.Token(packageName);
- ImeTracker.get().onRequestHide(statsToken, ImeTracker.ORIGIN_SERVER_HIDE_INPUT, reason);
+ statsToken = ImeTracker.get().onRequestHide(packageName, uid,
+ ImeTracker.ORIGIN_SERVER_HIDE_INPUT, reason);
}
if ((flags & InputMethodManager.HIDE_IMPLICIT_ONLY) != 0
@@ -3565,7 +3576,7 @@
InputMethodDebug.softInputDisplayReasonToString(reason),
InputMethodDebug.softInputModeToString(mCurFocusedWindowSoftInputMode));
}
- onShowHideSoftInputRequested(false /* show */, windowToken, reason);
+ onShowHideSoftInputRequested(false /* show */, windowToken, reason, statsToken);
}
res = true;
} else {
@@ -4781,6 +4792,7 @@
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.applyImeVisibility");
synchronized (ImfLock.class) {
if (!calledWithValidTokenLocked(token)) {
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_APPLY_IME_VISIBILITY);
return;
}
if (!setVisible) {
@@ -4846,7 +4858,7 @@
/** Called right after {@link com.android.internal.inputmethod.IInputMethod#showSoftInput}. */
@GuardedBy("ImfLock.class")
private void onShowHideSoftInputRequested(boolean show, IBinder requestToken,
- @SoftInputShowHideReason int reason) {
+ @SoftInputShowHideReason int reason, @Nullable ImeTracker.Token statsToken) {
final WindowManagerInternal.ImeTargetInfo info =
mWindowManagerInternal.onToggleImeRequested(
show, mCurFocusedWindow, requestToken, mCurTokenDisplayId);
@@ -4855,6 +4867,8 @@
mCurFocusedWindowSoftInputMode, reason, mInFullscreenMode,
info.requestWindowName, info.imeControlTargetName, info.imeLayerTargetName,
info.imeSurfaceParentName));
+
+ mImeTrackerService.onImmsUpdate(statsToken.mBinder, info.requestWindowName);
}
@BinderThread
@@ -5994,6 +6008,9 @@
p.println(" mSoftInputShowHideHistory:");
mSoftInputShowHideHistory.dump(pw, " ");
+
+ p.println(" mImeTrackerService#History:");
+ mImeTrackerService.dump(pw, " ");
}
// Exit here for critical dump, as remaining sections require IPCs to other processes.
@@ -6584,6 +6601,12 @@
return true;
}
+ /** @hide */
+ @Override
+ public IImeTracker getImeTrackerService() {
+ return mImeTrackerService;
+ }
+
private static final class InputMethodPrivilegedOperationsImpl
extends IInputMethodPrivilegedOperations.Stub {
private final InputMethodManagerService mImms;
diff --git a/services/core/java/com/android/server/locksettings/AuthSecretHidlAdapter.java b/services/core/java/com/android/server/locksettings/AuthSecretHidlAdapter.java
new file mode 100644
index 0000000..3b5f340
--- /dev/null
+++ b/services/core/java/com/android/server/locksettings/AuthSecretHidlAdapter.java
@@ -0,0 +1,61 @@
+/*
+ * 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.locksettings;
+
+import android.hardware.authsecret.IAuthSecret;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import java.util.ArrayList;
+
+/**
+ * Adapt the legacy HIDL interface to present the AIDL interface.
+ */
+class AuthSecretHidlAdapter implements IAuthSecret {
+ // private final String TAG = "AuthSecretHidlAdapter";
+ private final android.hardware.authsecret.V1_0.IAuthSecret mImpl;
+
+ AuthSecretHidlAdapter(android.hardware.authsecret.V1_0.IAuthSecret impl) {
+ mImpl = impl;
+ }
+
+ @Override
+ public void setPrimaryUserCredential(byte[] secret) throws RemoteException {
+ final ArrayList<Byte> secretAsArrayList = new ArrayList<>(secret.length);
+ for (int i = 0; i < secret.length; ++i) {
+ secretAsArrayList.add(secret[i]);
+ }
+ mImpl.primaryUserCredential(secretAsArrayList);
+ }
+
+ @Override
+ public int getInterfaceVersion() throws RemoteException {
+ // Supports only V1
+ return 1;
+ }
+
+ @Override
+ public IBinder asBinder() {
+ throw new UnsupportedOperationException("AuthSecretHidlAdapter does not support asBinder");
+ }
+
+ @Override
+ public String getInterfaceHash() throws RemoteException {
+ throw new UnsupportedOperationException(
+ "AuthSecretHidlAdapter does not support getInterfaceHash");
+ }
+}
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index 2253a9a..121b7c8 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -173,7 +173,6 @@
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
-import java.util.Random;
import java.util.Set;
import java.util.StringJoiner;
import java.util.concurrent.CountDownLatch;
@@ -234,7 +233,6 @@
private final SynchronizedStrongAuthTracker mStrongAuthTracker;
private final BiometricDeferredQueue mBiometricDeferredQueue;
private final LongSparseArray<byte[]> mGatekeeperPasswords;
- private final Random mRandom;
private final NotificationManager mNotificationManager;
protected final UserManager mUserManager;
@@ -267,8 +265,7 @@
protected boolean mHasSecureLockScreen;
protected IGateKeeperService mGateKeeperService;
- protected IAuthSecret mAuthSecretServiceAidl;
- protected android.hardware.authsecret.V1_0.IAuthSecret mAuthSecretServiceHidl;
+ protected IAuthSecret mAuthSecretService;
private static final String GSI_RUNNING_PROP = "ro.gsid.image_running";
@@ -349,23 +346,17 @@
}
private LockscreenCredential generateRandomProfilePassword() {
- byte[] randomLockSeed = new byte[] {};
- try {
- randomLockSeed = SecureRandom.getInstance("SHA1PRNG").generateSeed(40);
- char[] newPasswordChars = HexEncoding.encode(randomLockSeed);
- byte[] newPassword = new byte[newPasswordChars.length];
- for (int i = 0; i < newPasswordChars.length; i++) {
- newPassword[i] = (byte) newPasswordChars[i];
- }
- LockscreenCredential credential =
- LockscreenCredential.createManagedPassword(newPassword);
- Arrays.fill(newPasswordChars, '\u0000');
- Arrays.fill(newPassword, (byte) 0);
- Arrays.fill(randomLockSeed, (byte) 0);
- return credential;
- } catch (NoSuchAlgorithmException e) {
- throw new IllegalStateException("Fail to generate profile password", e);
+ byte[] randomLockSeed = SecureRandomUtils.randomBytes(40);
+ char[] newPasswordChars = HexEncoding.encode(randomLockSeed);
+ byte[] newPassword = new byte[newPasswordChars.length];
+ for (int i = 0; i < newPasswordChars.length; i++) {
+ newPassword[i] = (byte) newPasswordChars[i];
}
+ LockscreenCredential credential = LockscreenCredential.createManagedPassword(newPassword);
+ Arrays.fill(newPasswordChars, '\u0000');
+ Arrays.fill(newPassword, (byte) 0);
+ Arrays.fill(randomLockSeed, (byte) 0);
+ return credential;
}
/**
@@ -598,7 +589,6 @@
mStrongAuthTracker = injector.getStrongAuthTracker();
mStrongAuthTracker.register(mStrongAuth);
mGatekeeperPasswords = new LongSparseArray<>();
- mRandom = new SecureRandom();
mSpManager = injector.getSyntheticPasswordManager(mStorage);
mManagedProfilePasswordCache = injector.getManagedProfilePasswordCache(mJavaKeyStore);
@@ -837,16 +827,19 @@
}
private void getAuthSecretHal() {
- mAuthSecretServiceAidl = IAuthSecret.Stub.asInterface(ServiceManager.
- waitForDeclaredService(IAuthSecret.DESCRIPTOR + "/default"));
- if (mAuthSecretServiceAidl == null) {
- Slog.i(TAG, "Device doesn't implement AuthSecret HAL(aidl), try to get hidl version");
-
+ mAuthSecretService =
+ IAuthSecret.Stub.asInterface(
+ ServiceManager.waitForDeclaredService(IAuthSecret.DESCRIPTOR + "/default"));
+ if (mAuthSecretService != null) {
+ Slog.i(TAG, "Device implements AIDL AuthSecret HAL");
+ } else {
try {
- mAuthSecretServiceHidl =
- android.hardware.authsecret.V1_0.IAuthSecret.getService(/* retry */ true);
+ android.hardware.authsecret.V1_0.IAuthSecret authSecretServiceHidl =
+ android.hardware.authsecret.V1_0.IAuthSecret.getService(/* retry */ true);
+ mAuthSecretService = new AuthSecretHidlAdapter(authSecretServiceHidl);
+ Slog.i(TAG, "Device implements HIDL AuthSecret HAL");
} catch (NoSuchElementException e) {
- Slog.i(TAG, "Device doesn't implement AuthSecret HAL(hidl)");
+ Slog.i(TAG, "Device doesn't implement AuthSecret HAL");
} catch (RemoteException e) {
Slog.w(TAG, "Failed to get AuthSecret HAL(hidl)", e);
}
@@ -1243,7 +1236,8 @@
private int getFrpCredentialType() {
PersistentData data = mStorage.readPersistentDataBlock();
- if (data.type != PersistentData.TYPE_SP && data.type != PersistentData.TYPE_SP_WEAVER) {
+ if (data.type != PersistentData.TYPE_SP_GATEKEEPER &&
+ data.type != PersistentData.TYPE_SP_WEAVER) {
return CREDENTIAL_TYPE_NONE;
}
int credentialType = SyntheticPasswordManager.getFrpCredentialType(data.payload);
@@ -1749,14 +1743,9 @@
private String getSalt(int userId) {
long salt = getLong(LockPatternUtils.LOCK_PASSWORD_SALT_KEY, 0, userId);
if (salt == 0) {
- try {
- salt = SecureRandom.getInstance("SHA1PRNG").nextLong();
- setLong(LockPatternUtils.LOCK_PASSWORD_SALT_KEY, salt, userId);
- Slog.v(TAG, "Initialized lock password salt for user: " + userId);
- } catch (NoSuchAlgorithmException e) {
- // Throw an exception rather than storing a password we'll never be able to recover
- throw new IllegalStateException("Couldn't get SecureRandom number", e);
- }
+ salt = SecureRandomUtils.randomLong();
+ setLong(LockPatternUtils.LOCK_PASSWORD_SALT_KEY, salt, userId);
+ Slog.v(TAG, "Initialized lock password salt for user: " + userId);
}
return Long.toHexString(salt);
}
@@ -2577,25 +2566,16 @@
// If the given user is the primary user, pass the auth secret to the HAL. Only the system
// user can be primary. Check for the system user ID before calling getUserInfo(), as other
// users may still be under construction.
+ if (mAuthSecretService == null) {
+ return;
+ }
if (userId == UserHandle.USER_SYSTEM &&
mUserManager.getUserInfo(userId).isPrimary()) {
- final byte[] rawSecret = sp.deriveVendorAuthSecret();
- if (mAuthSecretServiceAidl != null) {
- try {
- mAuthSecretServiceAidl.setPrimaryUserCredential(rawSecret);
- } catch (RemoteException e) {
- Slog.w(TAG, "Failed to pass primary user secret to AuthSecret HAL(aidl)", e);
- }
- } else if (mAuthSecretServiceHidl != null) {
- try {
- final ArrayList<Byte> secret = new ArrayList<>(rawSecret.length);
- for (int i = 0; i < rawSecret.length; ++i) {
- secret.add(rawSecret[i]);
- }
- mAuthSecretServiceHidl.primaryUserCredential(secret);
- } catch (RemoteException e) {
- Slog.w(TAG, "Failed to pass primary user secret to AuthSecret HAL(hidl)", e);
- }
+ final byte[] secret = sp.deriveVendorAuthSecret();
+ try {
+ mAuthSecretService.setPrimaryUserCredential(secret);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Failed to pass primary user secret to AuthSecret HAL", e);
}
}
}
@@ -2650,7 +2630,7 @@
synchronized (mGatekeeperPasswords) {
while (handle == 0L || mGatekeeperPasswords.get(handle) != null) {
- handle = mRandom.nextLong();
+ handle = SecureRandomUtils.randomLong();
}
mGatekeeperPasswords.put(handle, gatekeeperPassword);
}
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsStorage.java b/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
index 473c4b6..2c28af1 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
@@ -587,7 +587,7 @@
static final int VERSION_1_HEADER_SIZE = 1 + 1 + 4 + 4;
public static final int TYPE_NONE = 0;
- public static final int TYPE_SP = 1;
+ public static final int TYPE_SP_GATEKEEPER = 1;
public static final int TYPE_SP_WEAVER = 2;
public static final PersistentData NONE = new PersistentData(TYPE_NONE,
diff --git a/services/core/java/com/android/server/locksettings/SecureRandomUtils.java b/services/core/java/com/android/server/locksettings/SecureRandomUtils.java
new file mode 100644
index 0000000..4ba4dd0
--- /dev/null
+++ b/services/core/java/com/android/server/locksettings/SecureRandomUtils.java
@@ -0,0 +1,36 @@
+/*
+ * 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.locksettings;
+
+import java.security.SecureRandom;
+
+/** Utilities using a static SecureRandom */
+public class SecureRandomUtils {
+ private static final SecureRandom RNG = new SecureRandom();
+
+ /** Use SecureRandom to generate `length` random bytes */
+ public static byte[] randomBytes(int length) {
+ byte[] res = new byte[length];
+ RNG.nextBytes(res);
+ return res;
+ }
+
+ /** Use SecureRandom to generate a random long */
+ public static long randomLong() {
+ return RNG.nextLong();
+ }
+}
diff --git a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
index 67d1c71..cd972dc 100644
--- a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
+++ b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
@@ -59,8 +59,6 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.ByteBuffer;
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
@@ -284,8 +282,10 @@
*/
static SyntheticPassword create() {
SyntheticPassword result = new SyntheticPassword(SYNTHETIC_PASSWORD_VERSION_V3);
- byte[] escrowSplit0 = secureRandom(SYNTHETIC_PASSWORD_SECURITY_STRENGTH);
- byte[] escrowSplit1 = secureRandom(SYNTHETIC_PASSWORD_SECURITY_STRENGTH);
+ byte[] escrowSplit0 =
+ SecureRandomUtils.randomBytes(SYNTHETIC_PASSWORD_SECURITY_STRENGTH);
+ byte[] escrowSplit1 =
+ SecureRandomUtils.randomBytes(SYNTHETIC_PASSWORD_SECURITY_STRENGTH);
result.recreate(escrowSplit0, escrowSplit1);
byte[] encrypteEscrowSplit0 = SyntheticPasswordCrypto.encrypt(result.mSyntheticPassword,
PERSONALIZATION_E0, escrowSplit0);
@@ -347,7 +347,7 @@
result.scryptLogR = PASSWORD_SCRYPT_LOG_R;
result.scryptLogP = PASSWORD_SCRYPT_LOG_P;
result.credentialType = credentialType;
- result.salt = secureRandom(PASSWORD_SALT_LENGTH);
+ result.salt = SecureRandomUtils.randomBytes(PASSWORD_SALT_LENGTH);
return result;
}
@@ -490,7 +490,7 @@
android.hardware.weaver.V1_0.IWeaver hidlWeaver = getWeaverHidlService();
if (hidlWeaver != null) {
Slog.i(TAG, "Using HIDL weaver service");
- return new WeaverHidlWrapper(hidlWeaver);
+ return new WeaverHidlAdapter(hidlWeaver);
}
} catch (RemoteException e) {
Slog.w(TAG, "Failed to get HIDL weaver service.", e);
@@ -552,7 +552,7 @@
throw new IllegalArgumentException("Invalid key size for weaver");
}
if (value == null) {
- value = secureRandom(mWeaverConfig.valueSize);
+ value = SecureRandomUtils.randomBytes(mWeaverConfig.valueSize);
}
try {
mWeaver.write(slot, key, value);
@@ -900,7 +900,7 @@
protectorSecret = transformUnderSecdiscardable(stretchedLskf,
createSecdiscardable(protectorId, userId));
// No need to pass in quality since the credential type already encodes sufficient info
- synchronizeFrpPassword(pwd, 0, userId);
+ synchronizeGatekeeperFrpPassword(pwd, 0, userId);
}
if (!credential.isNone()) {
saveState(PASSWORD_DATA_NAME, pwd.toBytes(), protectorId, userId);
@@ -916,7 +916,7 @@
LockscreenCredential userCredential,
ICheckCredentialProgressCallback progressCallback) {
PersistentData persistentData = mStorage.readPersistentDataBlock();
- if (persistentData.type == PersistentData.TYPE_SP) {
+ if (persistentData.type == PersistentData.TYPE_SP_GATEKEEPER) {
PasswordData pwd = PasswordData.fromBytes(persistentData.payload);
byte[] stretchedLskf = stretchLskf(userCredential, pwd);
@@ -941,7 +941,7 @@
return weaverVerify(weaverSlot, stretchedLskfToWeaverKey(stretchedLskf)).stripPayload();
} else {
- Slog.e(TAG, "persistentData.type must be TYPE_SP or TYPE_SP_WEAVER, but is "
+ Slog.e(TAG, "persistentData.type must be TYPE_SP_GATEKEEPER or TYPE_SP_WEAVER, but is "
+ persistentData.type);
return VerifyCredentialResponse.ERROR;
}
@@ -960,7 +960,7 @@
if (weaverSlot != INVALID_WEAVER_SLOT) {
synchronizeWeaverFrpPassword(pwd, requestedQuality, userInfo.id, weaverSlot);
} else {
- synchronizeFrpPassword(pwd, requestedQuality, userInfo.id);
+ synchronizeGatekeeperFrpPassword(pwd, requestedQuality, userInfo.id);
}
}
}
@@ -994,13 +994,13 @@
return true;
}
- private void synchronizeFrpPassword(@Nullable PasswordData pwd, int requestedQuality,
+ private void synchronizeGatekeeperFrpPassword(@Nullable PasswordData pwd, int requestedQuality,
int userId) {
if (shouldSynchronizeFrpCredential(pwd, userId)) {
Slogf.d(TAG, "Syncing Gatekeeper-based FRP credential tied to user %d", userId);
if (!isNoneCredential(pwd)) {
- mStorage.writePersistentDataBlock(PersistentData.TYPE_SP, userId, requestedQuality,
- pwd.toBytes());
+ mStorage.writePersistentDataBlock(PersistentData.TYPE_SP_GATEKEEPER, userId,
+ requestedQuality, pwd.toBytes());
} else {
mStorage.writePersistentDataBlock(PersistentData.TYPE_NONE, userId, 0, null);
}
@@ -1039,9 +1039,9 @@
}
TokenData tokenData = new TokenData();
tokenData.mType = type;
- final byte[] secdiscardable = secureRandom(SECDISCARDABLE_LENGTH);
+ final byte[] secdiscardable = SecureRandomUtils.randomBytes(SECDISCARDABLE_LENGTH);
if (isWeaverAvailable()) {
- tokenData.weaverSecret = secureRandom(mWeaverConfig.valueSize);
+ tokenData.weaverSecret = SecureRandomUtils.randomBytes(mWeaverConfig.valueSize);
tokenData.secdiscardableOnDisk = SyntheticPasswordCrypto.encrypt(tokenData.weaverSecret,
PERSONALIZATION_WEAVER_TOKEN, secdiscardable);
} else {
@@ -1224,7 +1224,7 @@
pwd.credentialType = credential.getType();
saveState(PASSWORD_DATA_NAME, pwd.toBytes(), protectorId, userId);
syncState(userId);
- synchronizeFrpPassword(pwd, 0, userId);
+ synchronizeGatekeeperFrpPassword(pwd, 0, userId);
} else {
Slog.w(TAG, "Fail to re-enroll user password for user " + userId);
// continue the flow anyway
@@ -1510,7 +1510,7 @@
* been created.
*/
private byte[] createSecdiscardable(long protectorId, int userId) {
- byte[] data = secureRandom(SECDISCARDABLE_LENGTH);
+ byte[] data = SecureRandomUtils.randomBytes(SECDISCARDABLE_LENGTH);
saveSecdiscardable(protectorId, data, userId);
return data;
}
@@ -1624,12 +1624,12 @@
}
private static long generateProtectorId() {
- SecureRandom rng = new SecureRandom();
- long result;
- do {
- result = rng.nextLong();
- } while (result == NULL_PROTECTOR_ID);
- return result;
+ while (true) {
+ final long result = SecureRandomUtils.randomLong();
+ if (result != NULL_PROTECTOR_ID) {
+ return result;
+ }
+ }
}
@VisibleForTesting
@@ -1637,15 +1637,6 @@
return 100000 + userId;
}
- protected static byte[] secureRandom(int length) {
- try {
- return SecureRandom.getInstance("SHA1PRNG").generateSeed(length);
- } catch (NoSuchAlgorithmException e) {
- e.printStackTrace();
- return null;
- }
- }
-
private String getProtectorKeyAlias(long protectorId) {
return TextUtils.formatSimple("%s%x", PROTECTOR_KEY_ALIAS_PREFIX, protectorId);
}
diff --git a/services/core/java/com/android/server/locksettings/WeaverHidlAdapter.java b/services/core/java/com/android/server/locksettings/WeaverHidlAdapter.java
new file mode 100644
index 0000000..2e9c3fd
--- /dev/null
+++ b/services/core/java/com/android/server/locksettings/WeaverHidlAdapter.java
@@ -0,0 +1,137 @@
+/*
+ * 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.locksettings;
+
+import android.hardware.weaver.IWeaver;
+import android.hardware.weaver.WeaverConfig;
+import android.hardware.weaver.WeaverReadResponse;
+import android.hardware.weaver.WeaverReadStatus;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.util.Slog;
+
+import java.util.ArrayList;
+
+/**
+ * Adapt the legacy HIDL interface to present the AIDL interface.
+ */
+class WeaverHidlAdapter implements IWeaver {
+ private static final String TAG = "WeaverHidlAdapter";
+ private final android.hardware.weaver.V1_0.IWeaver mImpl;
+
+ WeaverHidlAdapter(android.hardware.weaver.V1_0.IWeaver impl) {
+ mImpl = impl;
+ }
+
+ @Override
+ public WeaverConfig getConfig() throws RemoteException {
+ final WeaverConfig[] res = new WeaverConfig[1];
+ mImpl.getConfig((int status, android.hardware.weaver.V1_0.WeaverConfig config) -> {
+ if (status == android.hardware.weaver.V1_0.WeaverStatus.OK) {
+ WeaverConfig aidlRes = new WeaverConfig();
+ aidlRes.slots = config.slots;
+ aidlRes.keySize = config.keySize;
+ aidlRes.valueSize = config.valueSize;
+ res[0] = aidlRes;
+ } else {
+ Slog.e(TAG,
+ "Failed to get HIDL weaver config. status: " + status
+ + ", slots: " + config.slots);
+ }
+ });
+ return res[0];
+ }
+
+ @Override
+ public WeaverReadResponse read(int slotId, byte[] key)
+ throws RemoteException {
+ final WeaverReadResponse[] res = new WeaverReadResponse[1];
+ mImpl.read(
+ slotId, toByteArrayList(key),
+ (int inStatus, android.hardware.weaver.V1_0.WeaverReadResponse readResponse) -> {
+ WeaverReadResponse aidlRes =
+ new WeaverReadResponse();
+ switch (inStatus) {
+ case android.hardware.weaver.V1_0.WeaverReadStatus.OK:
+ aidlRes.status = WeaverReadStatus.OK;
+ break;
+ case android.hardware.weaver.V1_0.WeaverReadStatus.THROTTLE:
+ aidlRes.status = WeaverReadStatus.THROTTLE;
+ break;
+ case android.hardware.weaver.V1_0.WeaverReadStatus.INCORRECT_KEY:
+ aidlRes.status = WeaverReadStatus.INCORRECT_KEY;
+ break;
+ case android.hardware.weaver.V1_0.WeaverReadStatus.FAILED:
+ aidlRes.status = WeaverReadStatus.FAILED;
+ break;
+ default:
+ Slog.e(TAG, "Unexpected status in read: " + inStatus);
+ aidlRes.status = WeaverReadStatus.FAILED;
+ break;
+ }
+ aidlRes.timeout = readResponse.timeout;
+ aidlRes.value = fromByteArrayList(readResponse.value);
+ res[0] = aidlRes;
+ });
+ return res[0];
+ }
+
+ @Override
+ public void write(int slotId, byte[] key, byte[] value) throws RemoteException {
+ int writeStatus = mImpl.write(slotId, toByteArrayList(key), toByteArrayList(value));
+ if (writeStatus != android.hardware.weaver.V1_0.WeaverStatus.OK) {
+ throw new ServiceSpecificException(
+ IWeaver.STATUS_FAILED, "Failed IWeaver.write call, status: " + writeStatus);
+ }
+ }
+
+ @Override
+ public String getInterfaceHash() {
+ // We do not require the interface hash as the client.
+ throw new UnsupportedOperationException(
+ "WeaverHidlAdapter does not support getInterfaceHash");
+ }
+
+ @Override
+ public int getInterfaceVersion() {
+ // Supports only V2 which is at feature parity.
+ return 2;
+ }
+
+ @Override
+ public IBinder asBinder() {
+ // There is no IHwBinder to IBinder. Not required as the client.
+ throw new UnsupportedOperationException("WeaverHidlAdapter does not support asBinder");
+ }
+
+ private static ArrayList<Byte> toByteArrayList(byte[] data) {
+ ArrayList<Byte> result = new ArrayList<Byte>(data.length);
+ for (int i = 0; i < data.length; i++) {
+ result.add(data[i]);
+ }
+ return result;
+ }
+
+ private static byte[] fromByteArrayList(ArrayList<Byte> data) {
+ byte[] result = new byte[data.size()];
+ for (int i = 0; i < data.size(); i++) {
+ result[i] = data.get(i);
+ }
+ return result;
+ }
+}
diff --git a/services/core/java/com/android/server/locksettings/WeaverHidlWrapper.java b/services/core/java/com/android/server/locksettings/WeaverHidlWrapper.java
deleted file mode 100644
index 9d93c3d..0000000
--- a/services/core/java/com/android/server/locksettings/WeaverHidlWrapper.java
+++ /dev/null
@@ -1,143 +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.locksettings;
-
-import android.hardware.weaver.V1_0.IWeaver;
-import android.hardware.weaver.V1_0.WeaverConfig;
-import android.hardware.weaver.V1_0.WeaverReadResponse;
-import android.hardware.weaver.V1_0.WeaverReadStatus;
-import android.hardware.weaver.V1_0.WeaverStatus;
-import android.os.RemoteException;
-import android.os.ServiceSpecificException;
-import android.util.Slog;
-
-import java.util.ArrayList;
-
-/**
- * Implement the AIDL IWeaver interface wrapping the HIDL implementation
- */
-class WeaverHidlWrapper implements android.hardware.weaver.IWeaver {
- private static final String TAG = "WeaverHidlWrapper";
- private final IWeaver mImpl;
-
- WeaverHidlWrapper(IWeaver impl) {
- mImpl = impl;
- }
-
- private static ArrayList<Byte> toByteArrayList(byte[] data) {
- ArrayList<Byte> result = new ArrayList<Byte>(data.length);
- for (int i = 0; i < data.length; i++) {
- result.add(data[i]);
- }
- return result;
- }
-
- private static byte[] fromByteArrayList(ArrayList<Byte> data) {
- byte[] result = new byte[data.size()];
- for (int i = 0; i < data.size(); i++) {
- result[i] = data.get(i);
- }
- return result;
- }
-
- @Override
- public String getInterfaceHash() {
- // We do not require the interface hash as the client.
- throw new UnsupportedOperationException(
- "WeaverHidlWrapper does not support getInterfaceHash");
- }
- @Override
- public int getInterfaceVersion() {
- // Supports only V2 which is at feature parity.
- return 2;
- }
- @Override
- public android.os.IBinder asBinder() {
- // There is no IHwBinder to IBinder. Not required as the client.
- throw new UnsupportedOperationException("WeaverHidlWrapper does not support asBinder");
- }
-
- @Override
- public android.hardware.weaver.WeaverConfig getConfig() throws RemoteException {
- final WeaverConfig[] res = new WeaverConfig[1];
- mImpl.getConfig((int status, WeaverConfig config) -> {
- if (status == WeaverStatus.OK && config.slots > 0) {
- res[0] = config;
- } else {
- res[0] = null;
- Slog.e(TAG,
- "Failed to get HIDL weaver config. status: " + status
- + ", slots: " + config.slots);
- }
- });
-
- if (res[0] == null) {
- return null;
- }
- android.hardware.weaver.WeaverConfig config = new android.hardware.weaver.WeaverConfig();
- config.slots = res[0].slots;
- config.keySize = res[0].keySize;
- config.valueSize = res[0].valueSize;
- return config;
- }
-
- @Override
- public android.hardware.weaver.WeaverReadResponse read(int slotId, byte[] key)
- throws RemoteException {
- final WeaverReadResponse[] res = new WeaverReadResponse[1];
- final int[] status = new int[1];
- mImpl.read(
- slotId, toByteArrayList(key), (int inStatus, WeaverReadResponse readResponse) -> {
- status[0] = inStatus;
- res[0] = readResponse;
- });
-
- android.hardware.weaver.WeaverReadResponse aidlRes =
- new android.hardware.weaver.WeaverReadResponse();
- switch (status[0]) {
- case WeaverReadStatus.OK:
- aidlRes.status = android.hardware.weaver.WeaverReadStatus.OK;
- break;
- case WeaverReadStatus.THROTTLE:
- aidlRes.status = android.hardware.weaver.WeaverReadStatus.THROTTLE;
- break;
- case WeaverReadStatus.INCORRECT_KEY:
- aidlRes.status = android.hardware.weaver.WeaverReadStatus.INCORRECT_KEY;
- break;
- case WeaverReadStatus.FAILED:
- aidlRes.status = android.hardware.weaver.WeaverReadStatus.FAILED;
- break;
- default:
- aidlRes.status = android.hardware.weaver.WeaverReadStatus.FAILED;
- break;
- }
- if (res[0] != null) {
- aidlRes.timeout = res[0].timeout;
- aidlRes.value = fromByteArrayList(res[0].value);
- }
- return aidlRes;
- }
-
- @Override
- public void write(int slotId, byte[] key, byte[] value) throws RemoteException {
- int writeStatus = mImpl.write(slotId, toByteArrayList(key), toByteArrayList(value));
- if (writeStatus != WeaverStatus.OK) {
- throw new ServiceSpecificException(
- android.hardware.weaver.IWeaver.STATUS_FAILED, "Failed IWeaver.write call");
- }
- }
-}
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 50e1fca..e9ee750 100644
--- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
+++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
@@ -370,6 +370,26 @@
}
}
+ @Override
+ public void notifyActiveProjectionCapturedContentVisibilityChanged(boolean isVisible) {
+ 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.dispatchVisibilityChanged(mProjectionGrant, isVisible);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
@Override //Binder call
public void addCallback(final IMediaProjectionWatcherCallback callback) {
if (mContext.checkCallingPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION)
@@ -750,8 +770,9 @@
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!");
+ Slog.e(TAG,
+ "Tried to dispatch resize notification for a null media projection. "
+ + "Ignoring!");
return;
}
synchronized (mLock) {
@@ -774,6 +795,36 @@
// is for passing along if recording is still ongoing or not.
}
}
+
+ public void dispatchVisibilityChanged(MediaProjection projection, boolean isVisible) {
+ if (projection == null) {
+ Slog.e(TAG,
+ "Tried to dispatch visibility changed 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.onCapturedContentVisibilityChanged(isVisible);
+ } catch (RemoteException e) {
+ Slog.w(TAG,
+ "Failed to notify media projection has captured content "
+ + "visibility change to "
+ + isVisible, e);
+ }
+ });
+ }
+ // Do not need to notify watcher callback about visibility changes, 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/net/NetworkPolicyLogger.java b/services/core/java/com/android/server/net/NetworkPolicyLogger.java
index 4a0a07b..dc8fcb0 100644
--- a/services/core/java/com/android/server/net/NetworkPolicyLogger.java
+++ b/services/core/java/com/android/server/net/NetworkPolicyLogger.java
@@ -94,7 +94,7 @@
void networkBlocked(int uid, @Nullable UidBlockedState uidBlockedState) {
synchronized (mLock) {
if (LOGD || uid == mDebugUid) {
- Slog.d(TAG, "Blocked state of " + uid + ": " + uidBlockedState.toString());
+ Slog.d(TAG, "Blocked state of " + uid + ": " + uidBlockedState);
}
if (uidBlockedState == null) {
mNetworkBlockedBuffer.networkBlocked(uid, BLOCKED_REASON_NONE, ALLOWED_REASON_NONE,
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 90135ad..b06c411 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -240,7 +240,6 @@
import android.service.notification.NotificationRecordProto;
import android.service.notification.NotificationServiceDumpProto;
import android.service.notification.NotificationStats;
-import android.service.notification.SnoozeCriterion;
import android.service.notification.StatusBarNotification;
import android.service.notification.ZenModeConfig;
import android.service.notification.ZenModeProto;
@@ -8569,95 +8568,6 @@
}
}
- static class NotificationRecordExtractorData {
- // Class that stores any field in a NotificationRecord that can change via an extractor.
- // Used to cache previous data used in a sort.
- int mPosition;
- int mVisibility;
- boolean mShowBadge;
- boolean mAllowBubble;
- boolean mIsBubble;
- NotificationChannel mChannel;
- String mGroupKey;
- ArrayList<String> mOverridePeople;
- ArrayList<SnoozeCriterion> mSnoozeCriteria;
- Integer mUserSentiment;
- Integer mSuppressVisually;
- ArrayList<Notification.Action> mSystemSmartActions;
- ArrayList<CharSequence> mSmartReplies;
- int mImportance;
-
- // These fields may not trigger a reranking but diffs here may be logged.
- float mRankingScore;
- boolean mIsConversation;
-
- NotificationRecordExtractorData(int position, int visibility, boolean showBadge,
- boolean allowBubble, boolean isBubble, NotificationChannel channel, String groupKey,
- ArrayList<String> overridePeople, ArrayList<SnoozeCriterion> snoozeCriteria,
- Integer userSentiment, Integer suppressVisually,
- ArrayList<Notification.Action> systemSmartActions,
- ArrayList<CharSequence> smartReplies, int importance, float rankingScore,
- boolean isConversation) {
- mPosition = position;
- mVisibility = visibility;
- mShowBadge = showBadge;
- mAllowBubble = allowBubble;
- mIsBubble = isBubble;
- mChannel = channel;
- mGroupKey = groupKey;
- mOverridePeople = overridePeople;
- mSnoozeCriteria = snoozeCriteria;
- mUserSentiment = userSentiment;
- mSuppressVisually = suppressVisually;
- mSystemSmartActions = systemSmartActions;
- mSmartReplies = smartReplies;
- mImportance = importance;
- mRankingScore = rankingScore;
- mIsConversation = isConversation;
- }
-
- // Returns whether the provided NotificationRecord differs from the cached data in any way.
- // Should be guarded by mNotificationLock; not annotated here as this class is static.
- boolean hasDiffForRankingLocked(NotificationRecord r, int newPosition) {
- return mPosition != newPosition
- || mVisibility != r.getPackageVisibilityOverride()
- || mShowBadge != r.canShowBadge()
- || mAllowBubble != r.canBubble()
- || mIsBubble != r.getNotification().isBubbleNotification()
- || !Objects.equals(mChannel, r.getChannel())
- || !Objects.equals(mGroupKey, r.getGroupKey())
- || !Objects.equals(mOverridePeople, r.getPeopleOverride())
- || !Objects.equals(mSnoozeCriteria, r.getSnoozeCriteria())
- || !Objects.equals(mUserSentiment, r.getUserSentiment())
- || !Objects.equals(mSuppressVisually, r.getSuppressedVisualEffects())
- || !Objects.equals(mSystemSmartActions, r.getSystemGeneratedSmartActions())
- || !Objects.equals(mSmartReplies, r.getSmartReplies())
- || mImportance != r.getImportance();
- }
-
- // Returns whether the NotificationRecord has a change from this data for which we should
- // log an update. This method specifically targets fields that may be changed via
- // adjustments from the assistant.
- //
- // Fields here are the union of things in NotificationRecordLogger.shouldLogReported
- // and NotificationRecord.applyAdjustments.
- //
- // Should be guarded by mNotificationLock; not annotated here as this class is static.
- boolean hasDiffForLoggingLocked(NotificationRecord r, int newPosition) {
- return mPosition != newPosition
- || !Objects.equals(mChannel, r.getChannel())
- || !Objects.equals(mGroupKey, r.getGroupKey())
- || !Objects.equals(mOverridePeople, r.getPeopleOverride())
- || !Objects.equals(mSnoozeCriteria, r.getSnoozeCriteria())
- || !Objects.equals(mUserSentiment, r.getUserSentiment())
- || !Objects.equals(mSystemSmartActions, r.getSystemGeneratedSmartActions())
- || !Objects.equals(mSmartReplies, r.getSmartReplies())
- || mImportance != r.getImportance()
- || !r.rankingScoreMatches(mRankingScore)
- || mIsConversation != r.isConversation();
- }
- }
-
void handleRankingSort() {
if (mRankingHelper == null) return;
synchronized (mNotificationLock) {
@@ -8683,7 +8593,8 @@
r.getSmartReplies(),
r.getImportance(),
r.getRankingScore(),
- r.isConversation());
+ r.isConversation(),
+ r.getProposedImportance());
extractorDataBefore.put(r.getKey(), extractorData);
mRankingHelper.extractSignals(r);
}
@@ -9978,7 +9889,8 @@
record.getRankingScore() == 0
? RANKING_UNCHANGED
: (record.getRankingScore() > 0 ? RANKING_PROMOTED : RANKING_DEMOTED),
- record.getNotification().isBubbleNotification()
+ record.getNotification().isBubbleNotification(),
+ record.getProposedImportance()
);
rankings.add(ranking);
}
diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java
index 6dd0daa..91b5afe 100644
--- a/services/core/java/com/android/server/notification/NotificationRecord.java
+++ b/services/core/java/com/android/server/notification/NotificationRecord.java
@@ -210,6 +210,7 @@
// Whether this notification record should have an update logged the next time notifications
// are sorted.
private boolean mPendingLogUpdate = false;
+ private int mProposedImportance = IMPORTANCE_UNSPECIFIED;
public NotificationRecord(Context context, StatusBarNotification sbn,
NotificationChannel channel) {
@@ -499,6 +500,8 @@
pw.println(prefix + "mImportance="
+ NotificationListenerService.Ranking.importanceToString(mImportance));
pw.println(prefix + "mImportanceExplanation=" + getImportanceExplanation());
+ pw.println(prefix + "mProposedImportance="
+ + NotificationListenerService.Ranking.importanceToString(mProposedImportance));
pw.println(prefix + "mIsAppImportanceLocked=" + mIsAppImportanceLocked);
pw.println(prefix + "mIntercept=" + mIntercept);
pw.println(prefix + "mHidden==" + mHidden);
@@ -738,6 +741,12 @@
Adjustment.KEY_NOT_CONVERSATION,
Boolean.toString(mIsNotConversationOverride));
}
+ if (signals.containsKey(Adjustment.KEY_IMPORTANCE_PROPOSAL)) {
+ mProposedImportance = signals.getInt(Adjustment.KEY_IMPORTANCE_PROPOSAL);
+ EventLogTags.writeNotificationAdjusted(getKey(),
+ Adjustment.KEY_IMPORTANCE_PROPOSAL,
+ Integer.toString(mProposedImportance));
+ }
if (!signals.isEmpty() && adjustment.getIssuer() != null) {
mAdjustmentIssuer = adjustment.getIssuer();
}
@@ -870,6 +879,10 @@
return stats.naturalImportance;
}
+ public int getProposedImportance() {
+ return mProposedImportance;
+ }
+
public float getRankingScore() {
return mRankingScore;
}
diff --git a/services/core/java/com/android/server/notification/NotificationRecordExtractorData.java b/services/core/java/com/android/server/notification/NotificationRecordExtractorData.java
new file mode 100644
index 0000000..6dc9029
--- /dev/null
+++ b/services/core/java/com/android/server/notification/NotificationRecordExtractorData.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.notification;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.service.notification.SnoozeCriterion;
+
+import java.util.ArrayList;
+import java.util.Objects;
+
+/**
+ * Class that stores any field in a NotificationRecord that can change via an extractor.
+ * Used to cache previous data used in a sort.
+ */
+public final class NotificationRecordExtractorData {
+ private final int mPosition;
+ private final int mVisibility;
+ private final boolean mShowBadge;
+ private final boolean mAllowBubble;
+ private final boolean mIsBubble;
+ private final NotificationChannel mChannel;
+ private final String mGroupKey;
+ private final ArrayList<String> mOverridePeople;
+ private final ArrayList<SnoozeCriterion> mSnoozeCriteria;
+ private final Integer mUserSentiment;
+ private final Integer mSuppressVisually;
+ private final ArrayList<Notification.Action> mSystemSmartActions;
+ private final ArrayList<CharSequence> mSmartReplies;
+ private final int mImportance;
+
+ // These fields may not trigger a reranking but diffs here may be logged.
+ private final float mRankingScore;
+ private final boolean mIsConversation;
+ private final int mProposedImportance;
+
+ NotificationRecordExtractorData(int position, int visibility, boolean showBadge,
+ boolean allowBubble, boolean isBubble, NotificationChannel channel, String groupKey,
+ ArrayList<String> overridePeople, ArrayList<SnoozeCriterion> snoozeCriteria,
+ Integer userSentiment, Integer suppressVisually,
+ ArrayList<Notification.Action> systemSmartActions,
+ ArrayList<CharSequence> smartReplies, int importance, float rankingScore,
+ boolean isConversation, int proposedImportance) {
+ mPosition = position;
+ mVisibility = visibility;
+ mShowBadge = showBadge;
+ mAllowBubble = allowBubble;
+ mIsBubble = isBubble;
+ mChannel = channel;
+ mGroupKey = groupKey;
+ mOverridePeople = overridePeople;
+ mSnoozeCriteria = snoozeCriteria;
+ mUserSentiment = userSentiment;
+ mSuppressVisually = suppressVisually;
+ mSystemSmartActions = systemSmartActions;
+ mSmartReplies = smartReplies;
+ mImportance = importance;
+ mRankingScore = rankingScore;
+ mIsConversation = isConversation;
+ mProposedImportance = proposedImportance;
+ }
+
+ // Returns whether the provided NotificationRecord differs from the cached data in any way.
+ // Should be guarded by mNotificationLock; not annotated here as this class is static.
+ boolean hasDiffForRankingLocked(NotificationRecord r, int newPosition) {
+ return mPosition != newPosition
+ || mVisibility != r.getPackageVisibilityOverride()
+ || mShowBadge != r.canShowBadge()
+ || mAllowBubble != r.canBubble()
+ || mIsBubble != r.getNotification().isBubbleNotification()
+ || !Objects.equals(mChannel, r.getChannel())
+ || !Objects.equals(mGroupKey, r.getGroupKey())
+ || !Objects.equals(mOverridePeople, r.getPeopleOverride())
+ || !Objects.equals(mSnoozeCriteria, r.getSnoozeCriteria())
+ || !Objects.equals(mUserSentiment, r.getUserSentiment())
+ || !Objects.equals(mSuppressVisually, r.getSuppressedVisualEffects())
+ || !Objects.equals(mSystemSmartActions, r.getSystemGeneratedSmartActions())
+ || !Objects.equals(mSmartReplies, r.getSmartReplies())
+ || mImportance != r.getImportance()
+ || mProposedImportance != r.getProposedImportance();
+ }
+
+ // Returns whether the NotificationRecord has a change from this data for which we should
+ // log an update. This method specifically targets fields that may be changed via
+ // adjustments from the assistant.
+ //
+ // Fields here are the union of things in NotificationRecordLogger.shouldLogReported
+ // and NotificationRecord.applyAdjustments.
+ //
+ // Should be guarded by mNotificationLock; not annotated here as this class is static.
+ boolean hasDiffForLoggingLocked(NotificationRecord r, int newPosition) {
+ return mPosition != newPosition
+ || !Objects.equals(mChannel, r.getChannel())
+ || !Objects.equals(mGroupKey, r.getGroupKey())
+ || !Objects.equals(mOverridePeople, r.getPeopleOverride())
+ || !Objects.equals(mSnoozeCriteria, r.getSnoozeCriteria())
+ || !Objects.equals(mUserSentiment, r.getUserSentiment())
+ || !Objects.equals(mSystemSmartActions, r.getSystemGeneratedSmartActions())
+ || !Objects.equals(mSmartReplies, r.getSmartReplies())
+ || mImportance != r.getImportance()
+ || !r.rankingScoreMatches(mRankingScore)
+ || mIsConversation != r.isConversation()
+ || mProposedImportance != r.getProposedImportance();
+ }
+}
diff --git a/services/core/java/com/android/server/pm/AppStateHelper.java b/services/core/java/com/android/server/pm/AppStateHelper.java
index 2ef193c..32479ee 100644
--- a/services/core/java/com/android/server/pm/AppStateHelper.java
+++ b/services/core/java/com/android/server/pm/AppStateHelper.java
@@ -23,6 +23,7 @@
import android.app.usage.NetworkStats;
import android.app.usage.NetworkStatsManager;
import android.content.Context;
+import android.content.pm.PackageManagerInternal;
import android.media.AudioManager;
import android.media.IAudioService;
import android.net.ConnectivityManager;
@@ -36,6 +37,7 @@
import com.android.server.LocalServices;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -157,6 +159,56 @@
return false;
}
+ private static boolean containsAny(Collection<String> list, Collection<String> which) {
+ if (list.isEmpty()) {
+ return false;
+ }
+ for (var element : which) {
+ if (list.contains(element)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void addLibraryDependency(ArraySet<String> results, List<String> libPackageNames) {
+ var pmInternal = LocalServices.getService(PackageManagerInternal.class);
+
+ var libraryNames = new ArraySet<String>();
+ var staticSharedLibraryNames = new ArraySet<String>();
+ var sdkLibraryNames = new ArraySet<String>();
+ for (var packageName : libPackageNames) {
+ var pkg = pmInternal.getAndroidPackage(packageName);
+ if (pkg == null) {
+ continue;
+ }
+ libraryNames.addAll(pkg.getLibraryNames());
+ var libraryName = pkg.getStaticSharedLibraryName();
+ if (libraryName != null) {
+ staticSharedLibraryNames.add(libraryName);
+ }
+ libraryName = pkg.getSdkLibraryName();
+ if (libraryName != null) {
+ sdkLibraryNames.add(libraryName);
+ }
+ }
+
+ if (libraryNames.isEmpty()
+ && staticSharedLibraryNames.isEmpty()
+ && sdkLibraryNames.isEmpty()) {
+ return;
+ }
+
+ pmInternal.forEachPackage(pkg -> {
+ if (containsAny(pkg.getUsesLibraries(), libraryNames)
+ || containsAny(pkg.getUsesOptionalLibraries(), libraryNames)
+ || containsAny(pkg.getUsesStaticLibraries(), staticSharedLibraryNames)
+ || containsAny(pkg.getUsesSdkLibraries(), sdkLibraryNames)) {
+ results.add(pkg.getPackageName());
+ }
+ });
+ }
+
/**
* True if any app has sent or received network data over the past
* {@link #ACTIVE_NETWORK_DURATION_MILLIS} milliseconds.
@@ -225,6 +277,7 @@
*/
public List<String> getDependencyPackages(List<String> packageNames) {
var results = new ArraySet<String>();
+ // Include packages sharing the same process
var am = mContext.getSystemService(ActivityManager.class);
for (var info : am.getRunningAppProcesses()) {
for (var packageName : packageNames) {
@@ -236,10 +289,14 @@
}
}
}
+ // Include packages using bounded services
var amInternal = LocalServices.getService(ActivityManagerInternal.class);
for (var packageName : packageNames) {
results.addAll(amInternal.getClientPackages(packageName));
}
+ // Include packages using libraries
+ addLibraryDependency(results, packageNames);
+
return new ArrayList<>(results);
}
}
diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java
index d873736..0a59c19 100644
--- a/services/core/java/com/android/server/pm/ComputerEngine.java
+++ b/services/core/java/com/android/server/pm/ComputerEngine.java
@@ -2045,7 +2045,8 @@
}
final SharedLibraryInfo libraryInfo = getSharedLibraryInfo(
- ps.getPkg().getStaticSharedLibraryName(), ps.getPkg().getStaticSharedLibVersion());
+ ps.getPkg().getStaticSharedLibraryName(),
+ ps.getPkg().getStaticSharedLibraryVersion());
if (libraryInfo == null) {
return false;
}
diff --git a/services/core/java/com/android/server/pm/DeletePackageHelper.java b/services/core/java/com/android/server/pm/DeletePackageHelper.java
index 9ea1807..3df46a2 100644
--- a/services/core/java/com/android/server/pm/DeletePackageHelper.java
+++ b/services/core/java/com/android/server/pm/DeletePackageHelper.java
@@ -186,7 +186,7 @@
SharedLibraryInfo libraryInfo = null;
if (pkg.getStaticSharedLibraryName() != null) {
libraryInfo = computer.getSharedLibraryInfo(pkg.getStaticSharedLibraryName(),
- pkg.getStaticSharedLibVersion());
+ pkg.getStaticSharedLibraryVersion());
} else if (pkg.getSdkLibraryName() != null) {
libraryInfo = computer.getSharedLibraryInfo(pkg.getSdkLibraryName(),
pkg.getSdkLibVersionMajor());
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index 7049f9a..16bf0fe 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -297,7 +297,7 @@
SharedUserSetting sharedUserSetting = mPm.mSettings.getSharedUserSettingLPr(pkgSetting);
if (sharedUserSetting != null) {
sharedUserSetting.addPackage(pkgSetting);
- if (parsedPackage.isLeavingSharedUid()
+ if (parsedPackage.isLeavingSharedUser()
&& SharedUidMigration.applyStrategy(BEST_EFFORT)
&& sharedUserSetting.isSingleUser()) {
// Attempt the transparent shared UID migration
@@ -1552,7 +1552,7 @@
}
// APK should not re-join shared UID
- if (oldPackage.isLeavingSharedUid() && !parsedPackage.isLeavingSharedUid()) {
+ if (oldPackage.isLeavingSharedUser() && !parsedPackage.isLeavingSharedUser()) {
throw new PrepareFailure(INSTALL_FAILED_UID_CHANGED,
"Package " + parsedPackage.getPackageName()
+ " attempting to rejoin " + newSharedUid);
@@ -3801,7 +3801,7 @@
if (installedPkgSetting == null || !installedPkgSetting.hasSharedUser()) {
// Directly ignore sharedUserSetting for new installs, or if the app has
// already left shared UID
- ignoreSharedUserId = parsedPackage.isLeavingSharedUid();
+ ignoreSharedUserId = parsedPackage.isLeavingSharedUser();
}
if (!ignoreSharedUserId && parsedPackage.getSharedUserId() != null) {
@@ -4324,10 +4324,10 @@
SharedLibraryInfo libInfo = versionedLib.valueAt(i);
final long libVersionCode = libInfo.getDeclaringPackage()
.getLongVersionCode();
- if (libInfo.getLongVersion() < pkg.getStaticSharedLibVersion()) {
+ if (libInfo.getLongVersion() < pkg.getStaticSharedLibraryVersion()) {
minVersionCode = Math.max(minVersionCode, libVersionCode + 1);
} else if (libInfo.getLongVersion()
- > pkg.getStaticSharedLibVersion()) {
+ > pkg.getStaticSharedLibraryVersion()) {
maxVersionCode = Math.min(maxVersionCode, libVersionCode - 1);
} else {
minVersionCode = maxVersionCode = libVersionCode;
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java
index 9c60795..8c5bab6 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerService.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerService.java
@@ -1211,7 +1211,8 @@
// Take a short detour to confirm with user
final Intent intent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE);
intent.setData(Uri.fromParts("package", versionedPackage.getPackageName(), null));
- intent.putExtra(PackageInstaller.EXTRA_CALLBACK, adapter.getBinder().asBinder());
+ intent.putExtra(PackageInstaller.EXTRA_CALLBACK,
+ new PackageManager.UninstallCompleteCallback(adapter.getBinder().asBinder()));
adapter.onUserActionRequired(intent);
}
}
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index ed4c849..afcd9d1 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -2579,6 +2579,10 @@
: PackageInstaller.ACTION_CONFIRM_INSTALL);
intent.setPackage(mPm.getPackageInstallerPackageName());
intent.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId);
+ synchronized (mLock) {
+ intent.putExtra(PackageInstaller.EXTRA_RESOLVED_BASE_PATH,
+ mResolvedBaseFile != null ? mResolvedBaseFile.getAbsolutePath() : null);
+ }
sendOnUserActionRequired(mContext, target, sessionId, intent);
}
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index a52ed8b..c1298ff 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -499,7 +499,7 @@
private static final String PROPERTY_KNOWN_DIGESTERS_LIST = "known_digesters_list";
/**
- * Whether of not requesting the approval before committing sessions is available.
+ * Whether or not requesting the approval before committing sessions is available.
*
* Flag type: {@code boolean}
* Namespace: NAMESPACE_PACKAGE_MANAGER_SERVICE
@@ -508,6 +508,15 @@
"is_preapproval_available";
/**
+ * Whether or not the update ownership enforcement is available.
+ *
+ * Flag type: {@code boolean}
+ * Namespace: NAMESPACE_PACKAGE_MANAGER_SERVICE
+ */
+ private static final String PROPERTY_IS_UPDATE_OWNERSHIP_ENFORCEMENT_AVAILABLE =
+ "is_update_ownership_enforcement_available";
+
+ /**
* The default response for package verification timeout.
*
* This can be either PackageManager.VERIFICATION_ALLOW or
@@ -1105,8 +1114,9 @@
@Deprecated
@NonNull
public Computer snapshotComputer(boolean allowLiveComputer) {
+ var isHoldingPackageLock = Thread.holdsLock(mLock);
if (allowLiveComputer) {
- if (Thread.holdsLock(mLock)) {
+ if (isHoldingPackageLock) {
// If the current thread holds mLock then it may have modified state but not
// yet invalidated the snapshot. Always give the thread the live computer.
return mLiveComputer;
@@ -1120,6 +1130,15 @@
return oldSnapshot.use();
}
+ if (isHoldingPackageLock) {
+ // If the current thread holds mLock then it already has exclusive write access to the
+ // two snapshot fields, and we can just go ahead and rebuild the snapshot.
+ @SuppressWarnings("GuardedBy")
+ var newSnapshot = rebuildSnapshot(oldSnapshot, pendingVersion);
+ sSnapshot.set(newSnapshot);
+ return newSnapshot.use();
+ }
+
synchronized (mSnapshotLock) {
// Re-capture pending version in case a new invalidation occurred since last check
var rebuildSnapshot = sSnapshot.get();
@@ -1137,7 +1156,11 @@
// Fetch version one last time to ensure that the rebuilt snapshot matches
// the latest invalidation, which could have come in between entering the
// SnapshotLock and mLock sync blocks.
+ rebuildSnapshot = sSnapshot.get();
rebuildVersion = sSnapshotPendingVersion.get();
+ if (rebuildSnapshot != null && rebuildSnapshot.getVersion() == rebuildVersion) {
+ return rebuildSnapshot.use();
+ }
// Build the snapshot for this version
var newSnapshot = rebuildSnapshot(rebuildSnapshot, rebuildVersion);
@@ -1147,7 +1170,7 @@
}
}
- @GuardedBy({ "mLock", "mSnapshotLock"})
+ @GuardedBy("mLock")
private Computer rebuildSnapshot(@Nullable Computer oldSnapshot, int newVersion) {
var now = SystemClock.currentTimeMicro();
var hits = oldSnapshot == null ? -1 : oldSnapshot.getUsed();
@@ -2065,6 +2088,14 @@
mInitAppsHelper.initNonSystemApps(packageParser, userIds, startTime);
packageParser.close();
+ mRequiredVerifierPackages = getRequiredButNotReallyRequiredVerifiersLPr(computer);
+ mRequiredInstallerPackage = getRequiredInstallerLPr(computer);
+ mRequiredUninstallerPackage = getRequiredUninstallerLPr(computer);
+
+ // PermissionController hosts default permission granting and role management, so it's a
+ // critical part of the core system.
+ mRequiredPermissionControllerPackage = getRequiredPermissionControllerLPr(computer);
+
// Resolve the storage manager.
mStorageManagerPackage = getStorageManagerPackageName(computer);
@@ -2210,9 +2241,6 @@
EventLog.writeEvent(EventLogTags.BOOT_PROGRESS_PMS_READY,
SystemClock.uptimeMillis());
- mRequiredVerifierPackages = getRequiredButNotReallyRequiredVerifiersLPr(computer);
- mRequiredInstallerPackage = getRequiredInstallerLPr(computer);
- mRequiredUninstallerPackage = getRequiredUninstallerLPr(computer);
ComponentName intentFilterVerifierComponent =
getIntentFilterVerifierComponentNameLPr(computer);
ComponentName domainVerificationAgent =
@@ -2230,10 +2258,6 @@
PackageManager.SYSTEM_SHARED_LIBRARY_SHARED,
SharedLibraryInfo.VERSION_UNDEFINED);
- // PermissionController hosts default permission granting and role management, so it's a
- // critical part of the core system.
- mRequiredPermissionControllerPackage = getRequiredPermissionControllerLPr(computer);
-
mSettings.setPermissionControllerVersion(
computer.getPackageInfo(mRequiredPermissionControllerPackage, 0,
UserHandle.USER_SYSTEM).getLongVersionCode());
@@ -2878,7 +2902,7 @@
static void renameStaticSharedLibraryPackage(ParsedPackage parsedPackage) {
// Derive the new package synthetic package name
parsedPackage.setPackageName(toStaticSharedLibraryPackageName(
- parsedPackage.getPackageName(), parsedPackage.getStaticSharedLibVersion()));
+ parsedPackage.getPackageName(), parsedPackage.getStaticSharedLibraryVersion()));
}
private static String toStaticSharedLibraryPackageName(
@@ -7060,6 +7084,16 @@
}
}
+ static boolean isUpdateOwnershipEnforcementAvailable() {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ return DeviceConfig.getBoolean(NAMESPACE_PACKAGE_MANAGER_SERVICE,
+ PROPERTY_IS_UPDATE_OWNERSHIP_ENFORCEMENT_AVAILABLE, false /* defaultValue */);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
/**
* Returns the array containing per-uid timeout configuration.
* This is derived from DeviceConfig flags.
diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java
index b18179e..433e7a1 100644
--- a/services/core/java/com/android/server/pm/PackageSetting.java
+++ b/services/core/java/com/android/server/pm/PackageSetting.java
@@ -1386,6 +1386,8 @@
return AndroidPackageUtils.getHiddenApiEnforcementPolicy(getAndroidPackage(), this);
}
+
+
// Code below generated by codegen v1.0.23.
//
// DO NOT MODIFY!
diff --git a/services/core/java/com/android/server/pm/RemovePackageHelper.java b/services/core/java/com/android/server/pm/RemovePackageHelper.java
index 9b6bfd9..eb99536 100644
--- a/services/core/java/com/android/server/pm/RemovePackageHelper.java
+++ b/services/core/java/com/android/server/pm/RemovePackageHelper.java
@@ -234,7 +234,7 @@
}
if (pkg.getStaticSharedLibraryName() != null) {
if (mSharedLibraries.removeSharedLibrary(pkg.getStaticSharedLibraryName(),
- pkg.getStaticSharedLibVersion())) {
+ pkg.getStaticSharedLibraryVersion())) {
if (DEBUG_REMOVE && chatty) {
if (r == null) {
r = new StringBuilder(256);
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index 53be787..321c5c6 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -89,6 +89,7 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.BackgroundThread;
+import com.android.internal.security.VerityUtils;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.CollectionUtils;
import com.android.internal.util.IndentingPrintWriter;
@@ -370,6 +371,8 @@
// Current settings file.
private final File mSettingsFilename;
+ // Reserve copy of the current settings file.
+ private final File mSettingsReserveCopyFilename;
// Previous settings file.
// Removed when the current settings file successfully stored.
private final File mPreviousSettingsFilename;
@@ -640,6 +643,7 @@
mRuntimePermissionsPersistence = null;
mPermissionDataProvider = null;
mSettingsFilename = null;
+ mSettingsReserveCopyFilename = null;
mPreviousSettingsFilename = null;
mPackageListFilename = null;
mStoppedPackagesFilename = null;
@@ -711,6 +715,7 @@
|FileUtils.S_IROTH|FileUtils.S_IXOTH,
-1, -1);
mSettingsFilename = new File(mSystemDir, "packages.xml");
+ mSettingsReserveCopyFilename = new File(mSystemDir, "packages.xml.reservecopy");
mPreviousSettingsFilename = new File(mSystemDir, "packages-backup.xml");
mPackageListFilename = new File(mSystemDir, "packages.list");
FileUtils.setPermissions(mPackageListFilename, 0640, SYSTEM_UID, PACKAGE_INFO_GID);
@@ -752,6 +757,7 @@
mLock = null;
mRuntimePermissionsPersistence = r.mRuntimePermissionsPersistence;
mSettingsFilename = null;
+ mSettingsReserveCopyFilename = null;
mPreviousSettingsFilename = null;
mPackageListFilename = null;
mStoppedPackagesFilename = null;
@@ -1455,7 +1461,7 @@
void checkAndConvertSharedUserSettingsLPw(SharedUserSetting sharedUser) {
if (!sharedUser.isSingleUser()) return;
final AndroidPackage pkg = sharedUser.getPackageSettings().valueAt(0).getPkg();
- if (pkg != null && pkg.isLeavingSharedUid()
+ if (pkg != null && pkg.isLeavingSharedUser()
&& SharedUidMigration.applyStrategy(BEST_EFFORT)) {
convertSharedUserSettingsLPw(sharedUser);
}
@@ -2681,12 +2687,25 @@
// New settings successfully written, old ones are no longer needed.
mPreviousSettingsFilename.delete();
+ mSettingsReserveCopyFilename.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);
+ try {
+ FileUtils.copy(mSettingsFilename, mSettingsReserveCopyFilename);
+ } catch (IOException e) {
+ Slog.e(TAG, "Failed to backup settings", e);
+ }
+
+ try {
+ VerityUtils.setUpFsverity(mSettingsFilename.getAbsolutePath());
+ VerityUtils.setUpFsverity(mSettingsReserveCopyFilename.getAbsolutePath());
+ } catch (IOException e) {
+ Slog.e(TAG, "Failed to verity-protect settings", e);
+ }
+
writeKernelMappingLPr();
writePackageListLPr();
writeAllUsersPackageRestrictionsLPr(sync);
@@ -3117,49 +3136,62 @@
}
}
- boolean readLPw(@NonNull Computer computer, @NonNull List<UserInfo> users) {
- FileInputStream str = null;
- if (mPreviousSettingsFilename.exists()) {
- try {
- str = new FileInputStream(mPreviousSettingsFilename);
- mReadMessages.append("Reading from backup settings file\n");
- PackageManagerService.reportSettingsProblem(Log.INFO,
- "Need to read from backup settings file");
- if (mSettingsFilename.exists()) {
- // If both the previous and current settings files exist,
- // we ignore the current since it might have been corrupted.
- Slog.w(PackageManagerService.TAG, "Cleaning up settings file "
- + mSettingsFilename);
- mSettingsFilename.delete();
- }
- } catch (java.io.IOException e) {
- // We'll try for the normal settings file.
- }
- }
-
+ boolean readSettingsLPw(@NonNull Computer computer, @NonNull List<UserInfo> users,
+ ArrayMap<String, Long> originalFirstInstallTimes) {
mPendingPackages.clear();
mPastSignatures.clear();
mKeySetRefs.clear();
mInstallerPackages.clear();
+ originalFirstInstallTimes.clear();
- // If any user state doesn't have a first install time, e.g., after an OTA,
- // use the pre OTA firstInstallTime timestamp. This is because we migrated from per package
- // firstInstallTime to per user-state. Without this, OTA can cause this info to be lost.
- final ArrayMap<String, Long> originalFirstInstallTimes = new ArrayMap<>();
+ File file = null;
+ FileInputStream str = null;
try {
- if (str == null) {
- if (!mSettingsFilename.exists()) {
- mReadMessages.append("No settings file found\n");
+ // Check if the previous write was incomplete.
+ if (mPreviousSettingsFilename.exists()) {
+ try {
+ file = mPreviousSettingsFilename;
+ str = new FileInputStream(file);
+ mReadMessages.append("Reading from backup settings file\n");
PackageManagerService.reportSettingsProblem(Log.INFO,
- "No settings file; creating initial state");
- // It's enough to just touch version details to create them
- // with default values
- findOrCreateVersion(StorageManager.UUID_PRIVATE_INTERNAL).forceCurrent();
- findOrCreateVersion(StorageManager.UUID_PRIMARY_PHYSICAL).forceCurrent();
- return false;
+ "Need to read from backup settings file");
+ if (mSettingsFilename.exists()) {
+ // If both the previous and current settings files exist,
+ // we ignore the current since it might have been corrupted.
+ Slog.w(PackageManagerService.TAG, "Cleaning up settings file "
+ + mSettingsFilename);
+ mSettingsFilename.delete();
+ }
+ // Ignore reserve copy as well.
+ mSettingsReserveCopyFilename.delete();
+ } catch (java.io.IOException e) {
+ // We'll try for the normal settings file.
}
- str = new FileInputStream(mSettingsFilename);
+ }
+ if (str == null) {
+ if (mSettingsFilename.exists()) {
+ // Using packages.xml.
+ file = mSettingsFilename;
+ str = new FileInputStream(file);
+ } else if (mSettingsReserveCopyFilename.exists()) {
+ // Using reserve copy.
+ file = mSettingsReserveCopyFilename;
+ str = new FileInputStream(file);
+ mReadMessages.append("Reading from reserve copy settings file\n");
+ PackageManagerService.reportSettingsProblem(Log.INFO,
+ "Need to read from reserve copy settings file");
+ }
+ }
+ if (str == null) {
+ // No available data sources.
+ mReadMessages.append("No settings file found\n");
+ PackageManagerService.reportSettingsProblem(Log.INFO,
+ "No settings file; creating initial state");
+ // Not necessary, but will avoid wtf-s in the "finally" section.
+ findOrCreateVersion(StorageManager.UUID_PRIVATE_INTERNAL).forceCurrent();
+ findOrCreateVersion(StorageManager.UUID_PRIMARY_PHYSICAL).forceCurrent();
+ return false;
}
final TypedXmlPullParser parser = Xml.resolvePullParser(str);
@@ -3280,6 +3312,33 @@
mReadMessages.append("Error reading: " + e.toString());
PackageManagerService.reportSettingsProblem(Log.ERROR, "Error reading settings: " + e);
Slog.wtf(PackageManagerService.TAG, "Error reading package manager settings", e);
+
+ // Remove corrupted file and retry.
+ Slog.e(TAG,
+ "Error reading package manager settings, removing " + file + " and retrying.",
+ e);
+ file.delete();
+
+ // Ignore the result to not mark this as a "first boot".
+ readSettingsLPw(computer, users, originalFirstInstallTimes);
+ }
+
+ return true;
+ }
+
+ /**
+ * @return false if settings file is missing (i.e. during first boot), true otherwise
+ */
+ boolean readLPw(@NonNull Computer computer, @NonNull List<UserInfo> users) {
+ // If any user state doesn't have a first install time, e.g., after an OTA,
+ // use the pre OTA firstInstallTime timestamp. This is because we migrated from per package
+ // firstInstallTime to per user-state. Without this, OTA can cause this info to be lost.
+ final ArrayMap<String, Long> originalFirstInstallTimes = new ArrayMap<>();
+
+ try {
+ if (!readSettingsLPw(computer, users, originalFirstInstallTimes)) {
+ return false;
+ }
} finally {
if (!mVersion.containsKey(StorageManager.UUID_PRIVATE_INTERNAL)) {
Slog.wtf(PackageManagerService.TAG,
@@ -4857,7 +4916,7 @@
pw.print(prefix); pw.println(" static library:");
pw.print(prefix); pw.print(" ");
pw.print("name:"); pw.print(pkg.getStaticSharedLibraryName());
- pw.print(" version:"); pw.println(pkg.getStaticSharedLibVersion());
+ pw.print(" version:"); pw.println(pkg.getStaticSharedLibraryVersion());
}
if (pkg.getSdkLibraryName() != null) {
diff --git a/services/core/java/com/android/server/pm/SharedLibrariesImpl.java b/services/core/java/com/android/server/pm/SharedLibrariesImpl.java
index 8c2b212..d2ce23e 100644
--- a/services/core/java/com/android/server/pm/SharedLibrariesImpl.java
+++ b/services/core/java/com/android/server/pm/SharedLibrariesImpl.java
@@ -408,7 +408,7 @@
final int versionCount = versionedLib.size();
for (int i = 0; i < versionCount; i++) {
final long libVersion = versionedLib.keyAt(i);
- if (libVersion < pkg.getStaticSharedLibVersion()) {
+ if (libVersion < pkg.getStaticSharedLibraryVersion()) {
previousLibVersion = Math.max(previousLibVersion, libVersion);
}
}
@@ -468,7 +468,7 @@
}
} else if (pkg.getStaticSharedLibraryName() != null) {
SharedLibraryInfo definedLibrary = getSharedLibraryInfo(
- pkg.getStaticSharedLibraryName(), pkg.getStaticSharedLibVersion());
+ pkg.getStaticSharedLibraryName(), pkg.getStaticSharedLibraryVersion());
if (definedLibrary != null) {
action.accept(definedLibrary, libInfo);
}
diff --git a/services/core/java/com/android/server/pm/SharedUserSetting.java b/services/core/java/com/android/server/pm/SharedUserSetting.java
index a7a4c4e..a037ae8 100644
--- a/services/core/java/com/android/server/pm/SharedUserSetting.java
+++ b/services/core/java/com/android/server/pm/SharedUserSetting.java
@@ -253,7 +253,7 @@
}
if (mDisabledPackages.size() == 1) {
final AndroidPackage pkg = mDisabledPackages.valueAt(0).getPkg();
- return pkg != null && pkg.isLeavingSharedUid();
+ return pkg != null && pkg.isLeavingSharedUser();
}
return true;
}
diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java
index 4fddc9c..0362ddd 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackage.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackage.java
@@ -1470,15 +1470,9 @@
}
// Then make sure none of the activities have more than the max number of shortcuts.
- int total = 0;
for (int i = counts.size() - 1; i >= 0; i--) {
- int count = counts.valueAt(i);
- service.enforceMaxActivityShortcuts(count);
- total += count;
+ service.enforceMaxActivityShortcuts(counts.valueAt(i));
}
-
- // Finally make sure that the app doesn't have more than the max number of shortcuts.
- service.enforceMaxAppShortcuts(total);
}
/**
diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java
index 12a33ee..83720f1 100644
--- a/services/core/java/com/android/server/pm/ShortcutService.java
+++ b/services/core/java/com/android/server/pm/ShortcutService.java
@@ -181,9 +181,6 @@
static final int DEFAULT_MAX_SHORTCUTS_PER_ACTIVITY = 15;
@VisibleForTesting
- static final int DEFAULT_MAX_SHORTCUTS_PER_APP = 60;
-
- @VisibleForTesting
static final int DEFAULT_MAX_ICON_DIMENSION_DP = 96;
@VisibleForTesting
@@ -260,11 +257,6 @@
String KEY_MAX_SHORTCUTS = "max_shortcuts";
/**
- * Key name for the max dynamic shortcuts per app. (int)
- */
- String KEY_MAX_SHORTCUTS_PER_APP = "max_shortcuts_per_app";
-
- /**
* Key name for icon compression quality, 0-100.
*/
String KEY_ICON_QUALITY = "icon_quality";
@@ -337,14 +329,9 @@
new SparseArray<>();
/**
- * Max number of dynamic + manifest shortcuts that each activity can have at a time.
- */
- private int mMaxShortcutsPerActivity;
-
- /**
* Max number of dynamic + manifest shortcuts that each application can have at a time.
*/
- private int mMaxShortcutsPerApp;
+ private int mMaxShortcuts;
/**
* Max number of updating API calls that each application can make during the interval.
@@ -817,12 +804,9 @@
mMaxUpdatesPerInterval = Math.max(0, (int) parser.getLong(
ConfigConstants.KEY_MAX_UPDATES_PER_INTERVAL, DEFAULT_MAX_UPDATES_PER_INTERVAL));
- mMaxShortcutsPerActivity = Math.max(0, (int) parser.getLong(
+ mMaxShortcuts = Math.max(0, (int) parser.getLong(
ConfigConstants.KEY_MAX_SHORTCUTS, DEFAULT_MAX_SHORTCUTS_PER_ACTIVITY));
- mMaxShortcutsPerApp = Math.max(0, (int) parser.getLong(
- ConfigConstants.KEY_MAX_SHORTCUTS_PER_APP, DEFAULT_MAX_SHORTCUTS_PER_APP));
-
final int iconDimensionDp = Math.max(1, injectIsLowRamDevice()
? (int) parser.getLong(
ConfigConstants.KEY_MAX_ICON_DIMENSION_DP_LOWRAM,
@@ -1762,33 +1746,16 @@
* {@link #getMaxActivityShortcuts()}.
*/
void enforceMaxActivityShortcuts(int numShortcuts) {
- if (numShortcuts > mMaxShortcutsPerActivity) {
+ if (numShortcuts > mMaxShortcuts) {
throw new IllegalArgumentException("Max number of dynamic shortcuts exceeded");
}
}
/**
- * @throws IllegalArgumentException if {@code numShortcuts} is bigger than
- * {@link #getMaxAppShortcuts()}.
- */
- void enforceMaxAppShortcuts(int numShortcuts) {
- if (numShortcuts > mMaxShortcutsPerApp) {
- throw new IllegalArgumentException("Max number of dynamic shortcuts per app exceeded");
- }
- }
-
- /**
* Return the max number of dynamic + manifest shortcuts for each launcher icon.
*/
int getMaxActivityShortcuts() {
- return mMaxShortcutsPerActivity;
- }
-
- /**
- * Return the max number of dynamic + manifest shortcuts for each launcher icon.
- */
- int getMaxAppShortcuts() {
- return mMaxShortcutsPerApp;
+ return mMaxShortcuts;
}
/**
@@ -2221,8 +2188,6 @@
ps.ensureNotImmutable(shortcut.getId(), /*ignoreInvisible=*/ true);
fillInDefaultActivity(Arrays.asList(shortcut));
- enforceMaxAppShortcuts(ps.getShortcutCount());
-
if (!shortcut.hasRank()) {
shortcut.setRank(0);
}
@@ -2610,7 +2575,7 @@
throws RemoteException {
verifyCaller(packageName, userId);
- return mMaxShortcutsPerActivity;
+ return mMaxShortcuts;
}
@Override
@@ -4759,7 +4724,7 @@
pw.print(" maxUpdatesPerInterval: ");
pw.println(mMaxUpdatesPerInterval);
pw.print(" maxShortcutsPerActivity: ");
- pw.println(mMaxShortcutsPerActivity);
+ pw.println(mMaxShortcuts);
pw.println();
mStatLogger.dump(pw, " ");
@@ -5246,7 +5211,7 @@
@VisibleForTesting
int getMaxShortcutsForTest() {
- return mMaxShortcutsPerActivity;
+ return mMaxShortcuts;
}
@VisibleForTesting
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index b8f8c3a..6bac905 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -1473,6 +1473,27 @@
}
}
+ @Override
+ public void revokeUserAdmin(@UserIdInt int userId) {
+ checkManageUserAndAcrossUsersFullPermission("revoke admin privileges");
+
+ synchronized (mPackagesLock) {
+ UserInfo info;
+ synchronized (mUsersLock) {
+ info = getUserInfoLU(userId);
+ }
+ if (info == null || !info.isAdmin()) {
+ // Exit if no user found with that id, or the user is not an Admin.
+ return;
+ }
+
+ info.flags ^= UserInfo.FLAG_ADMIN;
+ synchronized (mUsersLock) {
+ writeUserLP(getUserDataLU(info.id));
+ }
+ }
+ }
+
/**
* Evicts a user's CE key by stopping and restarting the user.
*
diff --git a/services/core/java/com/android/server/pm/UserVisibilityMediator.java b/services/core/java/com/android/server/pm/UserVisibilityMediator.java
index 40d87bc..d8e4dac 100644
--- a/services/core/java/com/android/server/pm/UserVisibilityMediator.java
+++ b/services/core/java/com/android/server/pm/UserVisibilityMediator.java
@@ -100,20 +100,20 @@
private final Object mLock = new Object();
- private final boolean mUsersOnSecondaryDisplaysEnabled;
+ private final boolean mVisibleBackgroundUsersEnabled;
@UserIdInt
@GuardedBy("mLock")
private int mCurrentUserId = INITIAL_CURRENT_USER_ID;
/**
- * Map of background users started on secondary displays.
+ * Map of background users started visible on displays (key is user id, value is display id).
*
* <p>Only set when {@code mUsersOnSecondaryDisplaysEnabled} is {@code true}.
*/
@Nullable
@GuardedBy("mLock")
- private final SparseIntArray mUsersOnSecondaryDisplays;
+ private final SparseIntArray mUsersOnDisplaysMap;
/**
* Mapping from each started user to its profile group.
@@ -131,13 +131,13 @@
new CopyOnWriteArrayList<>();
UserVisibilityMediator(Handler handler) {
- this(UserManager.isUsersOnSecondaryDisplaysEnabled(), handler);
+ this(UserManager.isVisibleBackgroundUsersEnabled(), handler);
}
@VisibleForTesting
- UserVisibilityMediator(boolean usersOnSecondaryDisplaysEnabled, Handler handler) {
- mUsersOnSecondaryDisplaysEnabled = usersOnSecondaryDisplaysEnabled;
- mUsersOnSecondaryDisplays = mUsersOnSecondaryDisplaysEnabled ? new SparseIntArray() : null;
+ UserVisibilityMediator(boolean backgroundUsersOnDisplaysEnabled, Handler handler) {
+ mVisibleBackgroundUsersEnabled = backgroundUsersOnDisplaysEnabled;
+ mUsersOnDisplaysMap = mVisibleBackgroundUsersEnabled ? new SparseIntArray() : null;
mHandler = handler;
// TODO(b/242195409): might need to change this if boot logic is refactored for HSUM devices
mStartedProfileGroupIds.put(INITIAL_CURRENT_USER_ID, INITIAL_CURRENT_USER_ID);
@@ -207,7 +207,7 @@
if (DBG) {
Slogf.d(TAG, "adding user / display mapping (%d -> %d)", userId, displayId);
}
- mUsersOnSecondaryDisplays.put(userId, displayId);
+ mUsersOnDisplaysMap.put(userId, displayId);
break;
case SECONDARY_DISPLAY_MAPPING_NOT_NEEDED:
if (DBG) {
@@ -260,7 +260,7 @@
foreground, displayId);
return USER_ASSIGNMENT_RESULT_FAILURE;
}
- if (!mUsersOnSecondaryDisplaysEnabled) {
+ if (!mVisibleBackgroundUsersEnabled) {
Slogf.w(TAG, "getUserVisibilityOnStartLocked(%d, %d, %b, %d) failed: called on "
+ "device that doesn't support multiple users on multiple displays",
userId, profileGroupId, foreground, displayId);
@@ -300,7 +300,7 @@
private int canAssignUserToDisplayLocked(@UserIdInt int userId,
@UserIdInt int profileGroupId, int displayId) {
if (displayId == DEFAULT_DISPLAY
- && (!mUsersOnSecondaryDisplaysEnabled || !isProfile(userId, profileGroupId))) {
+ && (!mVisibleBackgroundUsersEnabled || !isProfile(userId, profileGroupId))) {
// Don't need to do anything because methods (such as isUserVisible()) already
// know that the current user (and its profiles) is assigned to the default display.
// But on MUMD devices, profiles are only supported in the default display, so it
@@ -341,9 +341,9 @@
}
// Check if display is available
- for (int i = 0; i < mUsersOnSecondaryDisplays.size(); i++) {
- int assignedUserId = mUsersOnSecondaryDisplays.keyAt(i);
- int assignedDisplayId = mUsersOnSecondaryDisplays.valueAt(i);
+ for (int i = 0; i < mUsersOnDisplaysMap.size(); i++) {
+ int assignedUserId = mUsersOnDisplaysMap.keyAt(i);
+ int assignedDisplayId = mUsersOnDisplaysMap.valueAt(i);
if (DBG) {
Slogf.d(TAG, "%d: assignedUserId=%d, assignedDisplayId=%d",
i, assignedUserId, assignedDisplayId);
@@ -388,7 +388,7 @@
}
mStartedProfileGroupIds.delete(userId);
- if (!mUsersOnSecondaryDisplaysEnabled) {
+ if (!mVisibleBackgroundUsersEnabled) {
// Don't need to do update mUsersOnSecondaryDisplays because methods (such as
// isUserVisible()) already know that the current user (and their profiles) is
// assigned to the default display.
@@ -396,9 +396,9 @@
}
if (DBG) {
Slogf.d(TAG, "Removing %d from mUsersOnSecondaryDisplays (%s)", userId,
- mUsersOnSecondaryDisplays);
+ mUsersOnDisplaysMap);
}
- mUsersOnSecondaryDisplays.delete(userId);
+ mUsersOnDisplaysMap.delete(userId);
}
/**
@@ -415,7 +415,7 @@
// Device doesn't support multiple users on multiple displays, so only users checked above
// can be visible
- if (!mUsersOnSecondaryDisplaysEnabled) {
+ if (!mVisibleBackgroundUsersEnabled) {
if (DBG) {
Slogf.d(TAG, "isUserVisible(%d): false for non-current user on MUMD", userId);
}
@@ -424,7 +424,7 @@
boolean visible;
synchronized (mLock) {
- visible = mUsersOnSecondaryDisplays.indexOfKey(userId) >= 0;
+ visible = mUsersOnDisplaysMap.indexOfKey(userId) >= 0;
}
if (DBG) {
Slogf.d(TAG, "isUserVisible(%d): %b from mapping", userId, visible);
@@ -440,7 +440,7 @@
return false;
}
- if (!mUsersOnSecondaryDisplaysEnabled || displayId == Display.DEFAULT_DISPLAY) {
+ if (!mVisibleBackgroundUsersEnabled || displayId == Display.DEFAULT_DISPLAY) {
// TODO(b/245939659): will need to move the displayId == Display.DEFAULT_DISPLAY outside
// once it supports background users on DEFAULT_DISPLAY (for example, passengers in a
// no-driver configuration)
@@ -448,7 +448,7 @@
}
synchronized (mLock) {
- return mUsersOnSecondaryDisplays.get(userId, Display.INVALID_DISPLAY) == displayId;
+ return mUsersOnDisplaysMap.get(userId, Display.INVALID_DISPLAY) == displayId;
}
}
@@ -460,12 +460,12 @@
return Display.DEFAULT_DISPLAY;
}
- if (!mUsersOnSecondaryDisplaysEnabled) {
+ if (!mVisibleBackgroundUsersEnabled) {
return Display.INVALID_DISPLAY;
}
synchronized (mLock) {
- return mUsersOnSecondaryDisplays.get(userId, Display.INVALID_DISPLAY);
+ return mUsersOnDisplaysMap.get(userId, Display.INVALID_DISPLAY);
}
}
@@ -473,16 +473,16 @@
* See {@link UserManagerInternal#getUserAssignedToDisplay(int)}.
*/
public int getUserAssignedToDisplay(@UserIdInt int displayId) {
- if (displayId == Display.DEFAULT_DISPLAY || !mUsersOnSecondaryDisplaysEnabled) {
+ if (displayId == Display.DEFAULT_DISPLAY || !mVisibleBackgroundUsersEnabled) {
return getCurrentUserId();
}
synchronized (mLock) {
- for (int i = 0; i < mUsersOnSecondaryDisplays.size(); i++) {
- if (mUsersOnSecondaryDisplays.valueAt(i) != displayId) {
+ for (int i = 0; i < mUsersOnDisplaysMap.size(); i++) {
+ if (mUsersOnDisplaysMap.valueAt(i) != displayId) {
continue;
}
- int userId = mUsersOnSecondaryDisplays.keyAt(i);
+ int userId = mUsersOnDisplaysMap.keyAt(i);
if (!isStartedProfile(userId)) {
return userId;
} else if (DBG) {
@@ -615,12 +615,11 @@
dumpSparseIntArray(ipw, mStartedProfileGroupIds, "started user / profile group",
"u", "pg");
- ipw.print("Supports background users on secondary displays: ");
- ipw.println(mUsersOnSecondaryDisplaysEnabled);
+ ipw.print("Supports visible background users on displays: ");
+ ipw.println(mVisibleBackgroundUsersEnabled);
- if (mUsersOnSecondaryDisplays != null) {
- dumpSparseIntArray(ipw, mUsersOnSecondaryDisplays,
- "background user / secondary display", "u", "d");
+ if (mUsersOnDisplaysMap != null) {
+ dumpSparseIntArray(ipw, mUsersOnDisplaysMap, "user / display", "u", "d");
}
int numberListeners = mListeners.size();
ipw.print("Number of listeners: ");
diff --git a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java
index 87805e0..ff993ea 100644
--- a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java
+++ b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java
@@ -131,7 +131,7 @@
info.splitRevisionCodes = pkg.getSplitRevisionCodes();
info.versionName = pkg.getVersionName();
info.sharedUserId = pkg.getSharedUserId();
- info.sharedUserLabel = pkg.getSharedUserLabel();
+ info.sharedUserLabel = pkg.getSharedUserLabelRes();
info.applicationInfo = applicationInfo;
info.installLocation = pkg.getInstallLocation();
if ((info.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0
@@ -218,7 +218,7 @@
}
}
}
- if (pkg.areAttributionsUserVisible()) {
+ if (pkg.isAttributionsUserVisible()) {
info.applicationInfo.privateFlagsExt
|= ApplicationInfo.PRIVATE_FLAG_EXT_ATTRIBUTIONS_ARE_USER_VISIBLE;
} else {
@@ -869,7 +869,7 @@
public static int appInfoFlags(AndroidPackage pkg, @Nullable PackageStateInternal pkgSetting) {
// @formatter:off
int pkgWithoutStateFlags = flag(pkg.isExternalStorage(), ApplicationInfo.FLAG_EXTERNAL_STORAGE)
- | flag(pkg.isBaseHardwareAccelerated(), ApplicationInfo.FLAG_HARDWARE_ACCELERATED)
+ | flag(pkg.isHardwareAccelerated(), ApplicationInfo.FLAG_HARDWARE_ACCELERATED)
| flag(pkg.isAllowBackup(), ApplicationInfo.FLAG_ALLOW_BACKUP)
| flag(pkg.isKillAfterRestore(), ApplicationInfo.FLAG_KILL_AFTER_RESTORE)
| flag(pkg.isRestoreAnyVersion(), ApplicationInfo.FLAG_RESTORE_ANY_VERSION)
@@ -972,7 +972,7 @@
// @formatter:off
int pkgWithoutStateFlags = flag(pkg.isProfileable(), ApplicationInfo.PRIVATE_FLAG_EXT_PROFILEABLE)
| flag(pkg.hasRequestForegroundServiceExemption(), ApplicationInfo.PRIVATE_FLAG_EXT_REQUEST_FOREGROUND_SERVICE_EXEMPTION)
- | flag(pkg.areAttributionsUserVisible(), ApplicationInfo.PRIVATE_FLAG_EXT_ATTRIBUTIONS_ARE_USER_VISIBLE)
+ | flag(pkg.isAttributionsUserVisible(), ApplicationInfo.PRIVATE_FLAG_EXT_ATTRIBUTIONS_ARE_USER_VISIBLE)
| flag(pkg.isOnBackInvokedCallbackEnabled(), ApplicationInfo.PRIVATE_FLAG_EXT_ENABLE_ON_BACK_INVOKED_CALLBACK)
| flag(isAllowlistedForHiddenApis, ApplicationInfo.PRIVATE_FLAG_EXT_ALLOWLISTED_FOR_HIDDEN_APIS);
return appInfoPrivateFlagsExt(pkgWithoutStateFlags, pkgSetting);
diff --git a/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageUtils.java b/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageUtils.java
index 4fee84f..f3ee531 100644
--- a/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageUtils.java
+++ b/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageUtils.java
@@ -103,7 +103,7 @@
return new SharedLibraryInfo(null, pkg.getPackageName(),
AndroidPackageUtils.getAllCodePaths(pkg),
pkg.getStaticSharedLibraryName(),
- pkg.getStaticSharedLibVersion(),
+ pkg.getStaticSharedLibraryVersion(),
SharedLibraryInfo.TYPE_STATIC,
new VersionedPackage(pkg.getManifestPackageName(),
pkg.getLongVersionCode()),
diff --git a/services/core/java/com/android/server/pm/parsing/pkg/PackageImpl.java b/services/core/java/com/android/server/pm/parsing/pkg/PackageImpl.java
index ba36ab7..e361c93 100644
--- a/services/core/java/com/android/server/pm/parsing/pkg/PackageImpl.java
+++ b/services/core/java/com/android/server/pm/parsing/pkg/PackageImpl.java
@@ -717,7 +717,7 @@
}
@Override
- public boolean areAttributionsUserVisible() {
+ public boolean isAttributionsUserVisible() {
return getBoolean(Booleans.ATTRIBUTIONS_ARE_USER_VISIBLE);
}
@@ -869,7 +869,7 @@
}
@Override
- public int getBanner() {
+ public int getBannerRes() {
return banner;
}
@@ -897,7 +897,7 @@
@Nullable
@Override
- public String getClassName() {
+ public String getApplicationClassName() {
return className;
}
@@ -924,7 +924,7 @@
}
@Override
- public int getDataExtractionRules() {
+ public int getDataExtractionRulesRes() {
return dataExtractionRules;
}
@@ -940,7 +940,7 @@
}
@Override
- public int getFullBackupContent() {
+ public int getFullBackupContentRes() {
return fullBackupContent;
}
@@ -1006,7 +1006,7 @@
}
@Override
- public int getLogo() {
+ public int getLogoRes() {
return logo;
}
@@ -1277,7 +1277,7 @@
}
@Override
- public int getSharedUserLabel() {
+ public int getSharedUserLabelRes() {
return sharedUserLabel;
}
@@ -1330,7 +1330,7 @@
}
@Override
- public long getStaticSharedLibVersion() {
+ public long getStaticSharedLibraryVersion() {
return staticSharedLibVersion;
}
@@ -1356,7 +1356,7 @@
}
@Override
- public int getTheme() {
+ public int getThemeRes() {
return theme;
}
@@ -1519,8 +1519,8 @@
}
@Override
- public boolean isBaseHardwareAccelerated() {
- return getBoolean(Booleans.BASE_HARDWARE_ACCELERATED);
+ public boolean isHardwareAccelerated() {
+ return getBoolean(Booleans.HARDWARE_ACCELERATED);
}
@Override
@@ -1609,7 +1609,7 @@
}
@Override
- public boolean isLeavingSharedUid() {
+ public boolean isLeavingSharedUser() {
return getBoolean(Booleans.LEAVING_SHARED_UID);
}
@@ -1840,14 +1840,14 @@
}
@Override
- public PackageImpl setBanner(int value) {
+ public PackageImpl setBannerRes(int value) {
banner = value;
return this;
}
@Override
- public PackageImpl setBaseHardwareAccelerated(boolean value) {
- return setBoolean(Booleans.BASE_HARDWARE_ACCELERATED, value);
+ public PackageImpl setHardwareAccelerated(boolean value) {
+ return setBoolean(Booleans.HARDWARE_ACCELERATED, value);
}
@Override
@@ -1874,7 +1874,7 @@
}
@Override
- public PackageImpl setClassName(@Nullable String className) {
+ public PackageImpl setApplicationClassName(@Nullable String className) {
this.className = className == null ? null : className.trim();
return this;
}
@@ -1903,7 +1903,7 @@
}
@Override
- public PackageImpl setDataExtractionRules(int value) {
+ public PackageImpl setDataExtractionRulesRes(int value) {
dataExtractionRules = value;
return this;
}
@@ -1940,7 +1940,7 @@
}
@Override
- public PackageImpl setFullBackupContent(int value) {
+ public PackageImpl setFullBackupContentRes(int value) {
fullBackupContent = value;
return this;
}
@@ -2022,7 +2022,7 @@
}
@Override
- public PackageImpl setLeavingSharedUid(boolean value) {
+ public PackageImpl setLeavingSharedUser(boolean value) {
return setBoolean(Booleans.LEAVING_SHARED_UID, value);
}
@@ -2033,7 +2033,7 @@
}
@Override
- public PackageImpl setLogo(int value) {
+ public PackageImpl setLogoRes(int value) {
logo = value;
return this;
}
@@ -2292,7 +2292,7 @@
}
@Override
- public PackageImpl setSharedUserLabel(int value) {
+ public PackageImpl setSharedUserLabelRes(int value) {
sharedUserLabel = value;
return this;
}
@@ -2318,7 +2318,7 @@
}
@Override
- public PackageImpl setStaticSharedLibVersion(long value) {
+ public PackageImpl setStaticSharedLibraryVersion(long value) {
staticSharedLibVersion = value;
return this;
}
@@ -2397,7 +2397,7 @@
}
@Override
- public PackageImpl setTheme(int value) {
+ public PackageImpl setThemeRes(int value) {
theme = value;
return this;
}
@@ -3529,7 +3529,7 @@
private static class Booleans {
@LongDef({
EXTERNAL_STORAGE,
- BASE_HARDWARE_ACCELERATED,
+ HARDWARE_ACCELERATED,
ALLOW_BACKUP,
KILL_AFTER_RESTORE,
RESTORE_ANY_VERSION,
@@ -3593,7 +3593,7 @@
public @interface Flags {}
private static final long EXTERNAL_STORAGE = 1L;
- private static final long BASE_HARDWARE_ACCELERATED = 1L << 1;
+ private static final long HARDWARE_ACCELERATED = 1L << 1;
private static final long ALLOW_BACKUP = 1L << 2;
private static final long KILL_AFTER_RESTORE = 1L << 3;
private static final long RESTORE_ANY_VERSION = 1L << 4;
diff --git a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
index 83e17a5..58f88c3 100644
--- a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
+++ b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
@@ -1800,6 +1800,15 @@
PermissionState permState;
if (permIdx >= 0) {
permState = uidState.valueAt(permIdx);
+ // Quick and dirty fix for shared UID packages - we should grant permission with the
+ // correct package even if a previous checkPermission() used a package that isn't
+ // requesting the permission. Ideally we should use package manager snapshot and get
+ // rid of this entire inner class.
+ if (!ArrayUtils.contains(permState.mPkgRequestingPerm.requestedPermissions,
+ permission) && ArrayUtils.contains(pkg.requestedPermissions,
+ permission)) {
+ permState.mPkgRequestingPerm = pkg;
+ }
} else {
permState = new PermissionState(permission, pkg, user);
uidState.put(permission, permState);
@@ -1887,7 +1896,7 @@
*/
private class PermissionState {
private final @NonNull String mPermission;
- private final @NonNull PackageInfo mPkgRequestingPerm;
+ private @NonNull PackageInfo mPkgRequestingPerm;
private final @NonNull UserHandle mUser;
/** Permission flags when the state was created */
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
index a949f75..268a36f 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
@@ -119,7 +119,7 @@
private final AppOpsManager mAppOpsManager;
private final Context mContext;
- private final PermissionManagerServiceImpl mPermissionManagerServiceImpl;
+ private final PermissionManagerServiceInterface mPermissionManagerServiceImpl;
@NonNull
private final AttributionSourceRegistry mAttributionSourceRegistry;
@@ -152,6 +152,8 @@
mPermissionManagerServiceImpl = new PermissionManagerServiceImpl(context,
availableFeatures);
+ //mPermissionManagerServiceImpl = new PermissionManagerServiceLoggingDecorator(
+ // LocalServices.getService(PermissionManagerServiceInterface.class));
}
/**
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceLoggingDecorator.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceLoggingDecorator.java
new file mode 100644
index 0000000..bfe0008
--- /dev/null
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceLoggingDecorator.java
@@ -0,0 +1,433 @@
+/*
+ * 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.permission;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.pm.PermissionGroupInfo;
+import android.content.pm.PermissionInfo;
+import android.content.pm.permission.SplitPermissionInfoParcelable;
+import android.permission.IOnPermissionsChangeListener;
+import android.util.Log;
+
+import com.android.server.pm.pkg.AndroidPackage;
+import com.android.server.pm.pkg.PackageState;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Logging decorator for {@link PermissionManagerServiceInterface}.
+ */
+public class PermissionManagerServiceLoggingDecorator implements PermissionManagerServiceInterface {
+ private static final String LOG_TAG =
+ PermissionManagerServiceLoggingDecorator.class.getSimpleName();
+
+ @NonNull
+ private final PermissionManagerServiceInterface mService;
+
+ public PermissionManagerServiceLoggingDecorator(
+ @NonNull PermissionManagerServiceInterface service
+ ) {
+ mService = service;
+ }
+
+ @Nullable
+ @Override
+ public byte[] backupRuntimePermissions(int userId) {
+ Log.i(LOG_TAG, "backupRuntimePermissions(userId = " + userId + ")");
+ return mService.backupRuntimePermissions(userId);
+ }
+
+ @Override
+ @SuppressWarnings("ArrayToString")
+ public void restoreRuntimePermissions(@NonNull byte[] backup, int userId) {
+ Log.i(LOG_TAG, "restoreRuntimePermissions(backup = " + backup + ", userId = " + userId
+ + ")");
+ mService.restoreRuntimePermissions(backup, userId);
+ }
+
+ @Override
+ public void restoreDelayedRuntimePermissions(@NonNull String packageName, int userId) {
+ Log.i(LOG_TAG, "restoreDelayedRuntimePermissions(packageName = " + packageName
+ + ", userId = " + userId + ")");
+ mService.restoreDelayedRuntimePermissions(packageName, userId);
+ }
+
+ @Override
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ Log.i(LOG_TAG, "dump(fd = " + fd + ", pw = " + pw + ", args = " + Arrays.toString(args)
+ + ")");
+ mService.dump(fd, pw, args);
+ }
+
+ @Override
+ public List<PermissionGroupInfo> getAllPermissionGroups(int flags) {
+ Log.i(LOG_TAG, "getAllPermissionGroups(flags = " + flags + ")");
+ return mService.getAllPermissionGroups(flags);
+ }
+
+ @Override
+ public PermissionGroupInfo getPermissionGroupInfo(String groupName, int flags) {
+ Log.i(LOG_TAG, "getPermissionGroupInfo(groupName = " + groupName + ", flags = " + flags
+ + ")");
+ return mService.getPermissionGroupInfo(groupName, flags);
+ }
+
+ @Override
+ public PermissionInfo getPermissionInfo(@NonNull String permName, int flags,
+ @NonNull String opPackageName) {
+ Log.i(LOG_TAG, "getPermissionInfo(permName = " + permName + ", flags = " + flags
+ + ", opPackageName = " + opPackageName + ")");
+ return mService.getPermissionInfo(permName, flags, opPackageName);
+ }
+
+ @Override
+ public List<PermissionInfo> queryPermissionsByGroup(String groupName, int flags) {
+ Log.i(LOG_TAG, "queryPermissionsByGroup(groupName = " + groupName + ", flags = " + flags
+ + ")");
+ return mService.queryPermissionsByGroup(groupName, flags);
+ }
+
+ @Override
+ public boolean addPermission(PermissionInfo info, boolean async) {
+ Log.i(LOG_TAG, "addPermission(info = " + info + ", async = " + async + ")");
+ return mService.addPermission(info, async);
+ }
+
+ @Override
+ public void removePermission(String permName) {
+ Log.i(LOG_TAG, "removePermission(permName = " + permName + ")");
+ mService.removePermission(permName);
+ }
+
+ @Override
+ public int getPermissionFlags(String packageName, String permName, int userId) {
+ Log.i(LOG_TAG, "getPermissionFlags(packageName = " + packageName + ", permName = "
+ + permName + ", userId = " + userId + ")");
+ return mService.getPermissionFlags(packageName, permName, userId);
+ }
+
+ @Override
+ public void updatePermissionFlags(String packageName, String permName, int flagMask,
+ int flagValues, boolean checkAdjustPolicyFlagPermission, int userId) {
+ Log.i(LOG_TAG, "updatePermissionFlags(packageName = " + packageName + ", permName = "
+ + permName + ", flagMask = " + flagMask + ", flagValues = " + flagValues
+ + ", checkAdjustPolicyFlagPermission = " + checkAdjustPolicyFlagPermission
+ + ", userId = " + userId + ")");
+ mService.updatePermissionFlags(packageName, permName, flagMask, flagValues,
+ checkAdjustPolicyFlagPermission, userId);
+ }
+
+ @Override
+ public void updatePermissionFlagsForAllApps(int flagMask, int flagValues, int userId) {
+ Log.i(LOG_TAG, "updatePermissionFlagsForAllApps(flagMask = " + flagMask + ", flagValues = "
+ + flagValues + ", userId = " + userId + ")");
+ mService.updatePermissionFlagsForAllApps(flagMask, flagValues, userId);
+ }
+
+ @Override
+ public void addOnPermissionsChangeListener(IOnPermissionsChangeListener listener) {
+ Log.i(LOG_TAG, "addOnPermissionsChangeListener(listener = " + listener + ")");
+ mService.addOnPermissionsChangeListener(listener);
+ }
+
+ @Override
+ public void removeOnPermissionsChangeListener(
+ IOnPermissionsChangeListener listener) {
+ Log.i(LOG_TAG, "removeOnPermissionsChangeListener(listener = " + listener + ")");
+ mService.removeOnPermissionsChangeListener(listener);
+ }
+
+ @Override
+ public boolean addAllowlistedRestrictedPermission(@NonNull String packageName,
+ @NonNull String permName, int flags, int userId) {
+ Log.i(LOG_TAG, "addAllowlistedRestrictedPermission(packageName = " + packageName
+ + ", permName = " + permName + ", flags = " + flags + ", userId = " + userId + ")");
+ return mService.addAllowlistedRestrictedPermission(packageName, permName, flags, userId);
+ }
+
+ @Override
+ public List<String> getAllowlistedRestrictedPermissions(@NonNull String packageName, int flags,
+ int userId) {
+ Log.i(LOG_TAG, "getAllowlistedRestrictedPermissions(packageName = " + packageName
+ + ", flags = " + flags + ", userId = " + userId + ")");
+ return mService.getAllowlistedRestrictedPermissions(packageName, flags, userId);
+ }
+
+ @Override
+ public boolean removeAllowlistedRestrictedPermission(@NonNull String packageName,
+ @NonNull String permName, int flags, int userId) {
+ Log.i(LOG_TAG, "removeAllowlistedRestrictedPermission(packageName = " + packageName
+ + ", permName = " + permName + ", flags = " + flags + ", userId = " + userId + ")");
+ return mService.removeAllowlistedRestrictedPermission(packageName, permName, flags, userId);
+ }
+
+ @Override
+ public void grantRuntimePermission(String packageName, String permName, int userId) {
+ Log.i(LOG_TAG, "grantRuntimePermission(packageName = " + packageName + ", permName = "
+ + permName + ", userId = " + userId + ")");
+ mService.grantRuntimePermission(packageName, permName, userId);
+ }
+
+ @Override
+ public void revokeRuntimePermission(String packageName, String permName, int userId,
+ String reason) {
+ Log.i(LOG_TAG, "revokeRuntimePermission(packageName = " + packageName + ", permName = "
+ + permName + ", userId = " + userId + ", reason = " + reason + ")");
+ mService.revokeRuntimePermission(packageName, permName, userId, reason);
+ }
+
+ @Override
+ public void revokePostNotificationPermissionWithoutKillForTest(String packageName, int userId) {
+ Log.i(LOG_TAG, "revokePostNotificationPermissionWithoutKillForTest(packageName = "
+ + packageName + ", userId = " + userId + ")");
+ mService.revokePostNotificationPermissionWithoutKillForTest(packageName, userId);
+ }
+
+ @Override
+ public boolean shouldShowRequestPermissionRationale(String packageName, String permName,
+ int userId) {
+ Log.i(LOG_TAG, "shouldShowRequestPermissionRationale(packageName = " + packageName
+ + ", permName = " + permName + ", userId = " + userId + ")");
+ return mService.shouldShowRequestPermissionRationale(packageName, permName, userId);
+ }
+
+ @Override
+ public boolean isPermissionRevokedByPolicy(String packageName, String permName, int userId) {
+ Log.i(LOG_TAG, "isPermissionRevokedByPolicy(packageName = " + packageName + ", permName = "
+ + permName + ", userId = " + userId + ")");
+ return mService.isPermissionRevokedByPolicy(packageName, permName, userId);
+ }
+
+ @Override
+ public List<SplitPermissionInfoParcelable> getSplitPermissions() {
+ Log.i(LOG_TAG, "getSplitPermissions()");
+ return mService.getSplitPermissions();
+ }
+
+ @Override
+ public int checkPermission(String pkgName, String permName, int userId) {
+ Log.i(LOG_TAG, "checkPermission(pkgName = " + pkgName + ", permName = " + permName
+ + ", userId = " + userId + ")");
+ return mService.checkPermission(pkgName, permName, userId);
+ }
+
+ @Override
+ public int checkUidPermission(int uid, String permName) {
+ Log.i(LOG_TAG, "checkUidPermission(uid = " + uid + ", permName = " + permName + ")");
+ return mService.checkUidPermission(uid, permName);
+ }
+
+ @Override
+ public void addOnRuntimePermissionStateChangedListener(
+ @NonNull PermissionManagerServiceInternal
+ .OnRuntimePermissionStateChangedListener listener) {
+ Log.i(LOG_TAG, "addOnRuntimePermissionStateChangedListener(listener = " + listener + ")");
+ mService.addOnRuntimePermissionStateChangedListener(listener);
+ }
+
+ @Override
+ public void removeOnRuntimePermissionStateChangedListener(
+ @NonNull PermissionManagerServiceInternal
+ .OnRuntimePermissionStateChangedListener listener) {
+ Log.i(LOG_TAG, "removeOnRuntimePermissionStateChangedListener(listener = " + listener
+ + ")");
+ mService.removeOnRuntimePermissionStateChangedListener(listener);
+ }
+
+ @Override
+ public Map<String, Set<String>> getAllAppOpPermissionPackages() {
+ Log.i(LOG_TAG, "getAllAppOpPermissionPackages()");
+ return mService.getAllAppOpPermissionPackages();
+ }
+
+ @Override
+ public boolean isPermissionsReviewRequired(@NonNull String packageName, int userId) {
+ Log.i(LOG_TAG, "isPermissionsReviewRequired(packageName = " + packageName + ", userId = "
+ + userId + ")");
+ return mService.isPermissionsReviewRequired(packageName, userId);
+ }
+
+ @Override
+ public void resetRuntimePermissions(@NonNull AndroidPackage pkg, int userId) {
+ Log.i(LOG_TAG, "resetRuntimePermissions(pkg = " + pkg + ", userId = " + userId + ")");
+ mService.resetRuntimePermissions(pkg, userId);
+ }
+
+ @Override
+ public void resetRuntimePermissionsForUser(int userId) {
+ Log.i(LOG_TAG, "resetRuntimePermissionsForUser(userId = " + userId + ")");
+ mService.resetRuntimePermissionsForUser(userId);
+ }
+
+ @Override
+ public void readLegacyPermissionStateTEMP() {
+ Log.i(LOG_TAG, "readLegacyPermissionStateTEMP()");
+ mService.readLegacyPermissionStateTEMP();
+ }
+
+ @Override
+ public void writeLegacyPermissionStateTEMP() {
+ Log.i(LOG_TAG, "writeLegacyPermissionStateTEMP()");
+ mService.writeLegacyPermissionStateTEMP();
+ }
+
+ @NonNull
+ @Override
+ public Set<String> getGrantedPermissions(@NonNull String packageName, int userId) {
+ Log.i(LOG_TAG, "getGrantedPermissions(packageName = " + packageName + ", userId = "
+ + userId + ")");
+ return mService.getGrantedPermissions(packageName, userId);
+ }
+
+ @NonNull
+ @Override
+ public int[] getPermissionGids(@NonNull String permissionName, int userId) {
+ Log.i(LOG_TAG, "getPermissionGids(permissionName = " + permissionName + ", userId = "
+ + userId + ")");
+ return mService.getPermissionGids(permissionName, userId);
+ }
+
+ @NonNull
+ @Override
+ public String[] getAppOpPermissionPackages(@NonNull String permissionName) {
+ Log.i(LOG_TAG, "getAppOpPermissionPackages(permissionName = " + permissionName + ")");
+ return mService.getAppOpPermissionPackages(permissionName);
+ }
+
+ @Nullable
+ @Override
+ public Permission getPermissionTEMP(@NonNull String permName) {
+ Log.i(LOG_TAG, "getPermissionTEMP(permName = " + permName + ")");
+ return mService.getPermissionTEMP(permName);
+ }
+
+ @NonNull
+ @Override
+ public List<PermissionInfo> getAllPermissionsWithProtection(int protection) {
+ Log.i(LOG_TAG, "getAllPermissionsWithProtection(protection = " + protection + ")");
+ return mService.getAllPermissionsWithProtection(protection);
+ }
+
+ @NonNull
+ @Override
+ public List<PermissionInfo> getAllPermissionsWithProtectionFlags(int protectionFlags) {
+ Log.i(LOG_TAG, "getAllPermissionsWithProtectionFlags(protectionFlags = " + protectionFlags
+ + ")");
+ return mService.getAllPermissionsWithProtectionFlags(protectionFlags);
+ }
+
+ @NonNull
+ @Override
+ public List<LegacyPermission> getLegacyPermissions() {
+ Log.i(LOG_TAG, "getLegacyPermissions()");
+ return mService.getLegacyPermissions();
+ }
+
+ @NonNull
+ @Override
+ public LegacyPermissionState getLegacyPermissionState(int appId) {
+ Log.i(LOG_TAG, "getLegacyPermissionState(appId = " + appId + ")");
+ return mService.getLegacyPermissionState(appId);
+ }
+
+ @Override
+ public void readLegacyPermissionsTEMP(
+ @NonNull LegacyPermissionSettings legacyPermissionSettings) {
+ Log.i(LOG_TAG, "readLegacyPermissionsTEMP(legacyPermissionSettings = "
+ + legacyPermissionSettings + ")");
+ mService.readLegacyPermissionsTEMP(legacyPermissionSettings);
+ }
+
+ @Override
+ public void writeLegacyPermissionsTEMP(
+ @NonNull LegacyPermissionSettings legacyPermissionSettings) {
+ Log.i(LOG_TAG, "writeLegacyPermissionsTEMP(legacyPermissionSettings = "
+ + legacyPermissionSettings + ")");
+ mService.writeLegacyPermissionsTEMP(legacyPermissionSettings);
+ }
+
+ @Override
+ public void onSystemReady() {
+ Log.i(LOG_TAG, "onSystemReady()");
+ mService.onSystemReady();
+ }
+
+ @Override
+ public void onStorageVolumeMounted(@NonNull String volumeUuid, boolean fingerprintChanged) {
+ Log.i(LOG_TAG, "onStorageVolumeMounted(volumeUuid = " + volumeUuid
+ + ", fingerprintChanged = " + fingerprintChanged + ")");
+ mService.onStorageVolumeMounted(volumeUuid, fingerprintChanged);
+ }
+
+ @NonNull
+ @Override
+ public int[] getGidsForUid(int uid) {
+ Log.i(LOG_TAG, "getGidsForUid(uid = " + uid + ")");
+ return mService.getGidsForUid(uid);
+ }
+
+ @Override
+ public void onUserCreated(int userId) {
+ Log.i(LOG_TAG, "onUserCreated(userId = " + userId + ")");
+ mService.onUserCreated(userId);
+ }
+
+ @Override
+ public void onUserRemoved(int userId) {
+ Log.i(LOG_TAG, "onUserRemoved(userId = " + userId + ")");
+ mService.onUserRemoved(userId);
+ }
+
+ @Override
+ public void onPackageAdded(@NonNull PackageState packageState, boolean isInstantApp,
+ @Nullable AndroidPackage oldPkg) {
+ Log.i(LOG_TAG, "onPackageAdded(packageState = " + packageState + ", isInstantApp = "
+ + isInstantApp + ", oldPkg = " + oldPkg + ")");
+ mService.onPackageAdded(packageState, isInstantApp, oldPkg);
+ }
+
+ @Override
+ public void onPackageInstalled(@NonNull AndroidPackage pkg, int previousAppId,
+ @NonNull PermissionManagerServiceInternal.PackageInstalledParams params, int userId) {
+ Log.i(LOG_TAG, "onPackageInstalled(pkg = " + pkg + ", previousAppId = " + previousAppId
+ + ", params = " + params + ", userId = " + userId + ")");
+ mService.onPackageInstalled(pkg, previousAppId, params, userId);
+ }
+
+ @Override
+ public void onPackageRemoved(@NonNull AndroidPackage pkg) {
+ Log.i(LOG_TAG, "onPackageRemoved(pkg = " + pkg + ")");
+ mService.onPackageRemoved(pkg);
+ }
+
+ @Override
+ public void onPackageUninstalled(@NonNull String packageName, int appId,
+ @NonNull PackageState packageState, @NonNull AndroidPackage pkg,
+ @NonNull List<AndroidPackage> sharedUserPkgs, int userId) {
+ Log.i(LOG_TAG, "onPackageUninstalled(packageName = " + packageName + ", appId = " + appId
+ + ", packageState = " + packageState + ", pkg = " + pkg + ", sharedUserPkgs = "
+ + sharedUserPkgs + ", userId = " + userId + ")");
+ mService.onPackageUninstalled(packageName, appId, packageState, pkg, sharedUserPkgs,
+ userId);
+ }
+}
diff --git a/services/core/java/com/android/server/pm/pkg/AndroidPackage.java b/services/core/java/com/android/server/pm/pkg/AndroidPackage.java
index 075173d..49f85e9 100644
--- a/services/core/java/com/android/server/pm/pkg/AndroidPackage.java
+++ b/services/core/java/com/android/server/pm/pkg/AndroidPackage.java
@@ -16,9 +16,14 @@
package com.android.server.pm.pkg;
+import android.annotation.Dimension;
+import android.annotation.DrawableRes;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.StringRes;
+import android.annotation.StyleRes;
import android.annotation.SystemApi;
+import android.annotation.XmlRes;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ActivityInfo;
@@ -86,6 +91,109 @@
public interface AndroidPackage {
/**
+ * @see ApplicationInfo#className
+ * @see R.styleable#AndroidManifestApplication_name
+ */
+ @Nullable
+ String getApplicationClassName();
+
+ /**
+ * @see ApplicationInfo#appComponentFactory
+ * @see R.styleable#AndroidManifestApplication_appComponentFactory
+ */
+ @Nullable
+ String getAppComponentFactory();
+
+ /**
+ * @see ApplicationInfo#backupAgentName
+ * @see R.styleable#AndroidManifestApplication_backupAgent
+ */
+ @Nullable
+ String getBackupAgentName();
+
+ /**
+ * @see ApplicationInfo#banner
+ * @see R.styleable#AndroidManifestApplication_banner
+ */
+ @DrawableRes
+ int getBannerRes();
+
+ /**
+ * @see PackageInfo#baseRevisionCode
+ * @see R.styleable#AndroidManifest_revisionCode
+ */
+ int getBaseRevisionCode();
+
+ /**
+ * @see ApplicationInfo#category
+ * @see R.styleable#AndroidManifestApplication_appCategory
+ */
+ int getCategory();
+
+ /**
+ * @see ApplicationInfo#classLoaderName
+ * @see R.styleable#AndroidManifestApplication_classLoader
+ */
+ @Nullable
+ String getClassLoaderName();
+
+ /**
+ * @see ApplicationInfo#compatibleWidthLimitDp
+ * @see R.styleable#AndroidManifestSupportsScreens_compatibleWidthLimitDp
+ */
+ @Dimension(unit = Dimension.DP)
+ int getCompatibleWidthLimitDp();
+
+ /**
+ * @see ApplicationInfo#dataExtractionRulesRes
+ * @see R.styleable#AndroidManifestApplication_dataExtractionRules
+ */
+ @XmlRes
+ int getDataExtractionRulesRes();
+
+ /**
+ * @see ApplicationInfo#descriptionRes
+ * @see R.styleable#AndroidManifestApplication_description
+ */
+ @StringRes // This is actually format="reference"
+ int getDescriptionRes();
+
+ /**
+ * @see ApplicationInfo#fullBackupContent
+ * @see R.styleable#AndroidManifestApplication_fullBackupContent
+ */
+ @XmlRes
+ int getFullBackupContentRes();
+
+ /**
+ * @see ApplicationInfo#getGwpAsanMode()
+ * @see R.styleable#AndroidManifestApplication_gwpAsanMode
+ */
+ @ApplicationInfo.GwpAsanMode
+ int getGwpAsanMode();
+
+ /**
+ * @see ApplicationInfo#iconRes
+ * @see R.styleable#AndroidManifestApplication_icon
+ */
+ @DrawableRes
+ int getIconRes();
+
+ /**
+ * @see ApplicationInfo#labelRes
+ * @see R.styleable#AndroidManifestApplication_label
+ */
+ @StringRes
+ int getLabelRes();
+
+ /**
+ * @see ApplicationInfo#largestWidthLimitDp
+ * @see R.styleable#AndroidManifestSupportsScreens_largestWidthLimitDp
+ */
+ @Dimension(unit = Dimension.DP)
+ int getLargestWidthLimitDp();
+
+ /**
* Library names this package is declared as, for use by other packages with "uses-library".
*
* @see R.styleable#AndroidManifestLibrary
@@ -94,12 +202,104 @@
List<String> getLibraryNames();
/**
+ * @see ApplicationInfo#logo
+ * @see R.styleable#AndroidManifestApplication_logo
+ */
+ @DrawableRes
+ int getLogoRes();
+
+ /**
+ * The resource ID used to provide the application's locales configuration.
+ *
+ * @see R.styleable#AndroidManifestApplication_localeConfig
+ */
+ @XmlRes
+ int getLocaleConfigRes();
+
+ /**
+ * @see PackageInfo#getLongVersionCode()
+ * @see R.styleable#AndroidManifest_versionCode
+ * @see R.styleable#AndroidManifest_versionCodeMajor
+ */
+ long getLongVersionCode();
+
+ /**
+ * @see ApplicationInfo#maxAspectRatio
+ * @see R.styleable#AndroidManifestApplication_maxAspectRatio
+ */
+ float getMaxAspectRatio();
+
+ /**
+ * @see ApplicationInfo#minAspectRatio
+ * @see R.styleable#AndroidManifestApplication_minAspectRatio
+ */
+ float getMinAspectRatio();
+
+ /**
+ * @see ApplicationInfo#getNativeHeapZeroInitialized()
+ * @see R.styleable#AndroidManifestApplication_nativeHeapZeroInitialized
+ */
+ @ApplicationInfo.NativeHeapZeroInitialized
+ int getNativeHeapZeroInitialized();
+
+ /**
+ * @see ApplicationInfo#networkSecurityConfigRes
+ * @see R.styleable#AndroidManifestApplication_networkSecurityConfig
+ */
+ @XmlRes
+ int getNetworkSecurityConfigRes();
+
+ /**
+ * @see PackageInfo#requiredAccountType
+ * @see R.styleable#AndroidManifestApplication_requiredAccountType
+ */
+ @Nullable
+ String getRequiredAccountType();
+
+ /**
+ * @see ApplicationInfo#requiresSmallestWidthDp
+ * @see R.styleable#AndroidManifestSupportsScreens_requiresSmallestWidthDp
+ */
+ @Dimension(unit = Dimension.DP)
+ int getRequiresSmallestWidthDp();
+
+ /**
+ * The restricted account authenticator type that is used by this application.
+ *
+ * @see PackageInfo#restrictedAccountType
+ * @see R.styleable#AndroidManifestApplication_restrictedAccountType
+ */
+ @Nullable
+ String getRestrictedAccountType();
+
+ /**
+ * @see ApplicationInfo#roundIconRes
+ * @see R.styleable#AndroidManifestApplication_roundIcon
+ */
+ @DrawableRes
+ int getRoundIconRes();
+
+ /**
* @see R.styleable#AndroidManifestSdkLibrary_name
*/
@Nullable
String getSdkLibraryName();
/**
+ * @see PackageInfo#sharedUserId
+ * @see R.styleable#AndroidManifest_sharedUserId
+ */
+ @Nullable
+ String getSharedUserId();
+
+ /**
+ * @see PackageInfo#sharedUserLabel
+ * @see R.styleable#AndroidManifest_sharedUserLabel
+ */
+ @StringRes
+ int getSharedUserLabelRes();
+
+ /**
* @return List of all splits for a package. Note that base.apk is considered a
* split and will be provided as index 0 of the list.
*/
@@ -113,6 +313,12 @@
String getStaticSharedLibraryName();
/**
+ * @see R.styleable#AndroidManifestStaticLibrary_version
+ * @hide
+ */
+ long getStaticSharedLibraryVersion();
+
+ /**
* @return The {@link UUID} for use with {@link StorageManager} APIs identifying where this
* package was installed.
*/
@@ -126,23 +332,319 @@
int getTargetSdkVersion();
/**
+ * @see ApplicationInfo#theme
+ * @see R.styleable#AndroidManifestApplication_theme
+ */
+ @StyleRes
+ int getThemeRes();
+
+ /**
+ * @see ApplicationInfo#uiOptions
+ * @see R.styleable#AndroidManifestApplication_uiOptions
+ */
+ int getUiOptions();
+
+ /**
+ * @see PackageInfo#versionName
+ */
+ @Nullable
+ String getVersionName();
+
+ /**
+ * @see ApplicationInfo#zygotePreloadName
+ * @see R.styleable#AndroidManifestApplication_zygotePreloadName
+ */
+ @Nullable
+ String getZygotePreloadName();
+
+ /**
+ * @see ApplicationInfo#PRIVATE_FLAG_ALLOW_AUDIO_PLAYBACK_CAPTURE
+ * @see R.styleable#AndroidManifestApplication_allowAudioPlaybackCapture
+ */
+ boolean isAllowAudioPlaybackCapture();
+
+ /**
+ * @see ApplicationInfo#FLAG_ALLOW_BACKUP
+ * @see R.styleable#AndroidManifestApplication_allowBackup
+ */
+ boolean isAllowBackup();
+
+ /**
+ * @see ApplicationInfo#FLAG_ALLOW_CLEAR_USER_DATA
+ * @see R.styleable#AndroidManifestApplication_allowClearUserData
+ */
+ boolean isAllowClearUserData();
+
+ /**
+ * @see ApplicationInfo#PRIVATE_FLAG_ALLOW_CLEAR_USER_DATA_ON_FAILED_RESTORE
+ * @see R.styleable#AndroidManifestApplication_allowClearUserDataOnFailedRestore
+ */
+ boolean isAllowClearUserDataOnFailedRestore();
+
+ /**
+ * @see ApplicationInfo#PRIVATE_FLAG_ALLOW_NATIVE_HEAP_POINTER_TAGGING
+ * @see R.styleable#AndroidManifestApplication_allowNativeHeapPointerTagging
+ */
+ boolean isAllowNativeHeapPointerTagging();
+
+ /**
+ * @see ApplicationInfo#FLAG_ALLOW_TASK_REPARENTING
+ * @see R.styleable#AndroidManifestApplication_allowTaskReparenting
+ */
+ boolean isAllowTaskReparenting();
+
+ /**
+ * If omitted from manifest, returns true if {@link #getTargetSdkVersion()} >= {@link
+ * android.os.Build.VERSION_CODES#DONUT}.
+ *
+ * @see R.styleable#AndroidManifestSupportsScreens_anyDensity
+ * @see ApplicationInfo#FLAG_SUPPORTS_SCREEN_DENSITIES
+ */
+ boolean isAnyDensity();
+
+ /**
+ * @see ApplicationInfo#areAttributionsUserVisible()
+ * @see R.styleable#AndroidManifestApplication_attributionsAreUserVisible
+ */
+ boolean isAttributionsUserVisible();
+
+ /**
+ * @see ApplicationInfo#PRIVATE_FLAG_BACKUP_IN_FOREGROUND
+ * @see R.styleable#AndroidManifestApplication_backupInForeground
+ */
+ boolean isBackupInForeground();
+
+ /**
+ * @see ApplicationInfo#FLAG_HARDWARE_ACCELERATED
+ * @see R.styleable#AndroidManifestApplication_hardwareAccelerated
+ */
+ boolean isHardwareAccelerated();
+
+ /**
+ * @see ApplicationInfo#PRIVATE_FLAG_CANT_SAVE_STATE
+ * @see R.styleable#AndroidManifestApplication_cantSaveState
+ */
+ boolean isCantSaveState();
+
+ /**
+ * @see PackageInfo#coreApp
+ */
+ boolean isCoreApp();
+
+ /**
+ * @see ApplicationInfo#crossProfile
+ * @see R.styleable#AndroidManifestApplication_crossProfile
+ */
+ boolean isCrossProfile();
+
+ /**
* @see ApplicationInfo#FLAG_DEBUGGABLE
* @see R.styleable#AndroidManifestApplication_debuggable
*/
boolean isDebuggable();
/**
+ * @see ApplicationInfo#PRIVATE_FLAG_DEFAULT_TO_DEVICE_PROTECTED_STORAGE
+ * @see R.styleable#AndroidManifestApplication_defaultToDeviceProtectedStorage
+ */
+ boolean isDefaultToDeviceProtectedStorage();
+
+ /**
+ * @see ApplicationInfo#PRIVATE_FLAG_DIRECT_BOOT_AWARE
+ * @see R.styleable#AndroidManifestApplication_directBootAware
+ */
+ boolean isDirectBootAware();
+
+ /**
+ * @see ApplicationInfo#FLAG_EXTRACT_NATIVE_LIBS
+ * @see R.styleable#AndroidManifestApplication_extractNativeLibs
+ */
+ boolean isExtractNativeLibs();
+
+ /**
+ * @see ApplicationInfo#FLAG_FACTORY_TEST
+ */
+ boolean isFactoryTest();
+
+ /**
+ * @see R.styleable#AndroidManifestApplication_forceQueryable
+ */
+ boolean isForceQueryable();
+
+ /**
+ * @see ApplicationInfo#FLAG_FULL_BACKUP_ONLY
+ * @see R.styleable#AndroidManifestApplication_fullBackupOnly
+ */
+ boolean isFullBackupOnly();
+
+ /**
+ * @see ApplicationInfo#FLAG_HAS_CODE
+ * @see R.styleable#AndroidManifestApplication_hasCode
+ */
+ boolean isHasCode();
+
+ /**
+ * @see ApplicationInfo#PRIVATE_FLAG_HAS_FRAGILE_USER_DATA
+ * @see R.styleable#AndroidManifestApplication_hasFragileUserData
+ */
+ boolean isHasFragileUserData();
+
+ /**
* @see ApplicationInfo#PRIVATE_FLAG_ISOLATED_SPLIT_LOADING
* @see R.styleable#AndroidManifest_isolatedSplits
*/
boolean isIsolatedSplitLoading();
/**
+ * @see ApplicationInfo#FLAG_KILL_AFTER_RESTORE
+ * @see R.styleable#AndroidManifestApplication_killAfterRestore
+ */
+ boolean isKillAfterRestore();
+
+ /**
+ * @see ApplicationInfo#FLAG_LARGE_HEAP
+ * @see R.styleable#AndroidManifestApplication_largeHeap
+ */
+ boolean isLargeHeap();
+
+ /**
+ * Returns true if R.styleable#AndroidManifest_sharedUserMaxSdkVersion is set to a value
+ * smaller than the current SDK version, indicating the package wants to leave its declared
+ * {@link #getSharedUserId()}. This only occurs on new installs, pretending the app never
+ * declared one.
+ *
+ * @see R.styleable#AndroidManifest_sharedUserMaxSdkVersion
+ */
+ boolean isLeavingSharedUser();
+
+ /**
+ * @see ApplicationInfo#FLAG_MULTIARCH
+ * @see R.styleable#AndroidManifestApplication_multiArch
+ */
+ boolean isMultiArch();
+
+ /**
+ * @see ApplicationInfo#nativeLibraryRootRequiresIsa
+ */
+ boolean isNativeLibraryRootRequiresIsa();
+
+ /**
+ * @see R.styleable#AndroidManifestApplication_enableOnBackInvokedCallback
+ */
+ boolean isOnBackInvokedCallbackEnabled();
+
+ /**
+ * @see ApplicationInfo#FLAG_PERSISTENT
+ * @see R.styleable#AndroidManifestApplication_persistent
+ */
+ boolean isPersistent();
+
+ /**
+ * @see ApplicationInfo#PRIVATE_FLAG_EXT_PROFILEABLE
+ * @see R.styleable#AndroidManifestProfileable
+ */
+ boolean isProfileable();
+
+ /**
+ * @see ApplicationInfo#PRIVATE_FLAG_PROFILEABLE_BY_SHELL
+ * @see R.styleable#AndroidManifestProfileable_shell
+ */
+ boolean isProfileableByShell();
+
+ /**
+ * @see ApplicationInfo#PRIVATE_FLAG_REQUEST_LEGACY_EXTERNAL_STORAGE
+ * @see R.styleable#AndroidManifestApplication_requestLegacyExternalStorage
+ */
+ boolean isRequestLegacyExternalStorage();
+
+ /**
+ * @see PackageInfo#requiredForAllUsers
+ * @see R.styleable#AndroidManifestApplication_requiredForAllUsers
+ */
+ boolean isRequiredForAllUsers();
+
+ /**
+ * Whether the enabled settings of components in the application should be reset to the default,
+ * when the application's user data is cleared.
+ *
+ * @see R.styleable#AndroidManifestApplication_resetEnabledSettingsOnAppDataCleared
+ */
+ boolean isResetEnabledSettingsOnAppDataCleared();
+
+ /**
+ * @see ApplicationInfo#FLAG_RESTORE_ANY_VERSION
+ * @see R.styleable#AndroidManifestApplication_restoreAnyVersion
+ */
+ boolean isRestoreAnyVersion();
+
+ /**
* @see ApplicationInfo#PRIVATE_FLAG_SIGNED_WITH_PLATFORM_KEY
*/
boolean isSignedWithPlatformKey();
/**
+ * If omitted from manifest, returns true if {@link #getTargetSdkVersion()} >= {@link
+ * android.os.Build.VERSION_CODES#GINGERBREAD}.
+ *
+ * @see R.styleable#AndroidManifestSupportsScreens_xlargeScreens
+ * @see ApplicationInfo#FLAG_SUPPORTS_XLARGE_SCREENS
+ */
+ boolean isSupportsExtraLargeScreens();
+
+ /**
+ * If omitted from manifest, returns true if {@link #getTargetSdkVersion()} >= {@link
+ * android.os.Build.VERSION_CODES#DONUT}.
+ *
+ * @see R.styleable#AndroidManifestSupportsScreens_largeScreens
+ * @see ApplicationInfo#FLAG_SUPPORTS_LARGE_SCREENS
+ */
+ boolean isSupportsLargeScreens();
+
+ /**
+ * If omitted from manifest, returns true.
+ *
+ * @see R.styleable#AndroidManifestSupportsScreens_normalScreens
+ * @see ApplicationInfo#FLAG_SUPPORTS_NORMAL_SCREENS
+ */
+ boolean isSupportsNormalScreens();
+
+ /**
+ * @see ApplicationInfo#FLAG_SUPPORTS_RTL
+ * @see R.styleable#AndroidManifestApplication_supportsRtl
+ */
+ boolean isSupportsRtl();
+
+ /**
+ * If omitted from manifest, returns true if {@link #getTargetSdkVersion()} >= {@link
+ * android.os.Build.VERSION_CODES#DONUT}.
+ *
+ * @see R.styleable#AndroidManifestSupportsScreens_smallScreens
+ * @see ApplicationInfo#FLAG_SUPPORTS_SMALL_SCREENS
+ */
+ boolean isSupportsSmallScreens();
+
+ /**
+ * @see ApplicationInfo#FLAG_TEST_ONLY
+ * @see R.styleable#AndroidManifestApplication_testOnly
+ */
+ boolean isTestOnly();
+
+ /**
+ * The install time abi override to choose 32bit abi's when multiple abi's are present. This is
+ * only meaningful for multiarch applications. The use32bitAbi attribute is ignored if
+ * cpuAbiOverride is also set.
+ *
+ * @see R.attr#use32bitAbi
+ */
+ boolean isUse32BitAbi();
+
+ /**
+ * @see ApplicationInfo#FLAG_USES_CLEARTEXT_TRAFFIC
+ * @see R.styleable#AndroidManifestApplication_usesCleartextTraffic
+ */
+ boolean isUsesCleartextTraffic();
+
+ /**
* @see ApplicationInfo#PRIVATE_FLAG_USE_EMBEDDED_DEX
* @see R.styleable#AndroidManifestApplication_useEmbeddedDex
*/
@@ -163,14 +665,6 @@
// Methods below this comment are not yet exposed as API
/**
- * @see ApplicationInfo#areAttributionsUserVisible()
- * @see R.styleable#AndroidManifestApplication_attributionsAreUserVisible
- * @hide
- */
- @Nullable
- boolean areAttributionsUserVisible();
-
- /**
* Set of Activities parsed from the manifest.
* <p>
* This contains minimal system state and does not
@@ -207,14 +701,6 @@
List<ParsedApexSystemService> getApexSystemServices();
/**
- * @see ApplicationInfo#appComponentFactory
- * @see R.styleable#AndroidManifestApplication_appComponentFactory
- * @hide
- */
- @Nullable
- String getAppComponentFactory();
-
- /**
* @see R.styleable#AndroidManifestAttribution
* @hide
*/
@@ -232,21 +718,6 @@
int getAutoRevokePermissions();
/**
- * @see ApplicationInfo#backupAgentName
- * @see R.styleable#AndroidManifestApplication_backupAgent
- * @hide
- */
- @Nullable
- String getBackupAgentName();
-
- /**
- * @see ApplicationInfo#banner
- * @see R.styleable#AndroidManifestApplication_banner
- * @hide
- */
- int getBanner();
-
- /**
* @see ApplicationInfo#sourceDir
* @see ApplicationInfo#getBaseCodePath
*
@@ -259,43 +730,6 @@
String getBaseApkPath();
/**
- * @see PackageInfo#baseRevisionCode
- * @see R.styleable#AndroidManifest_revisionCode
- * @hide
- */
- int getBaseRevisionCode();
-
- /**
- * @see ApplicationInfo#category
- * @see R.styleable#AndroidManifestApplication_appCategory
- * @hide
- */
- int getCategory();
-
- /**
- * @see ApplicationInfo#classLoaderName
- * @see R.styleable#AndroidManifestApplication_classLoader
- * @hide
- */
- @Nullable
- String getClassLoaderName();
-
- /**
- * @see ApplicationInfo#className
- * @see R.styleable#AndroidManifestApplication_name
- * @hide
- */
- @Nullable
- String getClassName();
-
- /**
- * @see ApplicationInfo#compatibleWidthLimitDp
- * @see R.styleable#AndroidManifestSupportsScreens_compatibleWidthLimitDp
- * @hide
- */
- int getCompatibleWidthLimitDp();
-
- /**
* @see ApplicationInfo#compileSdkVersion
* @see R.styleable#AndroidManifest_compileSdkVersion
* @hide
@@ -320,20 +754,6 @@
List<ConfigurationInfo> getConfigPreferences();
/**
- * @see ApplicationInfo#dataExtractionRulesRes
- * @see R.styleable#AndroidManifestApplication_dataExtractionRules
- * @hide
- */
- int getDataExtractionRules();
-
- /**
- * @see ApplicationInfo#descriptionRes
- * @see R.styleable#AndroidManifestApplication_description
- * @hide
- */
- int getDescriptionRes();
-
- /**
* @see PackageInfo#featureGroups
* @see R.styleable#AndroidManifestUsesFeature
* @hide
@@ -343,28 +763,6 @@
List<FeatureGroupInfo> getFeatureGroups();
/**
- * @see ApplicationInfo#fullBackupContent
- * @see R.styleable#AndroidManifestApplication_fullBackupContent
- * @hide
- */
- int getFullBackupContent();
-
- /**
- * @see ApplicationInfo#getGwpAsanMode()
- * @see R.styleable#AndroidManifestApplication_gwpAsanMode
- * @hide
- */
- @ApplicationInfo.GwpAsanMode
- int getGwpAsanMode();
-
- /**
- * @see ApplicationInfo#iconRes
- * @see R.styleable#AndroidManifestApplication_icon
- * @hide
- */
- int getIconRes();
-
- /**
* Permissions requested but not in the manifest. These may have been split or migrated from
* previous versions/definitions.
* @hide
@@ -411,43 +809,6 @@
Set<String> getKnownActivityEmbeddingCerts();
/**
- * @see ApplicationInfo#labelRes
- * @see R.styleable#AndroidManifestApplication_label
- * @hide
- */
- int getLabelRes();
-
- /**
- * @see ApplicationInfo#largestWidthLimitDp
- * @see R.styleable#AndroidManifestSupportsScreens_largestWidthLimitDp
- * @hide
- */
- int getLargestWidthLimitDp();
-
- /**
- * The resource ID used to provide the application's locales configuration.
- *
- * @see R.styleable#AndroidManifestApplication_localeConfig
- * @hide
- */
- int getLocaleConfigRes();
-
- /**
- * @see ApplicationInfo#logo
- * @see R.styleable#AndroidManifestApplication_logo
- * @hide
- */
- int getLogo();
-
- /**
- * @see PackageInfo#getLongVersionCode()
- * @see R.styleable#AndroidManifest_versionCode
- * @see R.styleable#AndroidManifest_versionCodeMajor
- * @hide
- */
- long getLongVersionCode();
-
- /**
* @see ApplicationInfo#manageSpaceActivityName
* @see R.styleable#AndroidManifestApplication_manageSpaceActivity
* @hide
@@ -464,13 +825,6 @@
String getManifestPackageName();
/**
- * @see ApplicationInfo#maxAspectRatio
- * @see R.styleable#AndroidManifestApplication_maxAspectRatio
- * @hide
- */
- float getMaxAspectRatio();
-
- /**
* @see R.styleable#AndroidManifestUsesSdk_maxSdkVersion
* @hide
*/
@@ -501,13 +855,6 @@
Set<String> getMimeGroups();
/**
- * @see ApplicationInfo#minAspectRatio
- * @see R.styleable#AndroidManifestApplication_minAspectRatio
- * @hide
- */
- float getMinAspectRatio();
-
- /**
* @see R.styleable#AndroidManifestExtensionSdk
* @hide
*/
@@ -523,14 +870,6 @@
int getMinSdkVersion();
/**
- * @see ApplicationInfo#getNativeHeapZeroInitialized()
- * @see R.styleable#AndroidManifestApplication_nativeHeapZeroInitialized
- * @hide
- */
- @ApplicationInfo.NativeHeapZeroInitialized
- int getNativeHeapZeroInitialized();
-
- /**
* @see ApplicationInfo#nativeLibraryDir
* @hide
*/
@@ -545,13 +884,6 @@
String getNativeLibraryRootDir();
/**
- * @see ApplicationInfo#networkSecurityConfigRes
- * @see R.styleable#AndroidManifestApplication_networkSecurityConfig
- * @hide
- */
- int getNetworkSecurityConfigRes();
-
- /**
* If {@link R.styleable#AndroidManifestApplication_label} is a string literal, this is it.
* Otherwise, it's stored as {@link #getLabelRes()}.
*
@@ -787,21 +1119,6 @@
List<String> getRequestedPermissions();
/**
- * @see PackageInfo#requiredAccountType
- * @see R.styleable#AndroidManifestApplication_requiredAccountType
- * @hide
- */
- @Nullable
- String getRequiredAccountType();
-
- /**
- * @see ApplicationInfo#requiresSmallestWidthDp
- * @see R.styleable#AndroidManifestSupportsScreens_requiresSmallestWidthDp
- * @hide
- */
- int getRequiresSmallestWidthDp();
-
- /**
* Whether or not the app requested explicitly resizeable Activities. Null value means nothing
* was explicitly requested.
*
@@ -824,23 +1141,6 @@
byte[] getRestrictUpdateHash();
/**
- * The restricted account authenticator type that is used by this application.
- *
- * @see PackageInfo#restrictedAccountType
- * @see R.styleable#AndroidManifestApplication_restrictedAccountType
- * @hide
- */
- @Nullable
- String getRestrictedAccountType();
-
- /**
- * @see ApplicationInfo#roundIconRes
- * @see R.styleable#AndroidManifestApplication_roundIcon
- * @hide
- */
- int getRoundIconRes();
-
- /**
* @see R.styleable#AndroidManifestSdkLibrary_versionMajor
* @hide
*/
@@ -872,21 +1172,6 @@
List<ParsedService> getServices();
/**
- * @see PackageInfo#sharedUserId
- * @see R.styleable#AndroidManifest_sharedUserId
- * @hide
- */
- @Nullable
- String getSharedUserId();
-
- /**
- * @see PackageInfo#sharedUserLabel
- * @see R.styleable#AndroidManifest_sharedUserLabel
- * @hide
- */
- int getSharedUserLabel();
-
- /**
* The signature data of all APKs in this package, which must be exactly the same across the
* base and splits.
* @hide
@@ -949,12 +1234,6 @@
int[] getSplitRevisionCodes();
/**
- * @see R.styleable#AndroidManifestStaticLibrary_version
- * @hide
- */
- long getStaticSharedLibVersion();
-
- /**
* @see ApplicationInfo#targetSandboxVersion
* @see R.styleable#AndroidManifest_targetSandboxVersion
* @hide
@@ -970,20 +1249,6 @@
String getTaskAffinity();
/**
- * @see ApplicationInfo#theme
- * @see R.styleable#AndroidManifestApplication_theme
- * @hide
- */
- int getTheme();
-
- /**
- * @see ApplicationInfo#uiOptions
- * @see R.styleable#AndroidManifestApplication_uiOptions
- * @hide
- */
- int getUiOptions();
-
- /**
* This is an appId, the {@link ApplicationInfo#uid} if the user ID is
* {@link android.os.UserHandle#SYSTEM}.
*
@@ -1095,27 +1360,12 @@
long[] getUsesStaticLibrariesVersions();
/**
- * @see PackageInfo#versionName
- * @hide
- */
- @Nullable
- String getVersionName();
-
- /**
* @see ApplicationInfo#volumeUuid
* @hide
*/
@Nullable
String getVolumeUuid();
- /**
- * @see ApplicationInfo#zygotePreloadName
- * @see R.styleable#AndroidManifestApplication_zygotePreloadName
- * @hide
- */
- @Nullable
- String getZygotePreloadName();
-
/** @hide */
boolean hasPreserveLegacyExternalStorage();
@@ -1133,110 +1383,10 @@
*/
Boolean hasRequestRawExternalStorageAccess();
- /**
- * @see ApplicationInfo#PRIVATE_FLAG_ALLOW_AUDIO_PLAYBACK_CAPTURE
- * @see R.styleable#AndroidManifestApplication_allowAudioPlaybackCapture
- * @hide
- */
- boolean isAllowAudioPlaybackCapture();
-
- /**
- * @see ApplicationInfo#FLAG_ALLOW_BACKUP
- * @see R.styleable#AndroidManifestApplication_allowBackup
- * @hide
- */
- boolean isAllowBackup();
-
- /**
- * @see ApplicationInfo#FLAG_ALLOW_CLEAR_USER_DATA
- * @see R.styleable#AndroidManifestApplication_allowClearUserData
- * @hide
- */
- boolean isAllowClearUserData();
-
- /**
- * @see ApplicationInfo#PRIVATE_FLAG_ALLOW_CLEAR_USER_DATA_ON_FAILED_RESTORE
- * @see R.styleable#AndroidManifestApplication_allowClearUserDataOnFailedRestore
- * @hide
- */
- boolean isAllowClearUserDataOnFailedRestore();
-
- /**
- * @see ApplicationInfo#PRIVATE_FLAG_ALLOW_NATIVE_HEAP_POINTER_TAGGING
- * @see R.styleable#AndroidManifestApplication_allowNativeHeapPointerTagging
- * @hide
- */
- boolean isAllowNativeHeapPointerTagging();
-
- /**
- * @see ApplicationInfo#FLAG_ALLOW_TASK_REPARENTING
- * @see R.styleable#AndroidManifestApplication_allowTaskReparenting
- * @hide
- */
- boolean isAllowTaskReparenting();
-
- /**
- * If omitted from manifest, returns true if {@link #getTargetSdkVersion()} >= {@link
- * android.os.Build.VERSION_CODES#DONUT}.
- *
- * @see R.styleable#AndroidManifestSupportsScreens_anyDensity
- * @see ApplicationInfo#FLAG_SUPPORTS_SCREEN_DENSITIES
- * @hide
- */
- boolean isAnyDensity();
-
/** @hide */
boolean isApex();
/**
- * @see ApplicationInfo#PRIVATE_FLAG_BACKUP_IN_FOREGROUND
- * @see R.styleable#AndroidManifestApplication_backupInForeground
- * @hide
- */
- boolean isBackupInForeground();
-
- /**
- * @see ApplicationInfo#FLAG_HARDWARE_ACCELERATED
- * @see R.styleable#AndroidManifestApplication_hardwareAccelerated
- * @hide
- */
- boolean isBaseHardwareAccelerated();
-
- /**
- * @see ApplicationInfo#PRIVATE_FLAG_CANT_SAVE_STATE
- * @see R.styleable#AndroidManifestApplication_cantSaveState
- * @hide
- */
- boolean isCantSaveState();
-
- /**
- * @see PackageInfo#coreApp
- * @hide
- */
- boolean isCoreApp();
-
- /**
- * @see ApplicationInfo#crossProfile
- * @see R.styleable#AndroidManifestApplication_crossProfile
- * @hide
- */
- boolean isCrossProfile();
-
- /**
- * @see ApplicationInfo#PRIVATE_FLAG_DEFAULT_TO_DEVICE_PROTECTED_STORAGE
- * @see R.styleable#AndroidManifestApplication_defaultToDeviceProtectedStorage
- * @hide
- */
- boolean isDefaultToDeviceProtectedStorage();
-
- /**
- * @see ApplicationInfo#PRIVATE_FLAG_DIRECT_BOOT_AWARE
- * @see R.styleable#AndroidManifestApplication_directBootAware
- * @hide
- */
- boolean isDirectBootAware();
-
- /**
* @see ApplicationInfo#enabled
* @see R.styleable#AndroidManifestApplication_enabled
* @hide
@@ -1250,32 +1400,6 @@
boolean isExternalStorage();
/**
- * @see ApplicationInfo#FLAG_EXTRACT_NATIVE_LIBS
- * @see R.styleable#AndroidManifestApplication_extractNativeLibs
- * @hide
- */
- boolean isExtractNativeLibs();
-
- /**
- * @see ApplicationInfo#FLAG_FACTORY_TEST
- * @hide
- */
- boolean isFactoryTest();
-
- /**
- * @see R.styleable#AndroidManifestApplication_forceQueryable
- * @hide
- */
- boolean isForceQueryable();
-
- /**
- * @see ApplicationInfo#FLAG_FULL_BACKUP_ONLY
- * @see R.styleable#AndroidManifestApplication_fullBackupOnly
- * @hide
- */
- boolean isFullBackupOnly();
-
- /**
* @see ApplicationInfo#FLAG_IS_GAME
* @see R.styleable#AndroidManifestApplication_isGame
* @hide
@@ -1284,13 +1408,6 @@
boolean isGame();
/**
- * @see ApplicationInfo#FLAG_HAS_CODE
- * @see R.styleable#AndroidManifestApplication_hasCode
- * @hide
- */
- boolean isHasCode();
-
- /**
* @see ApplicationInfo#PRIVATE_FLAG_HAS_DOMAIN_URLS
* @see R.styleable#AndroidManifestIntentFilter
* @hide
@@ -1298,55 +1415,6 @@
boolean isHasDomainUrls();
/**
- * @see ApplicationInfo#PRIVATE_FLAG_HAS_FRAGILE_USER_DATA
- * @see R.styleable#AndroidManifestApplication_hasFragileUserData
- * @hide
- */
- boolean isHasFragileUserData();
-
- /**
- * @see ApplicationInfo#FLAG_KILL_AFTER_RESTORE
- * @see R.styleable#AndroidManifestApplication_killAfterRestore
- * @hide
- */
- boolean isKillAfterRestore();
-
- /**
- * @see ApplicationInfo#FLAG_LARGE_HEAP
- * @see R.styleable#AndroidManifestApplication_largeHeap
- * @hide
- */
- boolean isLargeHeap();
-
- /**
- * Returns true if R.styleable#AndroidManifest_sharedUserMaxSdkVersion is set to a value
- * smaller than the current SDK version.
- *
- * @see R.styleable#AndroidManifest_sharedUserMaxSdkVersion
- * @hide
- */
- boolean isLeavingSharedUid();
-
- /**
- * @see ApplicationInfo#FLAG_MULTIARCH
- * @see R.styleable#AndroidManifestApplication_multiArch
- * @hide
- */
- boolean isMultiArch();
-
- /**
- * @see ApplicationInfo#nativeLibraryRootRequiresIsa
- * @hide
- */
- boolean isNativeLibraryRootRequiresIsa();
-
- /**
- * @see R.styleable#AndroidManifestApplication_enableOnBackInvokedCallback
- * @hide
- */
- boolean isOnBackInvokedCallbackEnabled();
-
- /**
* @see ApplicationInfo#PRIVATE_FLAG_IS_RESOURCE_OVERLAY
* @see ApplicationInfo#isResourceOverlay()
* @see R.styleable#AndroidManifestResourceOverlay
@@ -1371,50 +1439,6 @@
boolean isPartiallyDirectBootAware();
/**
- * @see ApplicationInfo#FLAG_PERSISTENT
- * @see R.styleable#AndroidManifestApplication_persistent
- * @hide
- */
- boolean isPersistent();
-
- /**
- * @see ApplicationInfo#PRIVATE_FLAG_EXT_PROFILEABLE
- * @see R.styleable#AndroidManifestProfileable
- * @hide
- */
- boolean isProfileable();
-
- /**
- * @see ApplicationInfo#PRIVATE_FLAG_PROFILEABLE_BY_SHELL
- * @see R.styleable#AndroidManifestProfileable_shell
- * @hide
- */
- boolean isProfileableByShell();
-
- /**
- * @see ApplicationInfo#PRIVATE_FLAG_REQUEST_LEGACY_EXTERNAL_STORAGE
- * @see R.styleable#AndroidManifestApplication_requestLegacyExternalStorage
- * @hide
- */
- boolean isRequestLegacyExternalStorage();
-
- /**
- * @see PackageInfo#requiredForAllUsers
- * @see R.styleable#AndroidManifestApplication_requiredForAllUsers
- * @hide
- */
- boolean isRequiredForAllUsers();
-
- /**
- * Whether the enabled settings of components in the application should be reset to the default,
- * when the application's user data is cleared.
- *
- * @see R.styleable#AndroidManifestApplication_resetEnabledSettingsOnAppDataCleared
- * @hide
- */
- boolean isResetEnabledSettingsOnAppDataCleared();
-
- /**
* If omitted from manifest, returns true if {@link #getTargetSdkVersion()} >= {@link
* android.os.Build.VERSION_CODES#DONUT}.
*
@@ -1432,13 +1456,6 @@
boolean isResizeableActivityViaSdkVersion();
/**
- * @see ApplicationInfo#FLAG_RESTORE_ANY_VERSION
- * @see R.styleable#AndroidManifestApplication_restoreAnyVersion
- * @hide
- */
- boolean isRestoreAnyVersion();
-
- /**
* True means that this package/app contains an SDK library.
* @see R.styleable#AndroidManifestSdkLibrary
* @hide
@@ -1459,76 +1476,6 @@
boolean isStub();
/**
- * If omitted from manifest, returns true if {@link #getTargetSdkVersion()} >= {@link
- * android.os.Build.VERSION_CODES#GINGERBREAD}.
- *
- * @see R.styleable#AndroidManifestSupportsScreens_xlargeScreens
- * @see ApplicationInfo#FLAG_SUPPORTS_XLARGE_SCREENS
- * @hide
- */
- boolean isSupportsExtraLargeScreens();
-
- /**
- * If omitted from manifest, returns true if {@link #getTargetSdkVersion()} >= {@link
- * android.os.Build.VERSION_CODES#DONUT}.
- *
- * @see R.styleable#AndroidManifestSupportsScreens_largeScreens
- * @see ApplicationInfo#FLAG_SUPPORTS_LARGE_SCREENS
- * @hide
- */
- boolean isSupportsLargeScreens();
-
- /**
- * If omitted from manifest, returns true.
- *
- * @see R.styleable#AndroidManifestSupportsScreens_normalScreens
- * @see ApplicationInfo#FLAG_SUPPORTS_NORMAL_SCREENS
- * @hide
- */
- boolean isSupportsNormalScreens();
-
- /**
- * @see ApplicationInfo#FLAG_SUPPORTS_RTL
- * @see R.styleable#AndroidManifestApplication_supportsRtl
- * @hide
- */
- boolean isSupportsRtl();
-
- /**
- * If omitted from manifest, returns true if {@link #getTargetSdkVersion()} >= {@link
- * android.os.Build.VERSION_CODES#DONUT}.
- *
- * @see R.styleable#AndroidManifestSupportsScreens_smallScreens
- * @see ApplicationInfo#FLAG_SUPPORTS_SMALL_SCREENS
- * @hide
- */
- boolean isSupportsSmallScreens();
-
- /**
- * @see ApplicationInfo#FLAG_TEST_ONLY
- * @see R.styleable#AndroidManifestApplication_testOnly
- * @hide
- */
- boolean isTestOnly();
-
- /**
- * The install time abi override to choose 32bit abi's when multiple abi's are present. This is
- * only meaningful for multiarch applications. The use32bitAbi attribute is ignored if
- * cpuAbiOverride is also set.
- *
- * @see R.attr#use32bitAbi
- * @hide
- */
- boolean isUse32BitAbi();
-
- /**
- * @see ApplicationInfo#FLAG_USES_CLEARTEXT_TRAFFIC
- * @see R.styleable#AndroidManifestApplication_usesCleartextTraffic
- * @hide
- */
- boolean isUsesCleartextTraffic();
-
- /**
* Set if the any of components are visible to instant applications.
*
* @see R.styleable#AndroidManifestActivity_visibleToInstantApps
diff --git a/services/core/java/com/android/server/pm/pkg/component/ParsedActivityUtils.java b/services/core/java/com/android/server/pm/pkg/component/ParsedActivityUtils.java
index ea791e1..b73ff34 100644
--- a/services/core/java/com/android/server/pm/pkg/component/ParsedActivityUtils.java
+++ b/services/core/java/com/android/server/pm/pkg/component/ParsedActivityUtils.java
@@ -144,7 +144,7 @@
| flag(ActivityInfo.FLAG_SYSTEM_USER_ONLY, R.styleable.AndroidManifestActivity_systemUserOnly, sa)));
if (!receiver) {
- activity.setFlags(activity.getFlags() | (flag(ActivityInfo.FLAG_HARDWARE_ACCELERATED, R.styleable.AndroidManifestActivity_hardwareAccelerated, pkg.isBaseHardwareAccelerated(), sa)
+ activity.setFlags(activity.getFlags() | (flag(ActivityInfo.FLAG_HARDWARE_ACCELERATED, R.styleable.AndroidManifestActivity_hardwareAccelerated, pkg.isHardwareAccelerated(), sa)
| flag(ActivityInfo.FLAG_ALLOW_EMBEDDED, R.styleable.AndroidManifestActivity_allowEmbedded, sa)
| flag(ActivityInfo.FLAG_ALWAYS_FOCUSABLE, R.styleable.AndroidManifestActivity_alwaysFocusable, sa)
| flag(ActivityInfo.FLAG_AUTO_REMOVE_FROM_RECENTS, R.styleable.AndroidManifestActivity_autoRemoveFromRecents, sa)
@@ -195,6 +195,15 @@
sa.getFloat(R.styleable.AndroidManifestActivity_minAspectRatio,
0 /*default*/));
}
+
+ if (sa.hasValue(R.styleable.AndroidManifestActivity_enableOnBackInvokedCallback)) {
+ boolean enable = sa.getBoolean(
+ R.styleable.AndroidManifestActivity_enableOnBackInvokedCallback,
+ false);
+ activity.setPrivateFlags(activity.getPrivateFlags()
+ | (enable ? ActivityInfo.PRIVATE_FLAG_ENABLE_ON_BACK_INVOKED_CALLBACK
+ : ActivityInfo.PRIVATE_FLAG_DISABLE_ON_BACK_INVOKED_CALLBACK));
+ }
} else {
activity.setLaunchMode(ActivityInfo.LAUNCH_MULTIPLE)
.setConfigChanges(0)
diff --git a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackage.java b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackage.java
index 16f5d16..12dfef4 100644
--- a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackage.java
+++ b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackage.java
@@ -155,7 +155,7 @@
ParsingPackage setUiOptions(int uiOptions);
- ParsingPackage setBaseHardwareAccelerated(boolean baseHardwareAccelerated);
+ ParsingPackage setHardwareAccelerated(boolean hardwareAccelerated);
ParsingPackage setResizeableActivity(Boolean resizeable);
@@ -255,13 +255,13 @@
ParsingPackage setBackupAgentName(String backupAgentName);
- ParsingPackage setBanner(int banner);
+ ParsingPackage setBannerRes(int banner);
ParsingPackage setCategory(int category);
ParsingPackage setClassLoaderName(String classLoaderName);
- ParsingPackage setClassName(String className);
+ ParsingPackage setApplicationClassName(String className);
ParsingPackage setCompatibleWidthLimitDp(int compatibleWidthLimitDp);
@@ -281,9 +281,9 @@
ParsingPackage setCrossProfile(boolean crossProfile);
- ParsingPackage setFullBackupContent(int fullBackupContent);
+ ParsingPackage setFullBackupContentRes(int fullBackupContentRes);
- ParsingPackage setDataExtractionRules(int dataExtractionRules);
+ ParsingPackage setDataExtractionRulesRes(int dataExtractionRulesRes);
ParsingPackage setHasDomainUrls(boolean hasDomainUrls);
@@ -292,13 +292,13 @@
ParsingPackage setInstallLocation(int installLocation);
/** @see R#styleable.AndroidManifest_sharedUserMaxSdkVersion */
- ParsingPackage setLeavingSharedUid(boolean leavingSharedUid);
+ ParsingPackage setLeavingSharedUser(boolean leavingSharedUser);
ParsingPackage setLabelRes(int labelRes);
ParsingPackage setLargestWidthLimitDp(int largestWidthLimitDp);
- ParsingPackage setLogo(int logo);
+ ParsingPackage setLogoRes(int logo);
ParsingPackage setManageSpaceActivityName(String manageSpaceActivityName);
@@ -336,13 +336,13 @@
ParsingPackage setRoundIconRes(int roundIconRes);
- ParsingPackage setSharedUserLabel(int sharedUserLabel);
+ ParsingPackage setSharedUserLabelRes(int sharedUserLabelRes);
ParsingPackage setSigningDetails(@NonNull SigningDetails signingDetails);
ParsingPackage setSplitClassLoaderName(int splitIndex, String classLoaderName);
- ParsingPackage setStaticSharedLibVersion(long staticSharedLibVersion);
+ ParsingPackage setStaticSharedLibraryVersion(long staticSharedLibraryVersion);
ParsingPackage setSupportsLargeScreens(int supportsLargeScreens);
@@ -354,7 +354,7 @@
ParsingPackage setTargetSandboxVersion(int targetSandboxVersion);
- ParsingPackage setTheme(int theme);
+ ParsingPackage setThemeRes(int theme);
ParsingPackage setRequestForegroundServiceExemption(boolean requestForegroundServiceExemption);
@@ -512,7 +512,7 @@
boolean isAnyDensity();
- boolean isBaseHardwareAccelerated();
+ boolean isHardwareAccelerated();
boolean isCantSaveState();
diff --git a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java
index c6e1793..2a2640d 100644
--- a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java
+++ b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java
@@ -1044,9 +1044,9 @@
}
return input.success(pkg
- .setLeavingSharedUid(leaving)
+ .setLeavingSharedUser(leaving)
.setSharedUserId(str.intern())
- .setSharedUserLabel(resId(R.styleable.AndroidManifest_sharedUserLabel, sa)));
+ .setSharedUserLabelRes(resId(R.styleable.AndroidManifest_sharedUserLabel, sa)));
}
private static ParseResult<ParsingPackage> parseKeySets(ParseInput input,
@@ -1869,7 +1869,7 @@
return input.error("Empty class name in package " + packageName);
}
- pkg.setClassName(outInfoName);
+ pkg.setApplicationClassName(outInfoName);
}
TypedValue labelValue = sa.peekValue(R.styleable.AndroidManifestApplication_label);
@@ -1939,7 +1939,7 @@
fullBackupContent = v.data == 0 ? -1 : 0;
}
- pkg.setFullBackupContent(fullBackupContent);
+ pkg.setFullBackupContentRes(fullBackupContent);
}
if (DEBUG_BACKUP) {
Slog.v(TAG, "fullBackupContent=" + fullBackupContent + " for " + pkgName);
@@ -2247,7 +2247,7 @@
.setOnBackInvokedCallbackEnabled(bool(false, R.styleable.AndroidManifestApplication_enableOnBackInvokedCallback, sa))
// targetSdkVersion gated
.setAllowAudioPlaybackCapture(bool(targetSdk >= Build.VERSION_CODES.Q, R.styleable.AndroidManifestApplication_allowAudioPlaybackCapture, sa))
- .setBaseHardwareAccelerated(bool(targetSdk >= Build.VERSION_CODES.ICE_CREAM_SANDWICH, R.styleable.AndroidManifestApplication_hardwareAccelerated, sa))
+ .setHardwareAccelerated(bool(targetSdk >= Build.VERSION_CODES.ICE_CREAM_SANDWICH, R.styleable.AndroidManifestApplication_hardwareAccelerated, sa))
.setRequestLegacyExternalStorage(bool(targetSdk < Build.VERSION_CODES.Q, R.styleable.AndroidManifestApplication_requestLegacyExternalStorage, sa))
.setUsesCleartextTraffic(bool(targetSdk < Build.VERSION_CODES.P, R.styleable.AndroidManifestApplication_usesCleartextTraffic, sa))
// Ints Default 0
@@ -2258,14 +2258,14 @@
.setMaxAspectRatio(aFloat(R.styleable.AndroidManifestApplication_maxAspectRatio, sa))
.setMinAspectRatio(aFloat(R.styleable.AndroidManifestApplication_minAspectRatio, sa))
// Resource ID
- .setBanner(resId(R.styleable.AndroidManifestApplication_banner, sa))
+ .setBannerRes(resId(R.styleable.AndroidManifestApplication_banner, sa))
.setDescriptionRes(resId(R.styleable.AndroidManifestApplication_description, sa))
.setIconRes(resId(R.styleable.AndroidManifestApplication_icon, sa))
- .setLogo(resId(R.styleable.AndroidManifestApplication_logo, sa))
+ .setLogoRes(resId(R.styleable.AndroidManifestApplication_logo, sa))
.setNetworkSecurityConfigRes(resId(R.styleable.AndroidManifestApplication_networkSecurityConfig, sa))
.setRoundIconRes(resId(R.styleable.AndroidManifestApplication_roundIcon, sa))
- .setTheme(resId(R.styleable.AndroidManifestApplication_theme, sa))
- .setDataExtractionRules(
+ .setThemeRes(resId(R.styleable.AndroidManifestApplication_theme, sa))
+ .setDataExtractionRulesRes(
resId(R.styleable.AndroidManifestApplication_dataExtractionRules, sa))
.setLocaleConfigRes(resId(R.styleable.AndroidManifestApplication_localeConfig, sa))
// Strings
@@ -2399,7 +2399,7 @@
}
return input.success(pkg.setStaticSharedLibraryName(lname.intern())
- .setStaticSharedLibVersion(
+ .setStaticSharedLibraryVersion(
PackageInfo.composeLongVersionCode(versionMajor, version))
.setStaticSharedLibrary(true));
} finally {
@@ -2694,7 +2694,7 @@
// Build custom App Details activity info instead of parsing it from xml
return input.success(ParsedActivity.makeAppDetailsActivity(packageName,
pkg.getProcessName(), pkg.getUiOptions(), taskAffinity,
- pkg.isBaseHardwareAccelerated()));
+ pkg.isHardwareAccelerated()));
}
/**
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index f88adab..fe0fe29 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -617,7 +617,6 @@
private final com.android.internal.policy.LogDecelerateInterpolator mLogDecelerateInterpolator
= new LogDecelerateInterpolator(100, 0);
- private boolean mPerDisplayFocusEnabled = false;
private volatile int mTopFocusedDisplayId = INVALID_DISPLAY;
private int mPowerButtonSuppressionDelayMillis = POWER_BUTTON_SUPPRESSION_DELAY_DEFAULT_MILLIS;
@@ -2118,9 +2117,6 @@
mHandleVolumeKeysInWM = mContext.getResources().getBoolean(
com.android.internal.R.bool.config_handleVolumeKeysInWindowManager);
- mPerDisplayFocusEnabled = mContext.getResources().getBoolean(
- com.android.internal.R.bool.config_perDisplayFocusEnabled);
-
mWakeUpToLastStateTimeout = mContext.getResources().getInteger(
com.android.internal.R.integer.config_wakeUpToLastStateTimeoutMillis);
@@ -4314,23 +4310,20 @@
wakeUpFromWakeKey(event);
}
- if ((result & ACTION_PASS_TO_USER) != 0 && !mPerDisplayFocusEnabled
- && displayId != INVALID_DISPLAY && displayId != mTopFocusedDisplayId) {
- // If the key event is targeted to a specific display, then the user is interacting with
- // that display. Therefore, give focus to the display that the user is interacting with,
- // unless that display maintains its own focus.
- Display display = mDisplayManager.getDisplay(displayId);
- if ((display.getFlags() & Display.FLAG_OWN_FOCUS) == 0) {
- // An event is targeting a non-focused display. Move the display to top so that
- // it can become the focused display to interact with the user.
- // This should be done asynchronously, once the focus logic is fully moved to input
- // from windowmanager. Currently, we need to ensure the setInputWindows completes,
- // which would force the focus event to be queued before the current key event.
- // TODO(b/70668286): post call to 'moveDisplayToTop' to mHandler instead
- Log.i(TAG, "Moving non-focused display " + displayId + " to top "
- + "because a key is targeting it");
- mWindowManagerFuncs.moveDisplayToTop(displayId);
- }
+ // If the key event is targeted to a specific display, then the user is interacting with
+ // that display. Therefore, try to give focus to the display that the user is interacting
+ // with.
+ if ((result & ACTION_PASS_TO_USER) != 0 && displayId != INVALID_DISPLAY
+ && displayId != mTopFocusedDisplayId) {
+ // An event is targeting a non-focused display. Move the display to top so that
+ // it can become the focused display to interact with the user.
+ // This should be done asynchronously, once the focus logic is fully moved to input
+ // from windowmanager. Currently, we need to ensure the setInputWindows completes,
+ // which would force the focus event to be queued before the current key event.
+ // TODO(b/70668286): post call to 'moveDisplayToTop' to mHandler instead
+ Log.i(TAG, "Moving non-focused display " + displayId + " to top "
+ + "because a key is targeting it");
+ mWindowManagerFuncs.moveDisplayToTopIfAllowed(displayId);
}
return result;
diff --git a/services/core/java/com/android/server/policy/WindowManagerPolicy.java b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
index 88ec691..94fb840 100644
--- a/services/core/java/com/android/server/policy/WindowManagerPolicy.java
+++ b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
@@ -330,8 +330,11 @@
/**
* Hint to window manager that the user is interacting with a display that should be treated
* as the top display.
+ *
+ * Calling this method does not guarantee that the display will be moved to top. The window
+ * manager will make the final decision whether or not to move the display.
*/
- void moveDisplayToTop(int displayId);
+ void moveDisplayToTopIfAllowed(int displayId);
/**
* Return whether the app transition state is idle.
diff --git a/services/core/java/com/android/server/power/Notifier.java b/services/core/java/com/android/server/power/Notifier.java
index 09a7b29..326d709 100644
--- a/services/core/java/com/android/server/power/Notifier.java
+++ b/services/core/java/com/android/server/power/Notifier.java
@@ -638,7 +638,7 @@
} catch (RemoteException ex) {
// Ignore
}
- FrameworkStatsLog.write(FrameworkStatsLog.DISPLAY_WAKE_REPORTED, reason);
+ FrameworkStatsLog.write(FrameworkStatsLog.DISPLAY_WAKE_REPORTED, reason, reasonUid);
}
/**
diff --git a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
index f173f7a..966e883 100644
--- a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
+++ b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
@@ -34,6 +34,7 @@
import android.media.tv.AdResponse;
import android.media.tv.BroadcastInfoRequest;
import android.media.tv.BroadcastInfoResponse;
+import android.media.tv.TvRecordingInfo;
import android.media.tv.TvTrackInfo;
import android.media.tv.interactive.AppLinkInfo;
import android.media.tv.interactive.ITvInteractiveAppClient;
@@ -1369,6 +1370,58 @@
}
@Override
+ public void sendTvRecordingInfo(IBinder sessionToken, TvRecordingInfo recordingInfo,
+ int userId) {
+ if (DEBUG) {
+ Slogf.d(TAG, "sendTvRecordingInfo(recordingInfo=%s)", recordingInfo);
+ }
+ final int callingUid = Binder.getCallingUid();
+ final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
+ userId, "sendTvRecordingInfo");
+ SessionState sessionState = null;
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ try {
+ sessionState = getSessionStateLocked(sessionToken, callingUid,
+ resolvedUserId);
+ getSessionLocked(sessionState).sendTvRecordingInfo(recordingInfo);
+ } catch (RemoteException | SessionNotFoundException e) {
+ Slogf.e(TAG, "error in sendTvRecordingInfo", e);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public void sendTvRecordingInfoList(IBinder sessionToken,
+ List<TvRecordingInfo> recordingInfoList, int userId) {
+ if (DEBUG) {
+ Slogf.d(TAG, "sendTvRecordingInfoList(type=%s)", recordingInfoList);
+ }
+ final int callingUid = Binder.getCallingUid();
+ final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
+ userId, "sendTvRecordingInfoList");
+ SessionState sessionState = null;
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ try {
+ sessionState = getSessionStateLocked(sessionToken, callingUid,
+ resolvedUserId);
+ getSessionLocked(sessionState).sendTvRecordingInfoList(recordingInfoList);
+ } catch (RemoteException | SessionNotFoundException e) {
+ Slogf.e(TAG, "error in sendTvRecordingInfoList", e);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
public void sendSigningResult(
IBinder sessionToken, String signingId, byte[] result, int userId) {
if (DEBUG) {
@@ -2315,6 +2368,59 @@
}
@Override
+ public void onSetTvRecordingInfo(String recordingId, TvRecordingInfo recordingInfo) {
+ synchronized (mLock) {
+ if (DEBUG) {
+ Slogf.d(TAG, "onSetTvRecordingInfo");
+ }
+ if (mSessionState.mSession == null || mSessionState.mClient == null) {
+ return;
+ }
+ try {
+ mSessionState.mClient.onSetTvRecordingInfo(recordingId, recordingInfo,
+ mSessionState.mSeq);
+ } catch (RemoteException e) {
+ Slogf.e(TAG, "error in onSetTvRecordingInfo", e);
+ }
+ }
+ }
+
+ @Override
+ public void onRequestTvRecordingInfo(String recordingId) {
+ synchronized (mLock) {
+ if (DEBUG) {
+ Slogf.d(TAG, "onRequestTvRecordingInfo");
+ }
+ if (mSessionState.mSession == null || mSessionState.mClient == null) {
+ return;
+ }
+ try {
+ mSessionState.mClient.onRequestTvRecordingInfo(recordingId, mSessionState.mSeq);
+ } catch (RemoteException e) {
+ Slogf.e(TAG, "error in onRequestTvRecordingInfo", e);
+ }
+ }
+ }
+
+ @Override
+ public void onRequestTvRecordingInfoList(int type) {
+ synchronized (mLock) {
+ if (DEBUG) {
+ Slogf.d(TAG, "onRequestTvRecordingInfoList");
+ }
+ if (mSessionState.mSession == null || mSessionState.mClient == null) {
+ return;
+ }
+ try {
+ mSessionState.mClient.onRequestTvRecordingInfoList(type, mSessionState.mSeq);
+ } catch (RemoteException e) {
+ Slogf.e(TAG, "error in onRequestTvRecordingInfoList", e);
+ }
+ }
+ }
+
+
+ @Override
public void onRequestSigning(String id, String algorithm, String alias, byte[] data) {
synchronized (mLock) {
if (DEBUG) {
diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java
index 4fef2a8..e2ab216 100644
--- a/services/core/java/com/android/server/wm/AccessibilityController.java
+++ b/services/core/java/com/android/server/wm/AccessibilityController.java
@@ -141,7 +141,7 @@
private final SparseArray<WindowsForAccessibilityObserver> mWindowsForAccessibilityObserver =
new SparseArray<>();
private SparseArray<IBinder> mFocusedWindow = new SparseArray<>();
- private int mFocusedDisplay = -1;
+ private int mFocusedDisplay = Display.INVALID_DISPLAY;
private final SparseBooleanArray mIsImeVisibleArray = new SparseBooleanArray();
// Set to true if initializing window population complete.
private boolean mAllObserversInitialized = true;
diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java
index f683d0a..a16e659 100644
--- a/services/core/java/com/android/server/wm/ActivityClientController.java
+++ b/services/core/java/com/android/server/wm/ActivityClientController.java
@@ -100,7 +100,6 @@
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;
import com.android.server.Watchdog;
import com.android.server.pm.KnownPackages;
@@ -191,6 +190,15 @@
}
@Override
+ public void activityRefreshed(IBinder token) {
+ final long origId = Binder.clearCallingIdentity();
+ synchronized (mGlobalLock) {
+ ActivityRecord.activityRefreshedLocked(token);
+ }
+ Binder.restoreCallingIdentity(origId);
+ }
+
+ @Override
public void activityTopResumedStateLost() {
final long origId = Binder.clearCallingIdentity();
synchronized (mGlobalLock) {
@@ -482,46 +490,13 @@
finishTask == Activity.FINISH_TASK_WITH_ROOT_ACTIVITY;
if (finishTask == Activity.FINISH_TASK_WITH_ACTIVITY
|| (finishWithRootActivity && r == rootR)) {
- ActivityRecord topActivity =
- r.getTask().getTopNonFinishingActivity();
- boolean passesAsmChecks = topActivity != null
- && topActivity.getUid() == r.getUid();
- if (!passesAsmChecks) {
- Slog.i(TAG, "Finishing task from background. r: " + r);
- FrameworkStatsLog.write(FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED,
- /* caller_uid */
- r.getUid(),
- /* caller_activity_class_name */
- r.info.name,
- /* target_task_top_activity_uid */
- topActivity == null ? -1 : topActivity.getUid(),
- /* target_task_top_activity_class_name */
- topActivity == null ? null : topActivity.info.name,
- /* target_task_is_different */
- false,
- /* target_activity_uid */
- -1,
- /* target_activity_class_name */
- null,
- /* target_intent_action */
- null,
- /* target_intent_flags */
- 0,
- /* action */
- FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED__ACTION__FINISH_TASK,
- /* version */
- 1,
- /* multi_window */
- false
- );
- }
// If requested, remove the task that is associated to this activity only if it
// was the root activity in the task. The result code and data is ignored
// because we don't support returning them across task boundaries. Also, to
// keep backwards compatibility we remove the task from recents when finishing
// task with root activity.
mTaskSupervisor.removeTask(tr, false /*killProcess*/,
- finishWithRootActivity, "finish-activity");
+ finishWithRootActivity, "finish-activity", r.getUid(), r.info.name);
res = true;
// Explicitly dismissing the activity so reset its relaunch flag.
r.mRelaunchReason = RELAUNCH_REASON_NONE;
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index d4b9267..c1a68de 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -736,7 +736,6 @@
*/
private boolean mWillCloseOrEnterPip;
- @VisibleForTesting
final LetterboxUiController mLetterboxUiController;
/**
@@ -6076,6 +6075,19 @@
r.mDisplayContent.mUnknownAppVisibilityController.notifyAppResumedFinished(r);
}
+ static void activityRefreshedLocked(IBinder token) {
+ final ActivityRecord r = ActivityRecord.forTokenLocked(token);
+ ProtoLog.i(WM_DEBUG_STATES, "Refreshed activity: %s", r);
+ if (r == null) {
+ // In case the record on server side has been removed (e.g. destroy timeout)
+ // and the token could be null.
+ return;
+ }
+ if (r.mDisplayContent.mDisplayRotationCompatPolicy != null) {
+ r.mDisplayContent.mDisplayRotationCompatPolicy.onActivityRefreshed(r);
+ }
+ }
+
static void splashScreenAttachedLocked(IBinder token) {
final ActivityRecord r = ActivityRecord.forTokenLocked(token);
if (r == null) {
@@ -8149,7 +8161,7 @@
}
/**
- * Adjusts position of resolved bounds if they doesn't fill the parent using gravity
+ * Adjusts position of resolved bounds if they don't fill the parent using gravity
* requested in the config or via an ADB command. For more context see {@link
* LetterboxUiController#getHorizontalPositionMultiplier(Configuration)} and
* {@link LetterboxUiController#getVerticalPositionMultiplier(Configuration)}
@@ -8168,9 +8180,8 @@
int offsetX = 0;
if (parentBounds.width() != screenResolvedBounds.width()) {
if (screenResolvedBounds.width() <= parentAppBounds.width()) {
- float positionMultiplier =
- mLetterboxUiController.getHorizontalPositionMultiplier(
- newParentConfiguration);
+ float positionMultiplier = mLetterboxUiController.getHorizontalPositionMultiplier(
+ newParentConfiguration);
offsetX = (int) Math.ceil((parentAppBounds.width() - screenResolvedBounds.width())
* positionMultiplier);
}
@@ -8180,9 +8191,8 @@
int offsetY = 0;
if (parentBounds.height() != screenResolvedBounds.height()) {
if (screenResolvedBounds.height() <= parentAppBounds.height()) {
- float positionMultiplier =
- mLetterboxUiController.getVerticalPositionMultiplier(
- newParentConfiguration);
+ float positionMultiplier = mLetterboxUiController.getVerticalPositionMultiplier(
+ newParentConfiguration);
offsetY = (int) Math.ceil((parentAppBounds.height() - screenResolvedBounds.height())
* positionMultiplier);
}
@@ -9153,6 +9163,8 @@
} else {
scheduleConfigurationChanged(newMergedOverrideConfig);
}
+ notifyDisplayCompatPolicyAboutConfigurationChange(
+ mLastReportedConfiguration.getMergedConfiguration(), mTmpConfig);
return true;
}
@@ -9221,11 +9233,24 @@
} else {
scheduleConfigurationChanged(newMergedOverrideConfig);
}
+ notifyDisplayCompatPolicyAboutConfigurationChange(
+ mLastReportedConfiguration.getMergedConfiguration(), mTmpConfig);
+
stopFreezingScreenLocked(false);
return true;
}
+ private void notifyDisplayCompatPolicyAboutConfigurationChange(
+ Configuration newConfig, Configuration lastReportedConfig) {
+ if (mDisplayContent.mDisplayRotationCompatPolicy == null
+ || !shouldBeResumed(/* activeActivity */ null)) {
+ return;
+ }
+ mDisplayContent.mDisplayRotationCompatPolicy.onActivityConfigurationChanging(
+ this, newConfig, lastReportedConfig);
+ }
+
/** Get process configuration, or global config if the process is not set. */
private Configuration getProcessGlobalConfiguration() {
return app != null ? app.getConfiguration() : mAtmService.getGlobalConfiguration();
@@ -9334,7 +9359,8 @@
preserveWindow);
final ActivityLifecycleItem lifecycleItem;
if (andResume) {
- lifecycleItem = ResumeActivityItem.obtain(isTransitionForward());
+ lifecycleItem = ResumeActivityItem.obtain(isTransitionForward(),
+ shouldSendCompatFakeFocus());
} else {
lifecycleItem = PauseActivityItem.obtain();
}
@@ -10095,6 +10121,18 @@
}
}
+ /**
+ * Whether we should send fake focus when the activity is resumed. This is done because some
+ * game engines wait to get focus before drawing the content of the app.
+ */
+ // TODO(b/263593361): Explore enabling compat fake focus for freeform.
+ // TODO(b/263592337): Explore enabling compat fake focus for fullscreen, e.g. for when
+ // covered with bubbles.
+ boolean shouldSendCompatFakeFocus() {
+ return mWmService.mLetterboxConfiguration.isCompatFakeFocusEnabled() && inMultiWindowMode()
+ && !inPinnedWindowingMode() && !inFreeformWindowingMode();
+ }
+
static class Builder {
private final ActivityTaskManagerService mAtmService;
private WindowProcessController mCallerApp;
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index eb3fdca..b4af69e 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -74,6 +74,8 @@
import static com.android.server.wm.ActivityTaskSupervisor.DEFER_RESUME;
import static com.android.server.wm.ActivityTaskSupervisor.ON_TOP;
import static com.android.server.wm.ActivityTaskSupervisor.PRESERVE_WINDOWS;
+import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_DEFAULT;
+import static com.android.server.wm.BackgroundActivityStartController.BAL_BLOCK;
import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.PHASE_BOUNDS;
import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.PHASE_DISPLAY;
import static com.android.server.wm.Task.REPARENT_MOVE_ROOT_TASK_TO_FRONT;
@@ -130,6 +132,7 @@
import com.android.server.statusbar.StatusBarManagerInternal;
import com.android.server.uri.NeededUriGrants;
import com.android.server.wm.ActivityMetricsLogger.LaunchingState;
+import com.android.server.wm.BackgroundActivityStartController.BalCode;
import com.android.server.wm.LaunchParamsController.LaunchParams;
import com.android.server.wm.TaskFragment.EmbeddingCheckResult;
@@ -171,9 +174,10 @@
private int mCallingUid;
private ActivityOptions mOptions;
- // If it is true, background activity can only be started in an existing task that contains
+ // If it is BAL_BLOCK, background activity can only be started in an existing task that contains
// an activity with same uid, or if activity starts are enabled in developer options.
- private boolean mRestrictedBgActivity;
+ @BalCode
+ private int mBalCode;
private int mLaunchMode;
private boolean mLaunchTaskBehind;
@@ -590,7 +594,7 @@
mIntent = starter.mIntent;
mCallingUid = starter.mCallingUid;
mOptions = starter.mOptions;
- mRestrictedBgActivity = starter.mRestrictedBgActivity;
+ mBalCode = starter.mBalCode;
mLaunchTaskBehind = starter.mLaunchTaskBehind;
mLaunchFlags = starter.mLaunchFlags;
@@ -1024,15 +1028,15 @@
ActivityOptions checkedOptions = options != null
? options.getOptions(intent, aInfo, callerApp, mSupervisor) : null;
- boolean restrictedBgActivity = false;
+ @BalCode int balCode = BAL_ALLOW_DEFAULT;
if (!abort) {
try {
Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER,
"shouldAbortBackgroundActivityStart");
BackgroundActivityStartController balController =
mController.getBackgroundActivityLaunchController();
- restrictedBgActivity =
- balController.shouldAbortBackgroundActivityStart(
+ balCode =
+ balController.checkBackgroundActivityStart(
callingUid,
callingPid,
callingPackage,
@@ -1221,13 +1225,13 @@
WindowProcessController homeProcess = mService.mHomeProcess;
boolean isHomeProcess = homeProcess != null
&& aInfo.applicationInfo.uid == homeProcess.mUid;
- if (!restrictedBgActivity && !isHomeProcess) {
+ if (balCode != BAL_BLOCK && !isHomeProcess) {
mService.resumeAppSwitches();
}
mLastStartActivityResult = startActivityUnchecked(r, sourceRecord, voiceSession,
request.voiceInteractor, startFlags, checkedOptions,
- inTask, inTaskFragment, restrictedBgActivity, intentGrants);
+ inTask, inTaskFragment, balCode, intentGrants);
if (request.outActivity != null) {
request.outActivity[0] = mLastStartActivityRecord;
@@ -1377,7 +1381,7 @@
private int startActivityUnchecked(final ActivityRecord r, ActivityRecord sourceRecord,
IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
int startFlags, ActivityOptions options, Task inTask,
- TaskFragment inTaskFragment, boolean restrictedBgActivity,
+ TaskFragment inTaskFragment, @BalCode int balCode,
NeededUriGrants intentGrants) {
int result = START_CANCELED;
final Task startedActivityRootTask;
@@ -1397,7 +1401,7 @@
try {
Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "startActivityInner");
result = startActivityInner(r, sourceRecord, voiceSession, voiceInteractor,
- startFlags, options, inTask, inTaskFragment, restrictedBgActivity,
+ startFlags, options, inTask, inTaskFragment, balCode,
intentGrants);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
@@ -1544,10 +1548,10 @@
int startActivityInner(final ActivityRecord r, ActivityRecord sourceRecord,
IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
int startFlags, ActivityOptions options, Task inTask,
- TaskFragment inTaskFragment, boolean restrictedBgActivity,
+ TaskFragment inTaskFragment, @BalCode int balCode,
NeededUriGrants intentGrants) {
setInitialState(r, options, inTask, inTaskFragment, startFlags, sourceRecord,
- voiceSession, voiceInteractor, restrictedBgActivity);
+ voiceSession, voiceInteractor, balCode);
computeLaunchingTaskFlags();
mIntent.setFlags(mLaunchFlags);
@@ -1663,6 +1667,12 @@
pmInternal.grantImplicitAccess(mStartActivity.mUserId, mIntent,
UserHandle.getAppId(mStartActivity.info.applicationInfo.uid) /*recipient*/,
resultToUid /*visible*/, true /*direct*/);
+ } else if (mStartActivity.mShareIdentity) {
+ final PackageManagerInternal pmInternal =
+ mService.getPackageManagerInternalLocked();
+ pmInternal.grantImplicitAccess(mStartActivity.mUserId, mIntent,
+ UserHandle.getAppId(mStartActivity.info.applicationInfo.uid) /*recipient*/,
+ r.launchedFromUid /*visible*/, true /*direct*/);
}
final Task startedTask = mStartActivity.getTask();
if (newTask) {
@@ -1794,7 +1804,8 @@
|| !targetTask.isUidPresent(mCallingUid)
|| (LAUNCH_SINGLE_INSTANCE == mLaunchMode && targetTask.inPinnedWindowingMode()));
- if (mRestrictedBgActivity && blockBalInTask && handleBackgroundActivityAbort(r)) {
+ if (mBalCode == BAL_BLOCK && blockBalInTask
+ && handleBackgroundActivityAbort(r)) {
Slog.e(TAG, "Abort background activity starts from " + mCallingUid);
return START_ABORTED;
}
@@ -2203,7 +2214,7 @@
mIntent = null;
mCallingUid = -1;
mOptions = null;
- mRestrictedBgActivity = false;
+ mBalCode = BAL_ALLOW_DEFAULT;
mLaunchTaskBehind = false;
mLaunchFlags = 0;
@@ -2248,7 +2259,7 @@
private void setInitialState(ActivityRecord r, ActivityOptions options, Task inTask,
TaskFragment inTaskFragment, int startFlags,
ActivityRecord sourceRecord, IVoiceInteractionSession voiceSession,
- IVoiceInteractor voiceInteractor, boolean restrictedBgActivity) {
+ IVoiceInteractor voiceInteractor, @BalCode int balCode) {
reset(false /* clearRequest */);
mStartActivity = r;
@@ -2259,7 +2270,7 @@
mSourceRootTask = mSourceRecord != null ? mSourceRecord.getRootTask() : null;
mVoiceSession = voiceSession;
mVoiceInteractor = voiceInteractor;
- mRestrictedBgActivity = restrictedBgActivity;
+ mBalCode = balCode;
mLaunchParams.reset();
@@ -2412,7 +2423,7 @@
mNoAnimation = (mLaunchFlags & FLAG_ACTIVITY_NO_ANIMATION) != 0;
- if (mRestrictedBgActivity && !mService.isBackgroundActivityStartsEnabled()) {
+ if (mBalCode == BAL_BLOCK && !mService.isBackgroundActivityStartsEnabled()) {
mAvoidMoveToFront = true;
mDoResume = false;
}
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java
index c63bd52..ef126a9 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java
@@ -29,6 +29,7 @@
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.res.CompatibilityInfo;
+import android.content.res.Configuration;
import android.os.Bundle;
import android.os.IBinder;
import android.os.LocaleList;
@@ -622,10 +623,19 @@
@Nullable
public final LocaleList mLocales;
+ /**
+ * Gender for the application, null if app-specific grammatical gender is not set.
+ */
+ @Nullable
+ public final @Configuration.GrammaticalGender
+ Integer mGrammaticalGender;
+
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
- public PackageConfig(Integer nightMode, LocaleList locales) {
+ public PackageConfig(Integer nightMode, LocaleList locales,
+ @Configuration.GrammaticalGender Integer grammaticalGender) {
mNightMode = nightMode;
mLocales = locales;
+ mGrammaticalGender = grammaticalGender;
}
/**
@@ -660,6 +670,13 @@
PackageConfigurationUpdater setLocales(LocaleList locales);
/**
+ * Sets the gender for the current application. This setting is persisted and will
+ * override the system configuration for this application.
+ */
+ PackageConfigurationUpdater setGrammaticalGender(
+ @Configuration.GrammaticalGender int gender);
+
+ /**
* Commit changes.
* @return true if the configuration changes were persisted,
* false if there were no changes, or if erroneous inputs were provided, such as:
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 1180df7..f4d76c2 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -65,6 +65,7 @@
import static android.provider.Settings.System.FONT_SCALE;
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_PIP;
import static android.view.WindowManagerPolicyConstants.KEYGUARD_GOING_AWAY_FLAG_TO_LAUNCHER_CLEAR_SNAPSHOT;
@@ -2843,7 +2844,7 @@
}
@Override
- public boolean resizeTask(int taskId, Rect bounds, int resizeMode) {
+ public void resizeTask(int taskId, Rect bounds, int resizeMode) {
enforceTaskPermission("resizeTask()");
final long ident = Binder.clearCallingIdentity();
try {
@@ -2852,19 +2853,48 @@
MATCH_ATTACHED_TASK_ONLY);
if (task == null) {
Slog.w(TAG, "resizeTask: taskId=" + taskId + " not found");
- return false;
+ return;
}
if (!task.getWindowConfiguration().canResizeTask()) {
Slog.w(TAG, "resizeTask not allowed on task=" + task);
- return false;
+ return;
}
// Reparent the task to the right root task if necessary
boolean preserveWindow = (resizeMode & RESIZE_MODE_PRESERVE_WINDOW) != 0;
- // After reparenting (which only resizes the task to the root task bounds),
- // resize the task to the actual bounds provided
- return task.resize(bounds, resizeMode, preserveWindow);
+ if (!getTransitionController().isShellTransitionsEnabled()) {
+ // After reparenting (which only resizes the task to the root task bounds),
+ // resize the task to the actual bounds provided
+ task.resize(bounds, resizeMode, preserveWindow);
+ return;
+ }
+
+ final Transition transition = new Transition(TRANSIT_CHANGE, 0 /* flags */,
+ getTransitionController(), mWindowManager.mSyncEngine);
+ if (mWindowManager.mSyncEngine.hasActiveSync()) {
+ mWindowManager.mSyncEngine.queueSyncSet(
+ () -> getTransitionController().moveToCollecting(transition),
+ () -> {
+ if (!task.getWindowConfiguration().canResizeTask()) {
+ Slog.w(TAG, "resizeTask not allowed on task=" + task);
+ transition.abort();
+ return;
+ }
+ getTransitionController().requestStartTransition(transition, task,
+ null /* remoteTransition */, null /* displayChange */);
+ getTransitionController().collect(task);
+ task.resize(bounds, resizeMode, preserveWindow);
+ transition.setReady(task, true);
+ });
+ } else {
+ getTransitionController().moveToCollecting(transition);
+ getTransitionController().requestStartTransition(transition, task,
+ null /* remoteTransition */, null /* displayChange */);
+ getTransitionController().collect(task);
+ task.resize(bounds, resizeMode, preserveWindow);
+ transition.setReady(task, true);
+ }
}
} finally {
Binder.restoreCallingIdentity(ident);
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index 42da2a5..473a6e5 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -46,6 +46,7 @@
import static android.os.Process.SYSTEM_UID;
import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.Display.INVALID_DISPLAY;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STATES;
@@ -98,6 +99,7 @@
import android.app.servertransaction.LaunchActivityItem;
import android.app.servertransaction.PauseActivityItem;
import android.app.servertransaction.ResumeActivityItem;
+import android.companion.virtual.VirtualDeviceManager;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ActivityInfo;
@@ -140,6 +142,7 @@
import com.android.internal.content.ReferrerIntent;
import com.android.internal.protolog.common.ProtoLog;
import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.FrameworkStatsLog;
import com.android.internal.util.function.pooled.PooledLambda;
import com.android.server.LocalServices;
import com.android.server.am.ActivityManagerService;
@@ -252,6 +255,7 @@
private WindowManagerService mWindowManager;
private AppOpsManager mAppOpsManager;
+ private VirtualDeviceManager mVirtualDeviceManager;
/** Common synchronization logic used to save things to disks. */
PersisterQueue mPersisterQueue;
@@ -895,12 +899,14 @@
final boolean isTransitionForward = r.isTransitionForward();
final IBinder fragmentToken = r.getTaskFragment().getFragmentToken();
+
+ final int deviceId = getDeviceIdForDisplayId(r.getDisplayId());
clientTransaction.addCallback(LaunchActivityItem.obtain(new Intent(r.intent),
System.identityHashCode(r), r.info,
// TODO: Have this take the merged configuration instead of separate global
// and override configs.
mergedConfiguration.getGlobalConfiguration(),
- mergedConfiguration.getOverrideConfiguration(),
+ mergedConfiguration.getOverrideConfiguration(), deviceId,
r.getFilteredReferrer(r.launchedFromPackage), task.voiceInteractor,
proc.getReportedProcState(), r.getSavedState(), r.getPersistentSavedState(),
results, newIntents, r.takeOptions(), isTransitionForward,
@@ -910,7 +916,8 @@
// Set desired final state.
final ActivityLifecycleItem lifecycleItem;
if (andResume) {
- lifecycleItem = ResumeActivityItem.obtain(isTransitionForward);
+ lifecycleItem = ResumeActivityItem.obtain(isTransitionForward,
+ r.shouldSendCompatFakeFocus());
} else {
lifecycleItem = PauseActivityItem.obtain();
}
@@ -1216,6 +1223,17 @@
}
}
+ int getDeviceIdForDisplayId(int displayId) {
+ if (displayId == DEFAULT_DISPLAY || displayId == INVALID_DISPLAY) {
+ return VirtualDeviceManager.DEVICE_ID_DEFAULT;
+ }
+ if (mVirtualDeviceManager == null) {
+ mVirtualDeviceManager =
+ mService.mContext.getSystemService(VirtualDeviceManager.class);
+ }
+ return mVirtualDeviceManager.getDeviceIdForDisplayId(displayId);
+ }
+
private AppOpsManager getAppOpsManager() {
if (mAppOpsManager == null) {
mAppOpsManager = mService.mContext.getSystemService(AppOpsManager.class);
@@ -1590,11 +1608,11 @@
* @return Returns true if the given task was found and removed.
*/
boolean removeTaskById(int taskId, boolean killProcess, boolean removeFromRecents,
- String reason) {
+ String reason, int callingUid) {
final Task task =
mRootWindowContainer.anyTaskForId(taskId, MATCH_ATTACHED_TASK_OR_RECENT_TASKS);
if (task != null) {
- removeTask(task, killProcess, removeFromRecents, reason);
+ removeTask(task, killProcess, removeFromRecents, reason, callingUid, null);
return true;
}
Slog.w(TAG, "Request to remove task ignored for non-existent task " + taskId);
@@ -1602,10 +1620,52 @@
}
void removeTask(Task task, boolean killProcess, boolean removeFromRecents, String reason) {
+ removeTask(task, killProcess, removeFromRecents, reason, SYSTEM_UID, null);
+ }
+
+ void removeTask(Task task, boolean killProcess, boolean removeFromRecents, String reason,
+ int callingUid, String callerActivityClassName) {
if (task.mInRemoveTask) {
// Prevent recursion.
return;
}
+ // We may have already checked that the callingUid has additional clearTask privileges, and
+ // cleared the calling identify. If so, we infer we do not need further restrictions here.
+ // TODO(b/263368846) Move to live with the rest of the ASM logic.
+ if (callingUid != SYSTEM_UID) {
+ ActivityRecord topActivity = task.getTopNonFinishingActivity();
+ boolean passesAsmChecks = topActivity != null
+ && topActivity.getUid() == callingUid;
+ if (!passesAsmChecks) {
+ Slog.i(TAG, "Finishing task from background. t: " + task);
+ FrameworkStatsLog.write(FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED,
+ /* caller_uid */
+ callingUid,
+ /* caller_activity_class_name */
+ callerActivityClassName,
+ /* target_task_top_activity_uid */
+ topActivity == null ? -1 : topActivity.getUid(),
+ /* target_task_top_activity_class_name */
+ topActivity == null ? null : topActivity.info.name,
+ /* target_task_is_different */
+ false,
+ /* target_activity_uid */
+ -1,
+ /* target_activity_class_name */
+ null,
+ /* target_intent_action */
+ null,
+ /* target_intent_flags */
+ 0,
+ /* action */
+ FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED__ACTION__FINISH_TASK,
+ /* version */
+ 1,
+ /* multi_window */
+ false
+ );
+ }
+ }
task.mTransitionController.requestCloseTransitionIfNeeded(task);
task.mInRemoveTask = true;
try {
@@ -1728,7 +1788,7 @@
// Task was trimmed from the recent tasks list -- remove the active task record as well
// since the user won't really be able to go back to it
removeTaskById(task.mTaskId, killProcess, false /* removeFromRecents */,
- "recent-task-trimmed");
+ "recent-task-trimmed", SYSTEM_UID);
}
task.removedFromRecents();
}
diff --git a/services/core/java/com/android/server/wm/AppTaskImpl.java b/services/core/java/com/android/server/wm/AppTaskImpl.java
index b160af6..7bd8c53 100644
--- a/services/core/java/com/android/server/wm/AppTaskImpl.java
+++ b/services/core/java/com/android/server/wm/AppTaskImpl.java
@@ -72,15 +72,16 @@
checkCallerOrSystemOrRoot();
synchronized (mService.mGlobalLock) {
- final long origId = Binder.clearCallingIdentity();
+ int origCallingUid = Binder.getCallingUid();
+ final long callingIdentity = Binder.clearCallingIdentity();
try {
// We remove the task from recents to preserve backwards
if (!mService.mTaskSupervisor.removeTaskById(mTaskId, false,
- REMOVE_FROM_RECENTS, "finish-and-remove-task")) {
+ REMOVE_FROM_RECENTS, "finish-and-remove-task", origCallingUid)) {
throw new IllegalArgumentException("Unable to find task ID " + mTaskId);
}
} finally {
- Binder.restoreCallingIdentity(origId);
+ Binder.restoreCallingIdentity(callingIdentity);
}
}
}
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index d515a27..2315795 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -25,6 +25,9 @@
import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_ALLOW;
import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_FG_ONLY;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.annotation.IntDef;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ActivityOptions;
@@ -39,6 +42,8 @@
import com.android.server.am.PendingIntentRecord;
+import java.lang.annotation.Retention;
+
/**
* Helper class to check permissions for starting Activities.
*
@@ -52,6 +57,56 @@
private final ActivityTaskManagerService mService;
private final ActivityTaskSupervisor mSupervisor;
+ // TODO(b/263368846) Rename when ASM logic is moved in
+ @Retention(SOURCE)
+ @IntDef({BAL_BLOCK,
+ BAL_ALLOW_DEFAULT,
+ BAL_ALLOW_ALLOWLISTED_UID,
+ BAL_ALLOW_ALLOWLISTED_COMPONENT,
+ BAL_ALLOW_VISIBLE_WINDOW,
+ BAL_ALLOW_PENDING_INTENT,
+ BAL_ALLOW_BAL_PERMISSION,
+ BAL_ALLOW_SAW_PERMISSION,
+ BAL_ALLOW_GRACE_PERIOD,
+ BAL_ALLOW_FOREGROUND,
+ BAL_ALLOW_SDK_SANDBOX
+ })
+ public @interface BalCode {}
+
+ static final int BAL_BLOCK = 0;
+
+ static final int BAL_ALLOW_DEFAULT = 1;
+
+ // Following codes are in order of precedence
+
+ /** Important UIDs which should be always allowed to launch activities */
+ static final int BAL_ALLOW_ALLOWLISTED_UID = 2;
+
+ /** Apps that fulfill a certain role that can can always launch new tasks */
+ static final int BAL_ALLOW_ALLOWLISTED_COMPONENT = 3;
+
+ /** Apps which currently have a visible window */
+ static final int BAL_ALLOW_VISIBLE_WINDOW = 4;
+
+ /** Allowed due to the PendingIntent sender */
+ static final int BAL_ALLOW_PENDING_INTENT = 5;
+
+ /** App has START_ACTIVITIES_FROM_BACKGROUND permission or BAL instrumentation privileges
+ * granted to it */
+ static final int BAL_ALLOW_BAL_PERMISSION = 6;
+
+ /** Process has SYSTEM_ALERT_WINDOW permission granted to it */
+ static final int BAL_ALLOW_SAW_PERMISSION = 7;
+
+ /** App is in grace period after an activity was started or finished */
+ static final int BAL_ALLOW_GRACE_PERIOD = 8;
+
+ /** App is in a foreground task or bound to a foreground service (but not itself visible) */
+ static final int BAL_ALLOW_FOREGROUND = 9;
+
+ /** Process belongs to a SDK sandbox */
+ static final int BAL_ALLOW_SDK_SANDBOX = 10;
+
BackgroundActivityStartController(
final ActivityTaskManagerService service, final ActivityTaskSupervisor supervisor) {
mService = service;
@@ -83,6 +138,27 @@
boolean allowBackgroundActivityStart,
Intent intent,
ActivityOptions checkedOptions) {
+ return checkBackgroundActivityStart(callingUid, callingPid, callingPackage,
+ realCallingUid, realCallingPid, callerApp, originatingPendingIntent,
+ allowBackgroundActivityStart, intent, checkedOptions) == BAL_BLOCK;
+ }
+
+ /**
+ * @return A code denoting which BAL rule allows an activity to be started,
+ * or {@link BAL_BLOCK} if the launch should be blocked
+ */
+ @BalCode
+ int checkBackgroundActivityStart(
+ int callingUid,
+ int callingPid,
+ final String callingPackage,
+ int realCallingUid,
+ int realCallingPid,
+ WindowProcessController callerApp,
+ PendingIntentRecord originatingPendingIntent,
+ boolean allowBackgroundActivityStart,
+ Intent intent,
+ ActivityOptions checkedOptions) {
// don't abort for the most important UIDs
final int callingAppId = UserHandle.getAppId(callingUid);
final boolean useCallingUidState =
@@ -93,32 +169,22 @@
if (callingUid == Process.ROOT_UID
|| callingAppId == Process.SYSTEM_UID
|| callingAppId == Process.NFC_UID) {
- if (DEBUG_ACTIVITY_STARTS) {
- Slog.d(
- TAG,
- "Activity start allowed for important callingUid (" + callingUid + ")");
- }
- return false;
+ return logStartAllowedAndReturnCode(/*background*/ false, callingUid,
+ BAL_ALLOW_ALLOWLISTED_UID, "Important callingUid");
}
// Always allow home application to start activities.
if (isHomeApp(callingUid, callingPackage)) {
- if (DEBUG_ACTIVITY_STARTS) {
- Slog.d(
- TAG,
- "Activity start allowed for home app callingUid (" + callingUid + ")");
- }
- return false;
+ return logStartAllowedAndReturnCode(/*background*/ false, callingUid,
+ BAL_ALLOW_ALLOWLISTED_COMPONENT, "Home app");
}
// IME should always be allowed to start activity, like IME settings.
final WindowState imeWindow =
mService.mRootWindowContainer.getCurrentInputMethodWindow();
if (imeWindow != null && callingAppId == imeWindow.mOwnerUid) {
- if (DEBUG_ACTIVITY_STARTS) {
- Slog.d(TAG, "Activity start allowed for active ime (" + callingUid + ")");
- }
- return false;
+ return logStartAllowedAndReturnCode(/*background*/ false, callingUid,
+ BAL_ALLOW_ALLOWLISTED_COMPONENT, "Active ime");
}
}
@@ -145,15 +211,12 @@
&& callingUidHasAnyVisibleWindow)
|| isCallingUidPersistentSystemProcess;
if (useCallingUidState && allowCallingUidStartActivity) {
- if (DEBUG_ACTIVITY_STARTS) {
- Slog.d(
- TAG,
- "Activity start allowed: callingUidHasAnyVisibleWindow = "
- + callingUid
- + ", isCallingUidPersistentSystemProcess = "
- + isCallingUidPersistentSystemProcess);
- }
- return false;
+ return logStartAllowedAndReturnCode(/*background*/ false,
+ BAL_ALLOW_VISIBLE_WINDOW,
+ "callingUidHasAnyVisibleWindow = "
+ + callingUid
+ + ", isCallingUidPersistentSystemProcess = "
+ + isCallingUidPersistentSystemProcess);
}
// take realCallingUid into consideration
final int realCallingUidProcState =
@@ -184,14 +247,9 @@
Process.getAppUidForSdkSandboxUid(UserHandle.getAppId(realCallingUid));
if (mService.hasActiveVisibleWindow(realCallingSdkSandboxUidToAppUid)) {
- if (DEBUG_ACTIVITY_STARTS) {
- Slog.d(
- TAG,
- "Activity start allowed: uid in SDK sandbox ("
- + realCallingUid
- + ") has visible (non-toast) window.");
- }
- return false;
+ return logStartAllowedAndReturnCode(/*background*/ false, realCallingUid,
+ BAL_ALLOW_SDK_SANDBOX,
+ "uid in SDK sandbox has visible (non-toast) window");
}
}
@@ -209,100 +267,60 @@
-1,
true)
== PackageManager.PERMISSION_GRANTED) {
- if (DEBUG_ACTIVITY_STARTS) {
- Slog.d(
- TAG,
- "Activity start allowed: realCallingUid ("
- + realCallingUid
- + ") has BAL permission.");
- }
- return false;
+ return logStartAllowedAndReturnCode(/*background*/ false, callingUid,
+ BAL_ALLOW_PENDING_INTENT,
+ "realCallingUid has BAL permission. realCallingUid: " + realCallingUid);
}
// don't abort if the realCallingUid has a visible window
// TODO(b/171459802): We should check appSwitchAllowed also
if (realCallingUidHasAnyVisibleWindow) {
- if (DEBUG_ACTIVITY_STARTS) {
- Slog.d(
- TAG,
- "Activity start allowed: realCallingUid ("
- + realCallingUid
- + ") has visible (non-toast) window");
- }
- return false;
+ return logStartAllowedAndReturnCode(/*background*/ false,
+ callingUid, BAL_ALLOW_PENDING_INTENT,
+ "realCallingUid has visible (non-toast) window. realCallingUid: "
+ + realCallingUid);
}
// if the realCallingUid is a persistent system process, abort if the IntentSender
// wasn't allowed to start an activity
if (isRealCallingUidPersistentSystemProcess && allowBackgroundActivityStart) {
- if (DEBUG_ACTIVITY_STARTS) {
- Slog.d(
- TAG,
- "Activity start allowed: realCallingUid ("
- + realCallingUid
- + ") is persistent system process AND intent sender allowed "
- + "(allowBackgroundActivityStart = true)");
- }
- return false;
+ return logStartAllowedAndReturnCode(/*background*/ false,
+ callingUid,
+ BAL_ALLOW_PENDING_INTENT,
+ "realCallingUid is persistent system process AND intent "
+ + "sender allowed (allowBackgroundActivityStart = true). "
+ + "realCallingUid: " + realCallingUid);
}
// don't abort if the realCallingUid is an associated companion app
if (mService.isAssociatedCompanionApp(
UserHandle.getUserId(realCallingUid), realCallingUid)) {
- if (DEBUG_ACTIVITY_STARTS) {
- Slog.d(
- TAG,
- "Activity start allowed: realCallingUid ("
- + realCallingUid
- + ") is companion app");
- }
- return false;
+ return logStartAllowedAndReturnCode(/*background*/ false, callingUid,
+ BAL_ALLOW_PENDING_INTENT, "realCallingUid is a companion app. "
+ + "realCallingUid: " + realCallingUid);
}
}
if (useCallingUidState) {
// don't abort if the callingUid has START_ACTIVITIES_FROM_BACKGROUND permission
- if (mService.checkPermission(START_ACTIVITIES_FROM_BACKGROUND, callingPid, callingUid)
- == PERMISSION_GRANTED) {
- if (DEBUG_ACTIVITY_STARTS) {
- Slog.d(
- TAG,
- "Background activity start allowed: START_ACTIVITIES_FROM_BACKGROUND "
- + "permission granted for uid "
- + callingUid);
- }
- return false;
+ if (ActivityTaskManagerService.checkPermission(START_ACTIVITIES_FROM_BACKGROUND,
+ callingPid, callingUid) == PERMISSION_GRANTED) {
+ return logStartAllowedAndReturnCode(/*background*/ true, callingUid,
+ BAL_ALLOW_BAL_PERMISSION,
+ "START_ACTIVITIES_FROM_BACKGROUND permission granted");
}
// don't abort if the caller has the same uid as the recents component
if (mSupervisor.mRecentTasks.isCallerRecents(callingUid)) {
- if (DEBUG_ACTIVITY_STARTS) {
- Slog.d(
- TAG,
- "Background activity start allowed: callingUid ("
- + callingUid
- + ") is recents");
- }
- return false;
+ return logStartAllowedAndReturnCode(/*background*/ true, callingUid,
+ BAL_ALLOW_ALLOWLISTED_COMPONENT, "Recents Component");
}
// don't abort if the callingUid is the device owner
if (mService.isDeviceOwner(callingUid)) {
- if (DEBUG_ACTIVITY_STARTS) {
- Slog.d(
- TAG,
- "Background activity start allowed: callingUid ("
- + callingUid
- + ") is device owner");
- }
- return false;
+ return logStartAllowedAndReturnCode(/*background*/ true, callingUid,
+ BAL_ALLOW_ALLOWLISTED_COMPONENT, "Device Owner");
}
// don't abort if the callingUid has companion device
final int callingUserId = UserHandle.getUserId(callingUid);
if (mService.isAssociatedCompanionApp(callingUserId, callingUid)) {
- if (DEBUG_ACTIVITY_STARTS) {
- Slog.d(
- TAG,
- "Background activity start allowed: callingUid ("
- + callingUid
- + ") is companion app");
- }
- return false;
+ return logStartAllowedAndReturnCode(/*background*/ true, callingUid,
+ BAL_ALLOW_ALLOWLISTED_COMPONENT, "Companion App");
}
// don't abort if the callingUid has SYSTEM_ALERT_WINDOW permission
if (mService.hasSystemAlertWindowPermission(callingUid, callingPid, callingPackage)) {
@@ -311,7 +329,8 @@
"Background activity start for "
+ callingPackage
+ " allowed because SYSTEM_ALERT_WINDOW permission is granted.");
- return false;
+ return logStartAllowedAndReturnCode(/*background*/ true, callingUid,
+ BAL_ALLOW_SAW_PERMISSION, "SYSTEM_ALERT_WINDOW permission is granted");
}
}
// If we don't have callerApp at this point, no caller was provided to startActivity().
@@ -326,17 +345,12 @@
// don't abort if the callerApp or other processes of that uid are allowed in any way
if (callerApp != null && useCallingUidState) {
// first check the original calling process
- if (callerApp.areBackgroundActivityStartsAllowed(appSwitchState)) {
- if (DEBUG_ACTIVITY_STARTS) {
- Slog.d(
- TAG,
- "Background activity start allowed: callerApp process (pid = "
- + callerApp.getPid()
- + ", uid = "
- + callerAppUid
- + ") is allowed");
- }
- return false;
+ @BalCode int balAllowedForCaller = callerApp
+ .areBackgroundActivityStartsAllowed(appSwitchState);
+ if (balAllowedForCaller != BAL_BLOCK) {
+ return logStartAllowedAndReturnCode(/*background*/ true, balAllowedForCaller,
+ "callerApp process (pid = " + callerApp.getPid()
+ + ", uid = " + callerAppUid + ") is allowed");
}
// only if that one wasn't allowed, check the other ones
final ArraySet<WindowProcessController> uidProcesses =
@@ -344,18 +358,12 @@
if (uidProcesses != null) {
for (int i = uidProcesses.size() - 1; i >= 0; i--) {
final WindowProcessController proc = uidProcesses.valueAt(i);
+ int balAllowedForUid = proc.areBackgroundActivityStartsAllowed(appSwitchState);
if (proc != callerApp
- && proc.areBackgroundActivityStartsAllowed(appSwitchState)) {
- if (DEBUG_ACTIVITY_STARTS) {
- Slog.d(
- TAG,
- "Background activity start allowed: process "
- + proc.getPid()
- + " from uid "
- + callerAppUid
- + " is allowed");
- }
- return false;
+ && balAllowedForUid != BAL_BLOCK) {
+ return logStartAllowedAndReturnCode(/*background*/ true, balAllowedForUid,
+ "process" + proc.getPid()
+ + " from uid " + callerAppUid + " is allowed");
}
}
}
@@ -416,6 +424,34 @@
realCallingUidHasAnyVisibleWindow,
(originatingPendingIntent != null));
}
- return true;
+ return BAL_BLOCK;
+ }
+
+ private int logStartAllowedAndReturnCode(boolean background, int callingUid, int code,
+ String msg) {
+ if (DEBUG_ACTIVITY_STARTS) {
+ return logStartAllowedAndReturnCode(background, code,
+ msg, "callingUid: " + callingUid);
+ }
+ return code;
+ }
+
+ private int logStartAllowedAndReturnCode(boolean background, int code,
+ String... msg) {
+ if (DEBUG_ACTIVITY_STARTS) {
+ StringBuilder builder = new StringBuilder();
+ if (background) {
+ builder.append("Background ");
+ }
+ builder.append("Activity start allowed: ");
+ for (int i = 0; i < msg.length; i++) {
+ builder.append(msg[i]);
+ builder.append(". ");
+ }
+ builder.append("BAL Code: ");
+ builder.append(code);
+ Slog.d(TAG, builder.toString());
+ }
+ return code;
}
}
diff --git a/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java b/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java
index 0afd872..020e9c58 100644
--- a/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java
+++ b/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java
@@ -22,6 +22,10 @@
import static com.android.server.wm.ActivityTaskManagerService.ACTIVITY_BG_START_GRACE_PERIOD_MS;
import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_ALLOW;
import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_FG_ONLY;
+import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_BAL_PERMISSION;
+import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_FOREGROUND;
+import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_GRACE_PERIOD;
+import static com.android.server.wm.BackgroundActivityStartController.BAL_BLOCK;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -71,7 +75,8 @@
mBackgroundActivityStartCallback = callback;
}
- boolean areBackgroundActivityStartsAllowed(int pid, int uid, String packageName,
+ @BackgroundActivityStartController.BalCode
+ int areBackgroundActivityStartsAllowed(int pid, int uid, String packageName,
int appSwitchState, boolean isCheckingForFgsStart,
boolean hasActivityInVisibleTask, boolean hasBackgroundActivityStartPrivileges,
long lastStopAppSwitchesTime, long lastActivityLaunchTime,
@@ -93,7 +98,7 @@
+ ")] Activity start allowed: within "
+ ACTIVITY_BG_START_GRACE_PERIOD_MS + "ms grace period");
}
- return true;
+ return BAL_ALLOW_GRACE_PERIOD;
}
if (DEBUG_ACTIVITY_STARTS) {
Slog.d(TAG, "[Process(" + pid + ")] Activity start within "
@@ -110,7 +115,7 @@
+ ")] Activity start allowed: process instrumenting with background "
+ "activity starts privileges");
}
- return true;
+ return BAL_ALLOW_BAL_PERMISSION;
}
// Allow if the caller has an activity in any foreground task.
if (hasActivityInVisibleTask
@@ -119,7 +124,7 @@
Slog.d(TAG, "[Process(" + pid
+ ")] Activity start allowed: process has activity in foreground task");
}
- return true;
+ return BAL_ALLOW_FOREGROUND;
}
// Allow if the caller is bound by a UID that's currently foreground.
if (isBoundByForegroundUid()) {
@@ -127,7 +132,7 @@
Slog.d(TAG, "[Process(" + pid
+ ")] Activity start allowed: process bound by foreground uid");
}
- return true;
+ return BAL_ALLOW_FOREGROUND;
}
// Allow if the flag was explicitly set.
if (isBackgroundStartAllowedByToken(uid, packageName, isCheckingForFgsStart)) {
@@ -135,9 +140,9 @@
Slog.d(TAG, "[Process(" + pid
+ ")] Activity start allowed: process allowed by token");
}
- return true;
+ return BAL_ALLOW_BAL_PERMISSION;
}
- return false;
+ return BAL_BLOCK;
}
/**
diff --git a/services/core/java/com/android/server/wm/ConfigurationContainer.java b/services/core/java/com/android/server/wm/ConfigurationContainer.java
index 0c6cea8..58d4e82 100644
--- a/services/core/java/com/android/server/wm/ConfigurationContainer.java
+++ b/services/core/java/com/android/server/wm/ConfigurationContainer.java
@@ -536,16 +536,19 @@
* Applies app-specific nightMode and {@link LocaleList} on requested configuration.
* @return true if any of the requested configuration has been updated.
*/
- public boolean applyAppSpecificConfig(Integer nightMode, LocaleList locales) {
+ public boolean applyAppSpecificConfig(Integer nightMode, LocaleList locales,
+ @Configuration.GrammaticalGender Integer gender) {
mRequestsTmpConfig.setTo(getRequestedOverrideConfiguration());
boolean newNightModeSet = (nightMode != null) && setOverrideNightMode(mRequestsTmpConfig,
nightMode);
boolean newLocalesSet = (locales != null) && setOverrideLocales(mRequestsTmpConfig,
locales);
- if (newNightModeSet || newLocalesSet) {
+ boolean newGenderSet = (gender != null) && setOverrideGender(mRequestsTmpConfig,
+ gender);
+ if (newNightModeSet || newLocalesSet || newGenderSet) {
onRequestedOverrideConfigurationChanged(mRequestsTmpConfig);
}
- return newNightModeSet || newLocalesSet;
+ return newNightModeSet || newLocalesSet || newGenderSet;
}
/**
@@ -578,6 +581,21 @@
return true;
}
+ /**
+ * Overrides the gender to this ConfigurationContainer.
+ *
+ * @return true if the grammatical gender has been changed.
+ */
+ private boolean setOverrideGender(Configuration requestsTmpConfig,
+ @Configuration.GrammaticalGender int gender) {
+ if (mRequestedOverrideConfiguration.getGrammaticalGender() == gender) {
+ return false;
+ } else {
+ requestsTmpConfig.setGrammaticalGender(gender);
+ return true;
+ }
+ }
+
public boolean isActivityTypeDream() {
return getActivityType() == ACTIVITY_TYPE_DREAM;
}
diff --git a/services/core/java/com/android/server/wm/ContentRecorder.java b/services/core/java/com/android/server/wm/ContentRecorder.java
index 8d5d0d5..af135b7 100644
--- a/services/core/java/com/android/server/wm/ContentRecorder.java
+++ b/services/core/java/com/android/server/wm/ContentRecorder.java
@@ -211,6 +211,7 @@
* Stops recording on this DisplayContent, and updates the session details.
*/
void stopRecording() {
+ unregisterListener();
if (mRecordedSurface != null) {
// Do not wait for the mirrored surface to be garbage collected, but clean up
// immediately.
@@ -227,7 +228,7 @@
* Ensure recording does not fall back to the display stack; ensure the recording is stopped
* and the client notified by tearing down the virtual display.
*/
- void stopMediaProjection() {
+ private void stopMediaProjection() {
ProtoLog.v(WM_DEBUG_CONTENT_RECORDING,
"Stop MediaProjection on virtual display %d", mDisplayContent.getDisplayId());
if (mMediaProjectionManager != null) {
@@ -247,6 +248,16 @@
null, mDisplayContent.mWmService);
}
+ private void unregisterListener() {
+ Task recordedTask = mRecordedWindowContainer != null
+ ? mRecordedWindowContainer.asTask() : null;
+ if (recordedTask == null || !isRecordingContentTask()) {
+ return;
+ }
+ recordedTask.unregisterWindowContainerListener(this);
+ mRecordedWindowContainer = null;
+ }
+
/**
* Start recording to this DisplayContent if it does not have its own content. Captures the
* content of a WindowContainer indicated by a WindowToken. If unable to start recording, falls
@@ -301,6 +312,13 @@
// Retrieve the size of the DisplayArea to mirror.
updateMirroredSurface(transaction, mRecordedWindowContainer.getBounds(), surfaceSize);
+ // Notify the client about the visibility of the mirrored region, now that we have begun
+ // capture.
+ if (mContentRecordingSession.getContentToRecord() == RECORD_CONTENT_TASK) {
+ mMediaProjectionManager.notifyActiveProjectionCapturedContentVisibilityChanged(
+ mRecordedWindowContainer.asTask().isVisibleRequested());
+ }
+
// No need to clean up. In SurfaceFlinger, parents hold references to their children. The
// mirrored SurfaceControl is alive since the parent DisplayContent SurfaceControl is
// holding a reference to it. Therefore, the mirrored SurfaceControl will be cleaned up
@@ -389,6 +407,7 @@
*/
private void handleStartRecordingFailed() {
final boolean shouldExitTaskRecording = isRecordingContentTask();
+ unregisterListener();
clearContentRecordingSession();
if (shouldExitTaskRecording) {
// Clean up the cached session first to ensure recording doesn't re-start, since
@@ -478,12 +497,7 @@
"Recorded task is removed, so stop recording on display %d",
mDisplayContent.getDisplayId());
- Task recordedTask = mRecordedWindowContainer != null
- ? mRecordedWindowContainer.asTask() : null;
- if (recordedTask == null || !isRecordingContentTask()) {
- return;
- }
- recordedTask.unregisterWindowContainerListener(this);
+ unregisterListener();
// Stop mirroring and teardown.
clearContentRecordingSession();
// Clean up the cached session first to ensure recording doesn't re-start, since
@@ -501,9 +515,20 @@
mLastOrientation = mergedOverrideConfiguration.orientation;
}
+ // WindowContainerListener
+ @Override
+ public void onVisibleRequestedChanged(boolean isVisibleRequested) {
+ // Check still recording just to be safe.
+ if (isCurrentlyRecording() && mLastRecordedBounds != null) {
+ mMediaProjectionManager.notifyActiveProjectionCapturedContentVisibilityChanged(
+ isVisibleRequested);
+ }
+ }
+
@VisibleForTesting interface MediaProjectionManagerWrapper {
void stopActiveProjection();
void notifyActiveProjectionCapturedContentResized(int width, int height);
+ void notifyActiveProjectionCapturedContentVisibilityChanged(boolean isVisible);
}
private static final class RemoteMediaProjectionManagerWrapper implements
@@ -543,6 +568,23 @@
}
}
+ @Override
+ public void notifyActiveProjectionCapturedContentVisibilityChanged(boolean isVisible) {
+ fetchMediaProjectionManager();
+ if (mIMediaProjectionManager == null) {
+ return;
+ }
+ try {
+ mIMediaProjectionManager.notifyActiveProjectionCapturedContentVisibilityChanged(
+ isVisible);
+ } catch (RemoteException e) {
+ ProtoLog.e(WM_DEBUG_CONTENT_RECORDING,
+ "Unable to tell MediaProjectionManagerService about visibility change on "
+ + "the active projection: %s",
+ e);
+ }
+ }
+
private void fetchMediaProjectionManager() {
if (mIMediaProjectionManager != null) {
return;
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index d324df0..82237bb 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -449,6 +449,7 @@
private final DisplayMetrics mDisplayMetrics = new DisplayMetrics();
private final DisplayPolicy mDisplayPolicy;
private final DisplayRotation mDisplayRotation;
+ @Nullable final DisplayRotationCompatPolicy mDisplayRotationCompatPolicy;
DisplayFrames mDisplayFrames;
private boolean mInTouchMode;
@@ -790,6 +791,12 @@
}
};
+ /**
+ * A lambda function to find the focused window of the given window.
+ *
+ * <p>The lambda returns true if a focused window was found, false otherwise. If a focused
+ * window is found it will be stored in <code>mTmpWindow</code>.
+ */
private final ToBooleanFunction<WindowState> mFindFocusedWindow = w -> {
final ActivityRecord focusedApp = mFocusedApp;
ProtoLog.v(WM_DEBUG_FOCUS, "Looking for focus: %s, flags=%d, canReceive=%b, reason=%s",
@@ -1172,6 +1179,13 @@
onDisplayChanged(this);
updateDisplayAreaOrganizers();
+ mDisplayRotationCompatPolicy =
+ // Not checking DeviceConfig value here to allow enabling via DeviceConfig
+ // without the need to restart the device.
+ mWmService.mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
+ /* checkDeviceConfig */ false)
+ ? new DisplayRotationCompatPolicy(this) : null;
+
mInputMonitor = new InputMonitor(mWmService, this);
mInsetsPolicy = new InsetsPolicy(mInsetsStateController, this);
mMinSizeOfResizeableTaskDp = getMinimalTaskSizeDp();
@@ -2750,6 +2764,14 @@
}
}
+ if (mDisplayRotationCompatPolicy != null) {
+ int compatOrientation = mDisplayRotationCompatPolicy.getOrientation();
+ if (compatOrientation != SCREEN_ORIENTATION_UNSPECIFIED) {
+ mLastOrientationSource = null;
+ return compatOrientation;
+ }
+ }
+
final int orientation = super.getOrientation();
if (!handlesOrientationChangeFromDescendant(orientation)) {
@@ -3312,6 +3334,10 @@
// on the next traversal if it's removed from RootWindowContainer child list.
getPendingTransaction().apply();
mWmService.mWindowPlacerLocked.requestTraversal();
+
+ if (mDisplayRotationCompatPolicy != null) {
+ mDisplayRotationCompatPolicy.dispose();
+ }
}
/** Returns true if a removal action is still being deferred. */
@@ -3719,6 +3745,14 @@
}
/**
+ * @see Display#FLAG_STEAL_TOP_FOCUS_DISABLED
+ * @return True if this display can become the top focused display, false otherwise.
+ */
+ boolean canStealTopFocus() {
+ return (mDisplayInfo.flags & Display.FLAG_STEAL_TOP_FOCUS_DISABLED) == 0;
+ }
+
+ /**
* Looking for the focused window on this display if the top focused display hasn't been
* found yet (topFocusedDisplayId is INVALID_DISPLAY) or per-display focused was allowed.
*
@@ -3730,9 +3764,15 @@
? findFocusedWindow() : null;
}
+ /**
+ * Find the focused window of this DisplayContent. The search takes the state of the display
+ * content into account
+ * @return The focused window, null if none was found.
+ */
WindowState findFocusedWindow() {
mTmpWindow = null;
+ // mFindFocusedWindow will populate mTmpWindow with the new focused window when found.
forAllWindows(mFindFocusedWindow, true /* traverseTopToBottom */);
if (mTmpWindow == null) {
@@ -6485,15 +6525,6 @@
}
/**
- * The MediaProjection instance is torn down.
- */
- @VisibleForTesting void stopMediaProjection() {
- if (mContentRecorder != null) {
- mContentRecorder.stopMediaProjection();
- }
- }
-
- /**
* Sets the incoming recording session. Should only be used when starting to record on
* this display; stopping recording is handled separately when the display is destroyed.
*
diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java
index 4fb137b..cf3a688 100644
--- a/services/core/java/com/android/server/wm/DisplayRotation.java
+++ b/services/core/java/com/android/server/wm/DisplayRotation.java
@@ -16,6 +16,7 @@
package com.android.server.wm;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
import static android.util.RotationUtils.deltaRotation;
@@ -55,6 +56,7 @@
import android.os.SystemProperties;
import android.os.UserHandle;
import android.provider.Settings;
+import android.util.ArraySet;
import android.util.Slog;
import android.util.TimeUtils;
import android.util.proto.ProtoOutputStream;
@@ -76,6 +78,7 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayDeque;
+import java.util.Set;
/**
* Defines the mapping between orientation and rotation of a display.
@@ -1552,6 +1555,15 @@
proto.end(token);
}
+ boolean isDeviceInPosture(DeviceStateController.FoldState state, boolean isTabletop) {
+ if (mFoldController == null) return false;
+ return mFoldController.isDeviceInPosture(state, isTabletop);
+ }
+
+ boolean isDisplaySeparatingHinge() {
+ return mFoldController != null && mFoldController.isSeparatingHinge();
+ }
+
/**
* Called by the DeviceStateManager callback when the device state changes.
*/
@@ -1569,6 +1581,63 @@
private DeviceStateController.FoldState mFoldState =
DeviceStateController.FoldState.UNKNOWN;
private boolean mInHalfFoldTransition = false;
+ private final boolean mIsDisplayAlwaysSeparatingHinge;
+ private final Set<Integer> mTabletopRotations;
+
+ FoldController() {
+ mTabletopRotations = new ArraySet<>();
+ int[] tabletop_rotations = mContext.getResources().getIntArray(
+ R.array.config_deviceTabletopRotations);
+ if (tabletop_rotations != null) {
+ for (int angle : tabletop_rotations) {
+ switch (angle) {
+ case 0:
+ mTabletopRotations.add(Surface.ROTATION_0);
+ break;
+ case 90:
+ mTabletopRotations.add(Surface.ROTATION_90);
+ break;
+ case 180:
+ mTabletopRotations.add(Surface.ROTATION_180);
+ break;
+ case 270:
+ mTabletopRotations.add(Surface.ROTATION_270);
+ break;
+ default:
+ ProtoLog.e(WM_DEBUG_ORIENTATION,
+ "Invalid surface rotation angle in "
+ + "config_deviceTabletopRotations: %d",
+ angle);
+ }
+ }
+ } else {
+ ProtoLog.w(WM_DEBUG_ORIENTATION,
+ "config_deviceTabletopRotations is not defined. Half-fold "
+ + "letterboxing will work inconsistently.");
+ }
+ mIsDisplayAlwaysSeparatingHinge = mContext.getResources().getBoolean(
+ R.bool.config_isDisplayHingeAlwaysSeparating);
+ }
+
+ boolean isDeviceInPosture(DeviceStateController.FoldState state, boolean isTabletop) {
+ if (state != mFoldState) {
+ return false;
+ }
+ if (mFoldState == DeviceStateController.FoldState.HALF_FOLDED) {
+ return !(isTabletop ^ mTabletopRotations.contains(mRotation));
+ }
+ return true;
+ }
+
+ DeviceStateController.FoldState getFoldState() {
+ return mFoldState;
+ }
+
+ boolean isSeparatingHinge() {
+ return mFoldState == DeviceStateController.FoldState.HALF_FOLDED
+ || (mFoldState == DeviceStateController.FoldState.OPEN
+ && mIsDisplayAlwaysSeparatingHinge);
+ }
boolean overrideFrozenRotation() {
return mFoldState == DeviceStateController.FoldState.HALF_FOLDED;
@@ -1617,6 +1686,15 @@
mService.updateRotation(false /* alwaysSendConfiguration */,
false /* forceRelayout */);
}
+ // Alert the activity of possible new bounds.
+ final Task topFullscreenTask =
+ mDisplayContent.getTask(t -> t.getWindowingMode() == WINDOWING_MODE_FULLSCREEN);
+ if (topFullscreenTask != null) {
+ final ActivityRecord top = topFullscreenTask.topRunningActivity();
+ if (top != null) {
+ top.recomputeConfiguration();
+ }
+ }
}
}
@@ -1727,6 +1805,7 @@
final int mHalfFoldSavedRotation;
final boolean mInHalfFoldTransition;
final DeviceStateController.FoldState mFoldState;
+ @Nullable final String mDisplayRotationCompatPolicySummary;
Record(DisplayRotation dr, int fromRotation, int toRotation) {
mFromRotation = fromRotation;
@@ -1761,6 +1840,10 @@
mInHalfFoldTransition = false;
mFoldState = DeviceStateController.FoldState.UNKNOWN;
}
+ mDisplayRotationCompatPolicySummary = dc.mDisplayRotationCompatPolicy == null
+ ? null
+ : dc.mDisplayRotationCompatPolicy
+ .getSummaryForDisplayRotationHistoryRecord();
}
void dump(String prefix, PrintWriter pw) {
@@ -1783,6 +1866,9 @@
+ " mInHalfFoldTransition=" + mInHalfFoldTransition
+ " mFoldState=" + mFoldState);
}
+ if (mDisplayRotationCompatPolicySummary != null) {
+ pw.println(prefix + mDisplayRotationCompatPolicySummary);
+ }
}
}
diff --git a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
new file mode 100644
index 0000000..7266d21
--- /dev/null
+++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
@@ -0,0 +1,434 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE;
+import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LOCKED;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+import static android.content.pm.ActivityInfo.screenOrientationToString;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static android.content.res.Configuration.ORIENTATION_UNDEFINED;
+import static android.view.Display.TYPE_INTERNAL;
+
+import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ORIENTATION;
+import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STATES;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.servertransaction.ClientTransaction;
+import android.app.servertransaction.RefreshCallbackItem;
+import android.app.servertransaction.ResumeActivityItem;
+import android.content.pm.ActivityInfo.ScreenOrientation;
+import android.content.res.Configuration;
+import android.hardware.camera2.CameraManager;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.protolog.common.ProtoLog;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Controls camera compatibility treatment that handles orientation mismatch between camera
+ * buffers and an app window for a particular display that can lead to camera issues like sideways
+ * or stretched viewfinder.
+ *
+ * <p>This includes force rotation of fixed orientation activities connected to the camera.
+ *
+ * <p>The treatment is enabled for internal displays that have {@code ignoreOrientationRequest}
+ * display setting enabled and when {@code
+ * R.bool.config_isWindowManagerCameraCompatTreatmentEnabled} is {@code true}.
+ */
+ // TODO(b/261444714): Consider moving Camera-specific logic outside of the WM Core path
+final class DisplayRotationCompatPolicy {
+
+ // Delay for updating display rotation after Camera connection is closed. Needed to avoid
+ // rotation flickering when an app is flipping between front and rear cameras or when size
+ // compat mode is restarted.
+ // TODO(b/263114289): Consider associating this delay with a specific activity so that if
+ // the new non-camera activity started on top of the camer one we can rotate faster.
+ private static final int CAMERA_CLOSED_ROTATION_UPDATE_DELAY_MS = 2000;
+ // Delay for updating display rotation after Camera connection is opened. This delay is
+ // selected to be long enough to avoid conflicts with transitions on the app's side.
+ // Using a delay < CAMERA_CLOSED_ROTATION_UPDATE_DELAY_MS to avoid flickering when an app
+ // is flipping between front and rear cameras (in case requested orientation changes at
+ // runtime at the same time) or when size compat mode is restarted.
+ private static final int CAMERA_OPENED_ROTATION_UPDATE_DELAY_MS =
+ CAMERA_CLOSED_ROTATION_UPDATE_DELAY_MS / 2;
+ // Delay for ensuring that onActivityRefreshed is always called after an activity refresh. The
+ // client process may not always report the event back to the server, such as process is
+ // crashed or got killed.
+ private static final int REFRESH_CALLBACK_TIMEOUT_MS = 2000;
+
+ private final DisplayContent mDisplayContent;
+ private final WindowManagerService mWmService;
+ private final CameraManager mCameraManager;
+ private final Handler mHandler;
+
+ // Bi-directional map between package names and active camera IDs since we need to 1) get a
+ // camera id by a package name when determining rotation; 2) get a package name by a camera id
+ // when camera connection is closed and we need to clean up our records.
+ @GuardedBy("this")
+ private final CameraIdPackageNameBiMap mCameraIdPackageBiMap = new CameraIdPackageNameBiMap();
+ @GuardedBy("this")
+ private final Set<String> mScheduledToBeRemovedCameraIdSet = new ArraySet<>();
+ @GuardedBy("this")
+ private final Set<String> mScheduledOrientationUpdateCameraIdSet = new ArraySet<>();
+
+ private final CameraManager.AvailabilityCallback mAvailabilityCallback =
+ new CameraManager.AvailabilityCallback() {
+ @Override
+ public void onCameraOpened(@NonNull String cameraId, @NonNull String packageId) {
+ notifyCameraOpened(cameraId, packageId);
+ }
+
+ @Override
+ public void onCameraClosed(@NonNull String cameraId) {
+ notifyCameraClosed(cameraId);
+ }
+ };
+
+ @ScreenOrientation
+ private int mLastReportedOrientation = SCREEN_ORIENTATION_UNSET;
+
+ DisplayRotationCompatPolicy(@NonNull DisplayContent displayContent) {
+ this(displayContent, displayContent.mWmService.mH);
+ }
+
+ @VisibleForTesting
+ DisplayRotationCompatPolicy(@NonNull DisplayContent displayContent, Handler handler) {
+ // This constructor is called from DisplayContent constructor. Don't use any fields in
+ // DisplayContent here since they aren't guaranteed to be set.
+ mHandler = handler;
+ mDisplayContent = displayContent;
+ mWmService = displayContent.mWmService;
+ mCameraManager = mWmService.mContext.getSystemService(CameraManager.class);
+ mCameraManager.registerAvailabilityCallback(
+ mWmService.mContext.getMainExecutor(), mAvailabilityCallback);
+ }
+
+ void dispose() {
+ mCameraManager.unregisterAvailabilityCallback(mAvailabilityCallback);
+ }
+
+ /**
+ * Determines orientation for Camera compatibility.
+ *
+ * <p>The goal of this function is to compute a orientation which would align orientations of
+ * portrait app window and natural orientation of the device and set opposite to natural
+ * orientation for a landscape app window. This is one of the strongest assumptions that apps
+ * make when they implement camera previews. Since app and natural display orientations aren't
+ * guaranteed to match, the rotation can cause letterboxing.
+ *
+ * <p>If treatment isn't applicable returns {@link SCREEN_ORIENTATION_UNSPECIFIED}. See {@link
+ * #shouldComputeCameraCompatOrientation} for conditions enabling the treatment.
+ */
+ @ScreenOrientation
+ int getOrientation() {
+ mLastReportedOrientation = getOrientationInternal();
+ return mLastReportedOrientation;
+ }
+
+ @ScreenOrientation
+ private synchronized int getOrientationInternal() {
+ if (!isTreatmentEnabledForDisplay()) {
+ return SCREEN_ORIENTATION_UNSPECIFIED;
+ }
+ ActivityRecord topActivity = mDisplayContent.topRunningActivity(
+ /* considerKeyguardState= */ true);
+ if (!isTreatmentEnabledForActivity(topActivity)) {
+ return SCREEN_ORIENTATION_UNSPECIFIED;
+ }
+ boolean isPortraitActivity =
+ topActivity.getRequestedConfigurationOrientation() == ORIENTATION_PORTRAIT;
+ boolean isNaturalDisplayOrientationPortrait =
+ mDisplayContent.getNaturalOrientation() == ORIENTATION_PORTRAIT;
+ // Rotate portrait-only activity in the natural orientation of the displays (and in the
+ // opposite to natural orientation for landscape-only) since many apps assume that those
+ // are aligned when they compute orientation of the preview.
+ // This means that even for a landscape-only activity and a device with landscape natural
+ // orientation this would return SCREEN_ORIENTATION_PORTRAIT because an assumption that
+ // natural orientation = portrait window = portait camera is the main wrong assumption
+ // that apps make when they implement camera previews so landscape windows need be
+ // rotated in the orientation oposite to the natural one even if it's portrait.
+ // TODO(b/261475895): Consider allowing more rotations for "sensor" and "user" versions
+ // of the portrait and landscape orientation requests.
+ int orientation = (isPortraitActivity && isNaturalDisplayOrientationPortrait)
+ || (!isPortraitActivity && !isNaturalDisplayOrientationPortrait)
+ ? SCREEN_ORIENTATION_PORTRAIT
+ : SCREEN_ORIENTATION_LANDSCAPE;
+ ProtoLog.v(WM_DEBUG_ORIENTATION,
+ "Display id=%d is ignoring all orientation requests, camera is active "
+ + "and the top activity is eligible for force rotation, return %s,"
+ + "portrait activity: %b, is natural orientation portrait: %b.",
+ mDisplayContent.mDisplayId, screenOrientationToString(orientation),
+ isPortraitActivity, isNaturalDisplayOrientationPortrait);
+ return orientation;
+ }
+
+ /**
+ * "Refreshes" activity by going through "stopped -> resumed" or "paused -> resumed" cycle.
+ * This allows to clear cached values in apps (e.g. display or camera rotation) that influence
+ * camera preview and can lead to sideways or stretching issues persisting even after force
+ * rotation.
+ */
+ void onActivityConfigurationChanging(ActivityRecord activity, Configuration newConfig,
+ Configuration lastReportedConfig) {
+ if (!isTreatmentEnabledForDisplay()
+ || !mWmService.mLetterboxConfiguration.isCameraCompatRefreshEnabled()
+ || !shouldRefreshActivity(activity, newConfig, lastReportedConfig)) {
+ return;
+ }
+ boolean cycleThroughStop = mWmService.mLetterboxConfiguration
+ .isCameraCompatRefreshCycleThroughStopEnabled();
+ try {
+ activity.mLetterboxUiController.setIsRefreshAfterRotationRequested(true);
+ ProtoLog.v(WM_DEBUG_STATES,
+ "Refershing activity for camera compatibility treatment, "
+ + "activityRecord=%s", activity);
+ final ClientTransaction transaction = ClientTransaction.obtain(
+ activity.app.getThread(), activity.token);
+ transaction.addCallback(
+ RefreshCallbackItem.obtain(cycleThroughStop ? ON_STOP : ON_PAUSE));
+ transaction.setLifecycleStateRequest(ResumeActivityItem.obtain(
+ /* isForward */ false, /* shouldSendCompatFakeFocus */ false));
+ activity.mAtmService.getLifecycleManager().scheduleTransaction(transaction);
+ mHandler.postDelayed(
+ () -> onActivityRefreshed(activity),
+ REFRESH_CALLBACK_TIMEOUT_MS);
+ } catch (RemoteException e) {
+ activity.mLetterboxUiController.setIsRefreshAfterRotationRequested(false);
+ }
+ }
+
+ void onActivityRefreshed(@NonNull ActivityRecord activity) {
+ activity.mLetterboxUiController.setIsRefreshAfterRotationRequested(false);
+ }
+
+ String getSummaryForDisplayRotationHistoryRecord() {
+ String summaryIfEnabled = "";
+ if (isTreatmentEnabledForDisplay()) {
+ ActivityRecord topActivity = mDisplayContent.topRunningActivity(
+ /* considerKeyguardState= */ true);
+ summaryIfEnabled =
+ " mLastReportedOrientation="
+ + screenOrientationToString(mLastReportedOrientation)
+ + " topActivity="
+ + (topActivity == null ? "null" : topActivity.shortComponentName)
+ + " isTreatmentEnabledForActivity="
+ + isTreatmentEnabledForActivity(topActivity)
+ + " CameraIdPackageNameBiMap="
+ + mCameraIdPackageBiMap.getSummaryForDisplayRotationHistoryRecord();
+ }
+ return "DisplayRotationCompatPolicy{"
+ + " isTreatmentEnabledForDisplay=" + isTreatmentEnabledForDisplay()
+ + summaryIfEnabled
+ + " }";
+ }
+
+ // Refreshing only when configuration changes after rotation.
+ private boolean shouldRefreshActivity(ActivityRecord activity, Configuration newConfig,
+ Configuration lastReportedConfig) {
+ return newConfig.windowConfiguration.getDisplayRotation()
+ != lastReportedConfig.windowConfiguration.getDisplayRotation()
+ && isTreatmentEnabledForActivity(activity);
+ }
+
+ /**
+ * Whether camera compat treatment is enabled for the display.
+ *
+ * <p>Conditions that need to be met:
+ * <ul>
+ * <li>{@code R.bool.config_isWindowManagerCameraCompatTreatmentEnabled} is {@code true}.
+ * <li>Setting {@code ignoreOrientationRequest} is enabled for the display.
+ * <li>Associated {@link DisplayContent} is for internal display. See b/225928882
+ * that tracks supporting external displays in the future.
+ * </ul>
+ */
+ private boolean isTreatmentEnabledForDisplay() {
+ return mWmService.mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
+ /* checkDeviceConfig */ true)
+ && mDisplayContent.getIgnoreOrientationRequest()
+ // TODO(b/225928882): Support camera compat rotation for external displays
+ && mDisplayContent.getDisplay().getType() == TYPE_INTERNAL;
+ }
+
+ /**
+ * Whether camera compat treatment is applicable for the given activity.
+ *
+ * <p>Conditions that need to be met:
+ * <ul>
+ * <li>{@link #isCameraActiveForPackage} is {@code true} for the activity.
+ * <li>The activity is in fullscreen
+ * <li>The activity has fixed orientation but not "locked" or "nosensor" one.
+ * </ul>
+ */
+ private boolean isTreatmentEnabledForActivity(@Nullable ActivityRecord activity) {
+ return activity != null && !activity.inMultiWindowMode()
+ && activity.getRequestedConfigurationOrientation() != ORIENTATION_UNDEFINED
+ // "locked" and "nosensor" values are often used by camera apps that can't
+ // handle dynamic changes so we shouldn't force rotate them.
+ && activity.getRequestedOrientation() != SCREEN_ORIENTATION_NOSENSOR
+ && activity.getRequestedOrientation() != SCREEN_ORIENTATION_LOCKED
+ && mCameraIdPackageBiMap.containsPackageName(activity.packageName);
+ }
+
+ private synchronized void notifyCameraOpened(
+ @NonNull String cameraId, @NonNull String packageName) {
+ // If an activity is restarting or camera is flipping, the camera connection can be
+ // quickly closed and reopened.
+ mScheduledToBeRemovedCameraIdSet.remove(cameraId);
+ ProtoLog.v(WM_DEBUG_ORIENTATION,
+ "Display id=%d is notified that Camera %s is open for package %s",
+ mDisplayContent.mDisplayId, cameraId, packageName);
+ // Some apps can’t handle configuration changes coming at the same time with Camera setup
+ // so delaying orientation update to accomadate for that.
+ mScheduledOrientationUpdateCameraIdSet.add(cameraId);
+ mHandler.postDelayed(
+ () -> delayedUpdateOrientationWithWmLock(cameraId, packageName),
+ CAMERA_OPENED_ROTATION_UPDATE_DELAY_MS);
+ }
+
+ private void updateOrientationWithWmLock() {
+ synchronized (mWmService.mGlobalLock) {
+ mDisplayContent.updateOrientation();
+ }
+ }
+
+ private void delayedUpdateOrientationWithWmLock(
+ @NonNull String cameraId, @NonNull String packageName) {
+ synchronized (this) {
+ if (!mScheduledOrientationUpdateCameraIdSet.remove(cameraId)) {
+ // Orientation update has happened already or was cancelled because
+ // camera was closed.
+ return;
+ }
+ mCameraIdPackageBiMap.put(packageName, cameraId);
+ }
+ updateOrientationWithWmLock();
+ }
+
+ private synchronized void notifyCameraClosed(@NonNull String cameraId) {
+ ProtoLog.v(WM_DEBUG_ORIENTATION,
+ "Display id=%d is notified that Camera %s is closed, scheduling rotation update.",
+ mDisplayContent.mDisplayId, cameraId);
+ mScheduledToBeRemovedCameraIdSet.add(cameraId);
+ // No need to update orientation for this camera if it's already closed.
+ mScheduledOrientationUpdateCameraIdSet.remove(cameraId);
+ scheduleRemoveCameraId(cameraId);
+ }
+
+ // Delay is needed to avoid rotation flickering when an app is flipping between front and
+ // rear cameras, when size compat mode is restarted or activity is being refreshed.
+ private void scheduleRemoveCameraId(@NonNull String cameraId) {
+ mHandler.postDelayed(
+ () -> removeCameraId(cameraId),
+ CAMERA_CLOSED_ROTATION_UPDATE_DELAY_MS);
+ }
+
+ private void removeCameraId(String cameraId) {
+ synchronized (this) {
+ if (!mScheduledToBeRemovedCameraIdSet.remove(cameraId)) {
+ // Already reconnected to this camera, no need to clean up.
+ return;
+ }
+ if (isActivityForCameraIdRefreshing(cameraId)) {
+ ProtoLog.v(WM_DEBUG_ORIENTATION,
+ "Display id=%d is notified that Camera %s is closed but activity is"
+ + " still refreshing. Rescheduling an update.",
+ mDisplayContent.mDisplayId, cameraId);
+ mScheduledToBeRemovedCameraIdSet.add(cameraId);
+ scheduleRemoveCameraId(cameraId);
+ return;
+ }
+ mCameraIdPackageBiMap.removeCameraId(cameraId);
+ }
+ ProtoLog.v(WM_DEBUG_ORIENTATION,
+ "Display id=%d is notified that Camera %s is closed, updating rotation.",
+ mDisplayContent.mDisplayId, cameraId);
+ updateOrientationWithWmLock();
+ }
+
+ private boolean isActivityForCameraIdRefreshing(String cameraId) {
+ ActivityRecord topActivity = mDisplayContent.topRunningActivity(
+ /* considerKeyguardState= */ true);
+ if (!isTreatmentEnabledForActivity(topActivity)) {
+ return false;
+ }
+ String activeCameraId = mCameraIdPackageBiMap.getCameraId(topActivity.packageName);
+ if (activeCameraId == null || activeCameraId != cameraId) {
+ return false;
+ }
+ return topActivity.mLetterboxUiController.isRefreshAfterRotationRequested();
+ }
+
+ private static class CameraIdPackageNameBiMap {
+
+ private final Map<String, String> mPackageToCameraIdMap = new ArrayMap<>();
+ private final Map<String, String> mCameraIdToPackageMap = new ArrayMap<>();
+
+ void put(String packageName, String cameraId) {
+ // Always using the last connected camera ID for the package even for the concurrent
+ // camera use case since we can't guess which camera is more important anyway.
+ removePackageName(packageName);
+ removeCameraId(cameraId);
+ mPackageToCameraIdMap.put(packageName, cameraId);
+ mCameraIdToPackageMap.put(cameraId, packageName);
+ }
+
+ boolean containsPackageName(String packageName) {
+ return mPackageToCameraIdMap.containsKey(packageName);
+ }
+
+ @Nullable
+ String getCameraId(String packageName) {
+ return mPackageToCameraIdMap.get(packageName);
+ }
+
+ void removeCameraId(String cameraId) {
+ String packageName = mCameraIdToPackageMap.get(cameraId);
+ if (packageName == null) {
+ return;
+ }
+ mPackageToCameraIdMap.remove(packageName, cameraId);
+ mCameraIdToPackageMap.remove(cameraId, packageName);
+ }
+
+ String getSummaryForDisplayRotationHistoryRecord() {
+ return "{ mPackageToCameraIdMap=" + mPackageToCameraIdMap + " }";
+ }
+
+ private void removePackageName(String packageName) {
+ String cameraId = mPackageToCameraIdMap.get(packageName);
+ if (cameraId == null) {
+ return;
+ }
+ mPackageToCameraIdMap.remove(packageName, cameraId);
+ mCameraIdToPackageMap.remove(cameraId, packageName);
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/wm/DisplayWindowSettings.java b/services/core/java/com/android/server/wm/DisplayWindowSettings.java
index b735b30..6d47eeb 100644
--- a/services/core/java/com/android/server/wm/DisplayWindowSettings.java
+++ b/services/core/java/com/android/server/wm/DisplayWindowSettings.java
@@ -291,7 +291,7 @@
boolean dontMoveToTop = settings.mDontMoveToTop != null
? settings.mDontMoveToTop : false;
- dc.mDontMoveToTop = dontMoveToTop;
+ dc.mDontMoveToTop = !dc.canStealTopFocus() || dontMoveToTop;
if (includeRotationSettings) applyRotationSettingsToDisplayLocked(dc);
}
diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
index 127a7bf..03c5589 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java
+++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
@@ -16,12 +16,16 @@
package com.android.server.wm;
+import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
+import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
+
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.graphics.Color;
import android.provider.DeviceConfig;
+import android.util.Slog;
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
@@ -33,6 +37,8 @@
/** Reads letterbox configs from resources and controls their overrides at runtime. */
final class LetterboxConfiguration {
+ private static final String TAG = TAG_WITH_CLASS_NAME ? "LetterboxConfiguration" : TAG_ATM;
+
/**
* Override of aspect ratio for fixed orientation letterboxing that is set via ADB with
* set-fixed-orientation-letterbox-aspect-ratio or via {@link
@@ -144,6 +150,14 @@
// side of the screen and 1.0 to the bottom side.
private float mLetterboxVerticalPositionMultiplier;
+ // Horizontal position of a center of the letterboxed app window when the device is half-folded.
+ // 0 corresponds to the left side of the screen and 1.0 to the right side.
+ private float mLetterboxBookModePositionMultiplier;
+
+ // Vertical position of a center of the letterboxed app window when the device is half-folded.
+ // 0 corresponds to the top side of the screen and 1.0 to the bottom side.
+ private float mLetterboxTabletopModePositionMultiplier;
+
// Default horizontal position the letterboxed app window when horizontal reachability is
// enabled and an app is fullscreen in landscape device orientation.
// It is used as a starting point for mLetterboxPositionForHorizontalReachability.
@@ -177,10 +191,36 @@
// Allows to enable letterboxing strategy for translucent activities ignoring flags.
private boolean mTranslucentLetterboxingOverrideEnabled;
+ // Whether sending compat fake focus is enabled for unfocused apps in splitscreen. Some game
+ // engines wait to get focus before drawing the content of the app so this needs to be used
+ // otherwise the apps get blacked out when they are resumed and do not have focus yet.
+ private boolean mIsCompatFakeFocusEnabled;
+
+ // Whether camera compatibility treatment is enabled.
+ // See DisplayRotationCompatPolicy for context.
+ private final boolean mIsCameraCompatTreatmentEnabled;
+
+ // Whether activity "refresh" in camera compatibility treatment is enabled.
+ // See RefreshCallbackItem for context.
+ private boolean mIsCameraCompatTreatmentRefreshEnabled = true;
+
+ // Whether activity "refresh" in camera compatibility treatment should happen using the
+ // "stopped -> resumed" cycle rather than "paused -> resumed" cycle. Using "stop -> resumed"
+ // cycle by default due to higher success rate confirmed with app compatibility testing.
+ // See RefreshCallbackItem for context.
+ private boolean mIsCameraCompatRefreshCycleThroughStopEnabled = true;
+
LetterboxConfiguration(Context systemUiContext) {
this(systemUiContext, new LetterboxConfigurationPersister(systemUiContext,
- () -> readLetterboxHorizontalReachabilityPositionFromConfig(systemUiContext),
- () -> readLetterboxVerticalReachabilityPositionFromConfig(systemUiContext)));
+ () -> readLetterboxHorizontalReachabilityPositionFromConfig(systemUiContext,
+ /* forBookMode */ false),
+ () -> readLetterboxVerticalReachabilityPositionFromConfig(systemUiContext,
+ /* forTabletopMode */ false),
+ () -> readLetterboxHorizontalReachabilityPositionFromConfig(systemUiContext,
+ /* forBookMode */ true),
+ () -> readLetterboxVerticalReachabilityPositionFromConfig(systemUiContext,
+ /* forTabletopMode */ true)
+ ));
}
@VisibleForTesting
@@ -200,14 +240,18 @@
R.dimen.config_letterboxHorizontalPositionMultiplier);
mLetterboxVerticalPositionMultiplier = mContext.getResources().getFloat(
R.dimen.config_letterboxVerticalPositionMultiplier);
+ mLetterboxBookModePositionMultiplier = mContext.getResources().getFloat(
+ R.dimen.config_letterboxBookModePositionMultiplier);
+ mLetterboxTabletopModePositionMultiplier = mContext.getResources().getFloat(
+ R.dimen.config_letterboxTabletopModePositionMultiplier);
mIsHorizontalReachabilityEnabled = mContext.getResources().getBoolean(
R.bool.config_letterboxIsHorizontalReachabilityEnabled);
mIsVerticalReachabilityEnabled = mContext.getResources().getBoolean(
R.bool.config_letterboxIsVerticalReachabilityEnabled);
mDefaultPositionForHorizontalReachability =
- readLetterboxHorizontalReachabilityPositionFromConfig(mContext);
+ readLetterboxHorizontalReachabilityPositionFromConfig(mContext, false);
mDefaultPositionForVerticalReachability =
- readLetterboxVerticalReachabilityPositionFromConfig(mContext);
+ readLetterboxVerticalReachabilityPositionFromConfig(mContext, false);
mIsEducationEnabled = mContext.getResources().getBoolean(
R.bool.config_letterboxIsEducationEnabled);
setDefaultMinAspectRatioForUnresizableApps(mContext.getResources().getFloat(
@@ -216,8 +260,12 @@
R.bool.config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled);
mTranslucentLetterboxingEnabled = mContext.getResources().getBoolean(
R.bool.config_letterboxIsEnabledForTranslucentActivities);
+ mIsCameraCompatTreatmentEnabled = mContext.getResources().getBoolean(
+ R.bool.config_isWindowManagerCameraCompatTreatmentEnabled);
mLetterboxConfigurationPersister = letterboxConfigurationPersister;
mLetterboxConfigurationPersister.start();
+ mIsCompatFakeFocusEnabled = mContext.getResources()
+ .getBoolean(R.bool.config_isCompatFakeFocusEnabled);
}
/**
@@ -460,11 +508,30 @@
* or via an ADB command. 0 corresponds to the left side of the screen and 1 to the
* right side.
*/
- float getLetterboxHorizontalPositionMultiplier() {
- return (mLetterboxHorizontalPositionMultiplier < 0.0f
- || mLetterboxHorizontalPositionMultiplier > 1.0f)
- // Default to central position if invalid value is provided.
- ? 0.5f : mLetterboxHorizontalPositionMultiplier;
+ float getLetterboxHorizontalPositionMultiplier(boolean isInBookMode) {
+ if (isInBookMode) {
+ if (mLetterboxBookModePositionMultiplier < 0.0f
+ || mLetterboxBookModePositionMultiplier > 1.0f) {
+ Slog.w(TAG,
+ "mLetterboxBookModePositionMultiplier out of bounds (isInBookMode=true): "
+ + mLetterboxBookModePositionMultiplier);
+ // Default to left position if invalid value is provided.
+ return 0.0f;
+ } else {
+ return mLetterboxBookModePositionMultiplier;
+ }
+ } else {
+ if (mLetterboxHorizontalPositionMultiplier < 0.0f
+ || mLetterboxHorizontalPositionMultiplier > 1.0f) {
+ Slog.w(TAG,
+ "mLetterboxBookModePositionMultiplier out of bounds (isInBookMode=false):"
+ + mLetterboxBookModePositionMultiplier);
+ // Default to central position if invalid value is provided.
+ return 0.5f;
+ } else {
+ return mLetterboxHorizontalPositionMultiplier;
+ }
+ }
}
/*
@@ -473,11 +540,18 @@
* or via an ADB command. 0 corresponds to the top side of the screen and 1 to the
* bottom side.
*/
- float getLetterboxVerticalPositionMultiplier() {
- return (mLetterboxVerticalPositionMultiplier < 0.0f
- || mLetterboxVerticalPositionMultiplier > 1.0f)
- // Default to central position if invalid value is provided.
- ? 0.5f : mLetterboxVerticalPositionMultiplier;
+ float getLetterboxVerticalPositionMultiplier(boolean isInTabletopMode) {
+ if (isInTabletopMode) {
+ return (mLetterboxTabletopModePositionMultiplier < 0.0f
+ || mLetterboxTabletopModePositionMultiplier > 1.0f)
+ // Default to top position if invalid value is provided.
+ ? 0.0f : mLetterboxTabletopModePositionMultiplier;
+ } else {
+ return (mLetterboxVerticalPositionMultiplier < 0.0f
+ || mLetterboxVerticalPositionMultiplier > 1.0f)
+ // Default to central position if invalid value is provided.
+ ? 0.5f : mLetterboxVerticalPositionMultiplier;
+ }
}
/**
@@ -618,7 +692,8 @@
*/
void resetDefaultPositionForHorizontalReachability() {
mDefaultPositionForHorizontalReachability =
- readLetterboxHorizontalReachabilityPositionFromConfig(mContext);
+ readLetterboxHorizontalReachabilityPositionFromConfig(mContext,
+ false /* forBookMode */);
}
/**
@@ -627,27 +702,34 @@
*/
void resetDefaultPositionForVerticalReachability() {
mDefaultPositionForVerticalReachability =
- readLetterboxVerticalReachabilityPositionFromConfig(mContext);
+ readLetterboxVerticalReachabilityPositionFromConfig(mContext,
+ false /* forTabletopMode */);
}
@LetterboxHorizontalReachabilityPosition
- private static int readLetterboxHorizontalReachabilityPositionFromConfig(Context context) {
+ private static int readLetterboxHorizontalReachabilityPositionFromConfig(Context context,
+ boolean forBookMode) {
int position = context.getResources().getInteger(
- R.integer.config_letterboxDefaultPositionForHorizontalReachability);
+ forBookMode
+ ? R.integer.config_letterboxDefaultPositionForBookModeReachability
+ : R.integer.config_letterboxDefaultPositionForHorizontalReachability);
return position == LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT
- || position == LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER
- || position == LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT
+ || position == LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER
+ || position == LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT
? position : LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER;
}
@LetterboxVerticalReachabilityPosition
- private static int readLetterboxVerticalReachabilityPositionFromConfig(Context context) {
+ private static int readLetterboxVerticalReachabilityPositionFromConfig(Context context,
+ boolean forTabletopMode) {
int position = context.getResources().getInteger(
- R.integer.config_letterboxDefaultPositionForVerticalReachability);
+ forTabletopMode
+ ? R.integer.config_letterboxDefaultPositionForTabletopModeReachability
+ : R.integer.config_letterboxDefaultPositionForVerticalReachability);
return position == LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP
|| position == LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER
|| position == LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM
- ? position : LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER;
+ ? position : LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER;
}
/*
@@ -656,9 +738,10 @@
*
* <p>The position multiplier is changed after each double tap in the letterbox area.
*/
- float getHorizontalMultiplierForReachability() {
+ float getHorizontalMultiplierForReachability(boolean isDeviceInBookMode) {
final int letterboxPositionForHorizontalReachability =
- mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
+ mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(
+ isDeviceInBookMode);
switch (letterboxPositionForHorizontalReachability) {
case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT:
return 0.0f;
@@ -679,9 +762,10 @@
*
* <p>The position multiplier is changed after each double tap in the letterbox area.
*/
- float getVerticalMultiplierForReachability() {
+ float getVerticalMultiplierForReachability(boolean isDeviceInTabletopMode) {
final int letterboxPositionForVerticalReachability =
- mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
+ mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(
+ isDeviceInTabletopMode);
switch (letterboxPositionForVerticalReachability) {
case LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP:
return 0.0f;
@@ -701,8 +785,9 @@
* enabled.
*/
@LetterboxHorizontalReachabilityPosition
- int getLetterboxPositionForHorizontalReachability() {
- return mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
+ int getLetterboxPositionForHorizontalReachability(boolean isInFullScreenBookMode) {
+ return mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(
+ isInFullScreenBookMode);
}
/*
@@ -710,8 +795,9 @@
* enabled.
*/
@LetterboxVerticalReachabilityPosition
- int getLetterboxPositionForVerticalReachability() {
- return mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
+ int getLetterboxPositionForVerticalReachability(boolean isInFullScreenTabletopMode) {
+ return mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(
+ isInFullScreenTabletopMode);
}
/** Returns a string representing the given {@link LetterboxHorizontalReachabilityPosition}. */
@@ -750,34 +836,41 @@
* Changes letterbox position for horizontal reachability to the next available one on the
* right side.
*/
- void movePositionForHorizontalReachabilityToNextRightStop() {
- updatePositionForHorizontalReachability(prev -> Math.min(
- prev + 1, LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT));
+ void movePositionForHorizontalReachabilityToNextRightStop(boolean isDeviceInBookMode) {
+ updatePositionForHorizontalReachability(isDeviceInBookMode, prev -> Math.min(
+ prev + (isDeviceInBookMode ? 2 : 1), // Move 2 stops in book mode to avoid center.
+ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT));
}
/**
* Changes letterbox position for horizontal reachability to the next available one on the left
* side.
*/
- void movePositionForHorizontalReachabilityToNextLeftStop() {
- updatePositionForHorizontalReachability(prev -> Math.max(prev - 1, 0));
+ void movePositionForHorizontalReachabilityToNextLeftStop(boolean isDeviceInBookMode) {
+ updatePositionForHorizontalReachability(isDeviceInBookMode, prev -> Math.max(
+ prev - (isDeviceInBookMode ? 2 : 1), 0)); // Move 2 stops in book mode to avoid
+ // center.
}
/**
* Changes letterbox position for vertical reachability to the next available one on the bottom
* side.
*/
- void movePositionForVerticalReachabilityToNextBottomStop() {
- updatePositionForVerticalReachability(prev -> Math.min(
- prev + 1, LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM));
+ void movePositionForVerticalReachabilityToNextBottomStop(boolean isDeviceInTabletopMode) {
+ updatePositionForVerticalReachability(isDeviceInTabletopMode, prev -> Math.min(
+ prev + (isDeviceInTabletopMode ? 2 : 1), // Move 2 stops in tabletop mode to avoid
+ // center.
+ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM));
}
/**
* Changes letterbox position for vertical reachability to the next available one on the top
* side.
*/
- void movePositionForVerticalReachabilityToNextTopStop() {
- updatePositionForVerticalReachability(prev -> Math.max(prev - 1, 0));
+ void movePositionForVerticalReachabilityToNextTopStop(boolean isDeviceInTabletopMode) {
+ updatePositionForVerticalReachability(isDeviceInTabletopMode, prev -> Math.max(
+ prev - (isDeviceInTabletopMode ? 2 : 1), 0)); // Move 2 stops in tabletop mode to
+ // avoid center.
}
/**
@@ -854,30 +947,95 @@
}
/** Calculates a new letterboxPositionForHorizontalReachability value and updates the store */
- private void updatePositionForHorizontalReachability(
+ private void updatePositionForHorizontalReachability(boolean isDeviceInBookMode,
Function<Integer, Integer> newHorizonalPositionFun) {
final int letterboxPositionForHorizontalReachability =
- mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
+ mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(
+ isDeviceInBookMode);
final int nextHorizontalPosition = newHorizonalPositionFun.apply(
letterboxPositionForHorizontalReachability);
mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability(
- nextHorizontalPosition);
+ isDeviceInBookMode, nextHorizontalPosition);
}
/** Calculates a new letterboxPositionForVerticalReachability value and updates the store */
- private void updatePositionForVerticalReachability(
+ private void updatePositionForVerticalReachability(boolean isDeviceInTabletopMode,
Function<Integer, Integer> newVerticalPositionFun) {
final int letterboxPositionForVerticalReachability =
- mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
+ mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(
+ isDeviceInTabletopMode);
final int nextVerticalPosition = newVerticalPositionFun.apply(
letterboxPositionForVerticalReachability);
mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability(
- nextVerticalPosition);
+ isDeviceInTabletopMode, nextVerticalPosition);
}
- // TODO(b/262378106): Cache runtime flag and implement DeviceConfig.OnPropertiesChangedListener
+ // TODO(b/262378106): Cache a runtime flag and implement
+ // DeviceConfig.OnPropertiesChangedListener
static boolean isTranslucentLetterboxingAllowed() {
return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
"enable_translucent_activity_letterbox", false);
}
+
+ // TODO(b/262866240): Add listener to check for device config property
+ /** Whether fake sending focus is enabled for unfocused apps in splitscreen */
+ boolean isCompatFakeFocusEnabled() {
+ return mIsCompatFakeFocusEnabled && DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_WINDOW_MANAGER, "enable_compat_fake_focus", true);
+ }
+
+ /** Whether camera compatibility treatment is enabled. */
+ boolean isCameraCompatTreatmentEnabled(boolean checkDeviceConfig) {
+ return mIsCameraCompatTreatmentEnabled
+ && (!checkDeviceConfig || isCameraCompatTreatmentAllowed());
+ }
+
+ // TODO(b/262977416): Cache a runtime flag and implement
+ // DeviceConfig.OnPropertiesChangedListener
+ private static boolean isCameraCompatTreatmentAllowed() {
+ return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
+ "enable_camera_compat_treatment", false);
+ }
+
+ /** Whether camera compatibility refresh is enabled. */
+ boolean isCameraCompatRefreshEnabled() {
+ return mIsCameraCompatTreatmentRefreshEnabled;
+ }
+
+ /** Overrides whether camera compatibility treatment is enabled. */
+ void setCameraCompatRefreshEnabled(boolean enabled) {
+ mIsCameraCompatTreatmentRefreshEnabled = enabled;
+ }
+
+ /**
+ * Resets whether camera compatibility treatment is enabled to {@code true}.
+ */
+ void resetCameraCompatRefreshEnabled() {
+ mIsCameraCompatTreatmentRefreshEnabled = true;
+ }
+
+ /**
+ * Whether activity "refresh" in camera compatibility treatment should happen using the
+ * "stopped -> resumed" cycle rather than "paused -> resumed" cycle.
+ */
+ boolean isCameraCompatRefreshCycleThroughStopEnabled() {
+ return mIsCameraCompatRefreshCycleThroughStopEnabled;
+ }
+
+ /**
+ * Overrides whether activity "refresh" in camera compatibility treatment should happen using
+ * "stopped -> resumed" cycle rather than "paused -> resumed" cycle.
+ */
+ void setCameraCompatRefreshCycleThroughStopEnabled(boolean enabled) {
+ mIsCameraCompatRefreshCycleThroughStopEnabled = enabled;
+ }
+
+ /**
+ * Resets whether activity "refresh" in camera compatibility treatment should happen using
+ * "stopped -> resumed" cycle rather than "paused -> resumed" cycle to {@code true}.
+ */
+ void resetCameraCompatRefreshCycleThroughStopEnabled() {
+ mIsCameraCompatRefreshCycleThroughStopEnabled = true;
+ }
+
}
diff --git a/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java b/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java
index 70639b1..4a99db5 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java
+++ b/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java
@@ -55,6 +55,8 @@
private final Context mContext;
private final Supplier<Integer> mDefaultHorizontalReachabilitySupplier;
private final Supplier<Integer> mDefaultVerticalReachabilitySupplier;
+ private final Supplier<Integer> mDefaultBookModeReachabilitySupplier;
+ private final Supplier<Integer> mDefaultTabletopModeReachabilitySupplier;
// Horizontal position of a center of the letterboxed app window which is global to prevent
// "jumps" when switching between letterboxed apps. It's updated to reposition the app window
@@ -64,6 +66,11 @@
@LetterboxHorizontalReachabilityPosition
private volatile int mLetterboxPositionForHorizontalReachability;
+ // The same as mLetterboxPositionForHorizontalReachability but used when the device is
+ // half-folded.
+ @LetterboxHorizontalReachabilityPosition
+ private volatile int mLetterboxPositionForBookModeReachability;
+
// Vertical position of a center of the letterboxed app window which is global to prevent
// "jumps" when switching between letterboxed apps. It's updated to reposition the app window
// in response to a double tap gesture (see LetterboxUiController#handleDoubleTap). Used in
@@ -72,6 +79,11 @@
@LetterboxVerticalReachabilityPosition
private volatile int mLetterboxPositionForVerticalReachability;
+ // The same as mLetterboxPositionForVerticalReachability but used when the device is
+ // half-folded.
+ @LetterboxVerticalReachabilityPosition
+ private volatile int mLetterboxPositionForTabletopModeReachability;
+
@NonNull
private final AtomicFile mConfigurationFile;
@@ -83,9 +95,13 @@
LetterboxConfigurationPersister(Context systemUiContext,
Supplier<Integer> defaultHorizontalReachabilitySupplier,
- Supplier<Integer> defaultVerticalReachabilitySupplier) {
+ Supplier<Integer> defaultVerticalReachabilitySupplier,
+ Supplier<Integer> defaultBookModeReachabilitySupplier,
+ Supplier<Integer> defaultTabletopModeReachabilitySupplier) {
this(systemUiContext, defaultHorizontalReachabilitySupplier,
defaultVerticalReachabilitySupplier,
+ defaultBookModeReachabilitySupplier,
+ defaultTabletopModeReachabilitySupplier,
Environment.getDataSystemDirectory(), new PersisterQueue(),
/* completionCallback */ null);
}
@@ -93,11 +109,18 @@
@VisibleForTesting
LetterboxConfigurationPersister(Context systemUiContext,
Supplier<Integer> defaultHorizontalReachabilitySupplier,
- Supplier<Integer> defaultVerticalReachabilitySupplier, File configFolder,
+ Supplier<Integer> defaultVerticalReachabilitySupplier,
+ Supplier<Integer> defaultBookModeReachabilitySupplier,
+ Supplier<Integer> defaultTabletopModeReachabilitySupplier,
+ File configFolder,
PersisterQueue persisterQueue, @Nullable Consumer<String> completionCallback) {
mContext = systemUiContext.createDeviceProtectedStorageContext();
mDefaultHorizontalReachabilitySupplier = defaultHorizontalReachabilitySupplier;
mDefaultVerticalReachabilitySupplier = defaultVerticalReachabilitySupplier;
+ mDefaultBookModeReachabilitySupplier =
+ defaultBookModeReachabilitySupplier;
+ mDefaultTabletopModeReachabilitySupplier =
+ defaultTabletopModeReachabilitySupplier;
mCompletionCallback = completionCallback;
final File prefFiles = new File(configFolder, LETTERBOX_CONFIGURATION_FILENAME);
mConfigurationFile = new AtomicFile(prefFiles);
@@ -117,8 +140,12 @@
* enabled.
*/
@LetterboxHorizontalReachabilityPosition
- int getLetterboxPositionForHorizontalReachability() {
- return mLetterboxPositionForHorizontalReachability;
+ int getLetterboxPositionForHorizontalReachability(boolean forBookMode) {
+ if (forBookMode) {
+ return mLetterboxPositionForBookModeReachability;
+ } else {
+ return mLetterboxPositionForHorizontalReachability;
+ }
}
/*
@@ -126,31 +153,55 @@
* enabled.
*/
@LetterboxVerticalReachabilityPosition
- int getLetterboxPositionForVerticalReachability() {
- return mLetterboxPositionForVerticalReachability;
- }
-
- /**
- * Updates letterboxPositionForVerticalReachability if different from the current value
- */
- void setLetterboxPositionForHorizontalReachability(
- int letterboxPositionForHorizontalReachability) {
- if (mLetterboxPositionForHorizontalReachability
- != letterboxPositionForHorizontalReachability) {
- mLetterboxPositionForHorizontalReachability =
- letterboxPositionForHorizontalReachability;
- updateConfiguration();
+ int getLetterboxPositionForVerticalReachability(boolean forTabletopMode) {
+ if (forTabletopMode) {
+ return mLetterboxPositionForTabletopModeReachability;
+ } else {
+ return mLetterboxPositionForVerticalReachability;
}
}
/**
* Updates letterboxPositionForVerticalReachability if different from the current value
*/
- void setLetterboxPositionForVerticalReachability(
+ void setLetterboxPositionForHorizontalReachability(boolean forBookMode,
+ int letterboxPositionForHorizontalReachability) {
+ if (forBookMode) {
+ if (mLetterboxPositionForBookModeReachability
+ != letterboxPositionForHorizontalReachability) {
+ mLetterboxPositionForBookModeReachability =
+ letterboxPositionForHorizontalReachability;
+ updateConfiguration();
+ }
+ } else {
+ if (mLetterboxPositionForHorizontalReachability
+ != letterboxPositionForHorizontalReachability) {
+ mLetterboxPositionForHorizontalReachability =
+ letterboxPositionForHorizontalReachability;
+ updateConfiguration();
+ }
+ }
+ }
+
+ /**
+ * Updates letterboxPositionForVerticalReachability if different from the current value
+ */
+ void setLetterboxPositionForVerticalReachability(boolean forTabletopMode,
int letterboxPositionForVerticalReachability) {
- if (mLetterboxPositionForVerticalReachability != letterboxPositionForVerticalReachability) {
- mLetterboxPositionForVerticalReachability = letterboxPositionForVerticalReachability;
- updateConfiguration();
+ if (forTabletopMode) {
+ if (mLetterboxPositionForTabletopModeReachability
+ != letterboxPositionForVerticalReachability) {
+ mLetterboxPositionForTabletopModeReachability =
+ letterboxPositionForVerticalReachability;
+ updateConfiguration();
+ }
+ } else {
+ if (mLetterboxPositionForVerticalReachability
+ != letterboxPositionForVerticalReachability) {
+ mLetterboxPositionForVerticalReachability =
+ letterboxPositionForVerticalReachability;
+ updateConfiguration();
+ }
}
}
@@ -158,6 +209,10 @@
void useDefaultValue() {
mLetterboxPositionForHorizontalReachability = mDefaultHorizontalReachabilitySupplier.get();
mLetterboxPositionForVerticalReachability = mDefaultVerticalReachabilitySupplier.get();
+ mLetterboxPositionForBookModeReachability =
+ mDefaultBookModeReachabilitySupplier.get();
+ mLetterboxPositionForTabletopModeReachability =
+ mDefaultTabletopModeReachabilitySupplier.get();
}
private void readCurrentConfiguration() {
@@ -171,6 +226,10 @@
letterboxData.letterboxPositionForHorizontalReachability;
mLetterboxPositionForVerticalReachability =
letterboxData.letterboxPositionForVerticalReachability;
+ mLetterboxPositionForBookModeReachability =
+ letterboxData.letterboxPositionForBookModeReachability;
+ mLetterboxPositionForTabletopModeReachability =
+ letterboxData.letterboxPositionForTabletopModeReachability;
} catch (IOException ioe) {
Slog.e(TAG,
"Error reading from LetterboxConfigurationPersister. "
@@ -192,6 +251,8 @@
mPersisterQueue.addItem(new UpdateValuesCommand(mConfigurationFile,
mLetterboxPositionForHorizontalReachability,
mLetterboxPositionForVerticalReachability,
+ mLetterboxPositionForBookModeReachability,
+ mLetterboxPositionForTabletopModeReachability,
mCompletionCallback), /* flush */ true);
}
@@ -221,13 +282,18 @@
private final int mHorizontalReachability;
private final int mVerticalReachability;
+ private final int mBookModeReachability;
+ private final int mTabletopModeReachability;
UpdateValuesCommand(@NonNull AtomicFile fileToUpdate,
int horizontalReachability, int verticalReachability,
+ int bookModeReachability, int tabletopModeReachability,
@Nullable Consumer<String> onComplete) {
mFileToUpdate = fileToUpdate;
mHorizontalReachability = horizontalReachability;
mVerticalReachability = verticalReachability;
+ mBookModeReachability = bookModeReachability;
+ mTabletopModeReachability = tabletopModeReachability;
mOnComplete = onComplete;
}
@@ -237,6 +303,10 @@
new WindowManagerProtos.LetterboxProto();
letterboxData.letterboxPositionForHorizontalReachability = mHorizontalReachability;
letterboxData.letterboxPositionForVerticalReachability = mVerticalReachability;
+ letterboxData.letterboxPositionForBookModeReachability =
+ mBookModeReachability;
+ letterboxData.letterboxPositionForTabletopModeReachability =
+ mTabletopModeReachability;
final byte[] bytes = WindowManagerProtos.LetterboxProto.toByteArray(letterboxData);
FileOutputStream fos = null;
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index a53a5fc..fd7e082 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -80,6 +80,7 @@
// SizeCompatTests and LetterboxTests but not all.
// TODO(b/185264020): Consider making LetterboxUiController applicable to any level of the
// hierarchy in addition to ActivityRecord (Task, DisplayArea, ...).
+// TODO(b/263021211): Consider renaming to more generic CompatUIController.
final class LetterboxUiController {
private static final String TAG = TAG_WITH_CLASS_NAME ? "LetterboxUiController" : TAG_ATM;
@@ -125,6 +126,11 @@
@Nullable
private Letterbox mLetterbox;
+ // Whether activity "refresh" was requested but not finished in
+ // ActivityRecord#activityResumedLocked following the camera compat force rotation in
+ // DisplayRotationCompatPolicy.
+ private boolean mIsRefreshAfterRotationRequested;
+
LetterboxUiController(WindowManagerService wmService, ActivityRecord activityRecord) {
mLetterboxConfiguration = wmService.mLetterboxConfiguration;
// Given activityRecord may not be fully constructed since LetterboxUiController
@@ -147,6 +153,18 @@
}
}
+ /**
+ * Whether activity "refresh" was requested but not finished in {@link #activityResumedLocked}
+ * following the camera compat force rotation in {@link DisplayRotationCompatPolicy}.
+ */
+ boolean isRefreshAfterRotationRequested() {
+ return mIsRefreshAfterRotationRequested;
+ }
+
+ void setIsRefreshAfterRotationRequested(boolean isRequested) {
+ mIsRefreshAfterRotationRequested = isRequested;
+ }
+
boolean hasWallpaperBackgroundForLetterbox() {
return mShowWallpaperForLetterboxBackground;
}
@@ -275,30 +293,62 @@
&& mActivityRecord.fillsParent();
}
+ // Check if we are in the given pose and in fullscreen mode.
+ // Note that we check the task rather than the parent as with ActivityEmbedding the parent might
+ // be a TaskFragment, and its windowing mode is always MULTI_WINDOW, even if the task is
+ // actually fullscreen.
+ private boolean isDisplayFullScreenAndInPosture(DeviceStateController.FoldState state,
+ boolean isTabletop) {
+ Task task = mActivityRecord.getTask();
+ return mActivityRecord.mDisplayContent != null
+ && mActivityRecord.mDisplayContent.getDisplayRotation().isDeviceInPosture(state,
+ isTabletop)
+ && task != null
+ && task.getWindowingMode() == WINDOWING_MODE_FULLSCREEN;
+ }
+
+ // Note that we check the task rather than the parent as with ActivityEmbedding the parent might
+ // be a TaskFragment, and its windowing mode is always MULTI_WINDOW, even if the task is
+ // actually fullscreen.
+ private boolean isDisplayFullScreenAndSeparatingHinge() {
+ Task task = mActivityRecord.getTask();
+ return mActivityRecord.mDisplayContent != null
+ && mActivityRecord.mDisplayContent.getDisplayRotation().isDisplaySeparatingHinge()
+ && task != null
+ && task.getWindowingMode() == WINDOWING_MODE_FULLSCREEN;
+ }
+
+
float getHorizontalPositionMultiplier(Configuration parentConfiguration) {
// Don't check resolved configuration because it may not be updated yet during
// configuration change.
+ boolean bookMode = isDisplayFullScreenAndInPosture(
+ DeviceStateController.FoldState.HALF_FOLDED, false /* isTabletop */);
return isHorizontalReachabilityEnabled(parentConfiguration)
// Using the last global dynamic position to avoid "jumps" when moving
// between apps or activities.
- ? mLetterboxConfiguration.getHorizontalMultiplierForReachability()
- : mLetterboxConfiguration.getLetterboxHorizontalPositionMultiplier();
+ ? mLetterboxConfiguration.getHorizontalMultiplierForReachability(bookMode)
+ : mLetterboxConfiguration.getLetterboxHorizontalPositionMultiplier(bookMode);
}
float getVerticalPositionMultiplier(Configuration parentConfiguration) {
// Don't check resolved configuration because it may not be updated yet during
// configuration change.
+ boolean tabletopMode = isDisplayFullScreenAndInPosture(
+ DeviceStateController.FoldState.HALF_FOLDED, true /* isTabletop */);
return isVerticalReachabilityEnabled(parentConfiguration)
// Using the last global dynamic position to avoid "jumps" when moving
// between apps or activities.
- ? mLetterboxConfiguration.getVerticalMultiplierForReachability()
- : mLetterboxConfiguration.getLetterboxVerticalPositionMultiplier();
+ ? mLetterboxConfiguration.getVerticalMultiplierForReachability(tabletopMode)
+ : mLetterboxConfiguration.getLetterboxVerticalPositionMultiplier(tabletopMode);
}
float getFixedOrientationLetterboxAspectRatio() {
- return mActivityRecord.shouldCreateCompatDisplayInsets()
- ? getDefaultMinAspectRatioForUnresizableApps()
- : mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio();
+ return isDisplayFullScreenAndSeparatingHinge()
+ ? getSplitScreenAspectRatio()
+ : mActivityRecord.shouldCreateCompatDisplayInsets()
+ ? getDefaultMinAspectRatioForUnresizableApps()
+ : mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio();
}
private float getDefaultMinAspectRatioForUnresizableApps() {
@@ -351,11 +401,13 @@
return;
}
+ boolean isInFullScreenBookMode = isDisplayFullScreenAndSeparatingHinge();
int letterboxPositionForHorizontalReachability = mLetterboxConfiguration
- .getLetterboxPositionForHorizontalReachability();
+ .getLetterboxPositionForHorizontalReachability(isInFullScreenBookMode);
if (mLetterbox.getInnerFrame().left > x) {
// Moving to the next stop on the left side of the app window: right > center > left.
- mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextLeftStop();
+ mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextLeftStop(
+ isInFullScreenBookMode);
int changeToLog =
letterboxPositionForHorizontalReachability
== LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER
@@ -364,7 +416,8 @@
logLetterboxPositionChange(changeToLog);
} else if (mLetterbox.getInnerFrame().right < x) {
// Moving to the next stop on the right side of the app window: left > center > right.
- mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextRightStop();
+ mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextRightStop(
+ isInFullScreenBookMode);
int changeToLog =
letterboxPositionForHorizontalReachability
== LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER
@@ -388,11 +441,13 @@
// Only react to clicks at the top and bottom of the letterboxed app window.
return;
}
+ boolean isInFullScreenTabletopMode = isDisplayFullScreenAndSeparatingHinge();
int letterboxPositionForVerticalReachability = mLetterboxConfiguration
- .getLetterboxPositionForVerticalReachability();
+ .getLetterboxPositionForVerticalReachability(isInFullScreenTabletopMode);
if (mLetterbox.getInnerFrame().top > y) {
// Moving to the next stop on the top side of the app window: bottom > center > top.
- mLetterboxConfiguration.movePositionForVerticalReachabilityToNextTopStop();
+ mLetterboxConfiguration.movePositionForVerticalReachabilityToNextTopStop(
+ isInFullScreenTabletopMode);
int changeToLog =
letterboxPositionForVerticalReachability
== LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER
@@ -401,7 +456,8 @@
logLetterboxPositionChange(changeToLog);
} else if (mLetterbox.getInnerFrame().bottom < y) {
// Moving to the next stop on the bottom side of the app window: top > center > bottom.
- mLetterboxConfiguration.movePositionForVerticalReachabilityToNextBottomStop();
+ mLetterboxConfiguration.movePositionForVerticalReachabilityToNextBottomStop(
+ isInFullScreenTabletopMode);
int changeToLog =
letterboxPositionForVerticalReachability
== LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER
@@ -712,10 +768,10 @@
+ getVerticalPositionMultiplier(mActivityRecord.getParent().getConfiguration()));
pw.println(prefix + " letterboxPositionForHorizontalReachability="
+ LetterboxConfiguration.letterboxHorizontalReachabilityPositionToString(
- mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability()));
+ mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability(false)));
pw.println(prefix + " letterboxPositionForVerticalReachability="
+ LetterboxConfiguration.letterboxVerticalReachabilityPositionToString(
- mLetterboxConfiguration.getLetterboxPositionForVerticalReachability()));
+ mLetterboxConfiguration.getLetterboxPositionForVerticalReachability(false)));
pw.println(prefix + " fixedOrientationLetterboxAspectRatio="
+ mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio());
pw.println(prefix + " defaultMinAspectRatioForUnresizableApps="
@@ -780,14 +836,20 @@
int positionToLog = APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__UNKNOWN_POSITION;
if (isHorizontalReachabilityEnabled()) {
int letterboxPositionForHorizontalReachability = getLetterboxConfiguration()
- .getLetterboxPositionForHorizontalReachability();
+ .getLetterboxPositionForHorizontalReachability(
+ isDisplayFullScreenAndInPosture(
+ DeviceStateController.FoldState.HALF_FOLDED,
+ false /* isTabletop */));
positionToLog = letterboxHorizontalReachabilityPositionToLetterboxPosition(
- letterboxPositionForHorizontalReachability);
+ letterboxPositionForHorizontalReachability);
} else if (isVerticalReachabilityEnabled()) {
int letterboxPositionForVerticalReachability = getLetterboxConfiguration()
- .getLetterboxPositionForVerticalReachability();
+ .getLetterboxPositionForVerticalReachability(
+ isDisplayFullScreenAndInPosture(
+ DeviceStateController.FoldState.HALF_FOLDED,
+ true /* isTabletop */));
positionToLog = letterboxVerticalReachabilityPositionToLetterboxPosition(
- letterboxPositionForVerticalReachability);
+ letterboxPositionForVerticalReachability);
}
return positionToLog;
}
diff --git a/services/core/java/com/android/server/wm/PackageConfigPersister.java b/services/core/java/com/android/server/wm/PackageConfigPersister.java
index 18a7d2e..23127ac 100644
--- a/services/core/java/com/android/server/wm/PackageConfigPersister.java
+++ b/services/core/java/com/android/server/wm/PackageConfigPersister.java
@@ -16,6 +16,8 @@
package com.android.server.wm;
+import static android.content.res.Configuration.GRAMMATICAL_GENDER_NOT_SPECIFIED;
+
import android.annotation.NonNull;
import android.content.res.Configuration;
import android.os.Environment;
@@ -165,7 +167,8 @@
if (modifiedRecord != null) {
container.applyAppSpecificConfig(modifiedRecord.mNightMode,
LocaleOverlayHelper.combineLocalesIfOverlayExists(
- modifiedRecord.mLocales, mAtm.getGlobalConfiguration().getLocales()));
+ modifiedRecord.mLocales, mAtm.getGlobalConfiguration().getLocales()),
+ modifiedRecord.mGrammaticalGender);
}
}
}
@@ -188,16 +191,19 @@
}
boolean isNightModeChanged = updateNightMode(impl.getNightMode(), record);
boolean isLocalesChanged = updateLocales(impl.getLocales(), record);
+ boolean isGenderChanged = updateGender(impl.getGrammaticalGender(), record);
if ((record.mNightMode == null || record.isResetNightMode())
- && (record.mLocales == null || record.mLocales.isEmpty())) {
+ && (record.mLocales == null || record.mLocales.isEmpty())
+ && (record.mGrammaticalGender == null
+ || record.mGrammaticalGender == GRAMMATICAL_GENDER_NOT_SPECIFIED)) {
// if all values default to system settings, we can remove the package.
removePackage(packageName, userId);
// if there was a pre-existing record for the package that was deleted,
// we return true (since it was successfully deleted), else false (since there was
// no change to the previous state).
return isRecordPresent;
- } else if (!isNightModeChanged && !isLocalesChanged) {
+ } else if (!isNightModeChanged && !isLocalesChanged && !isGenderChanged) {
return false;
} else {
final PackageConfigRecord pendingRecord =
@@ -211,7 +217,8 @@
}
if (!updateNightMode(record.mNightMode, writeRecord)
- && !updateLocales(record.mLocales, writeRecord)) {
+ && !updateLocales(record.mLocales, writeRecord)
+ && !updateGender(record.mGrammaticalGender, writeRecord)) {
return false;
}
@@ -240,6 +247,15 @@
return true;
}
+ private boolean updateGender(@Configuration.GrammaticalGender Integer requestedGender,
+ PackageConfigRecord record) {
+ if (requestedGender == null || requestedGender.equals(record.mGrammaticalGender)) {
+ return false;
+ }
+ record.mGrammaticalGender = requestedGender;
+ return true;
+ }
+
@GuardedBy("mLock")
void removeUser(int userId) {
synchronized (mLock) {
@@ -305,7 +321,9 @@
return null;
}
return new ActivityTaskManagerInternal.PackageConfig(
- packageConfigRecord.mNightMode, packageConfigRecord.mLocales);
+ packageConfigRecord.mNightMode,
+ packageConfigRecord.mLocales,
+ packageConfigRecord.mGrammaticalGender);
}
}
@@ -336,6 +354,8 @@
final int mUserId;
Integer mNightMode;
LocaleList mLocales;
+ @Configuration.GrammaticalGender
+ Integer mGrammaticalGender;
PackageConfigRecord(String name, int userId) {
mName = name;
diff --git a/services/core/java/com/android/server/wm/PackageConfigurationUpdaterImpl.java b/services/core/java/com/android/server/wm/PackageConfigurationUpdaterImpl.java
index f3be66c..2cf8a4a 100644
--- a/services/core/java/com/android/server/wm/PackageConfigurationUpdaterImpl.java
+++ b/services/core/java/com/android/server/wm/PackageConfigurationUpdaterImpl.java
@@ -17,6 +17,7 @@
package com.android.server.wm;
import android.content.pm.PackageManager;
+import android.content.res.Configuration;
import android.os.Binder;
import android.os.LocaleList;
import android.util.ArraySet;
@@ -33,6 +34,8 @@
private final Optional<Integer> mPid;
private Integer mNightMode;
private LocaleList mLocales;
+ private @Configuration.GrammaticalGender
+ int mGrammaticalGender;
private String mPackageName;
private int mUserId;
private ActivityTaskManagerService mAtm;
@@ -68,6 +71,15 @@
}
@Override
+ public ActivityTaskManagerInternal.PackageConfigurationUpdater setGrammaticalGender(
+ @Configuration.GrammaticalGender int gender) {
+ synchronized (this) {
+ mGrammaticalGender = gender;
+ }
+ return this;
+ }
+
+ @Override
public boolean commit() {
synchronized (this) {
synchronized (mAtm.mGlobalLock) {
@@ -112,12 +124,12 @@
for (int i = processes.size() - 1; i >= 0; i--) {
final WindowProcessController wpc = processes.valueAt(i);
if (wpc.mInfo.packageName.equals(packageName)) {
- wpc.applyAppSpecificConfig(mNightMode, localesOverride);
+ wpc.applyAppSpecificConfig(mNightMode, localesOverride, mGrammaticalGender);
}
// Always inform individual activities about the update, since activities from other
// packages may be sharing this process
wpc.updateAppSpecificSettingsForAllActivitiesInPackage(packageName, mNightMode,
- localesOverride);
+ localesOverride, mGrammaticalGender);
}
}
@@ -128,4 +140,9 @@
LocaleList getLocales() {
return mLocales;
}
+
+ @Configuration.GrammaticalGender
+ Integer getGrammaticalGender() {
+ return mGrammaticalGender;
+ }
}
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 13da96f..fd5c5eb 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -442,10 +442,14 @@
mDisplayOffTokenAcquirer = mService.new SleepTokenAcquirerImpl(DISPLAY_OFF_SLEEP_TOKEN_TAG);
}
+ /**
+ * Updates the children's focused window and the top focused display if needed.
+ */
boolean updateFocusedWindowLocked(int mode, boolean updateInputWindows) {
mTopFocusedAppByProcess.clear();
boolean changed = false;
int topFocusedDisplayId = INVALID_DISPLAY;
+ // Go through the children in z-order starting at the top-most
for (int i = mChildren.size() - 1; i >= 0; --i) {
final DisplayContent dc = mChildren.get(i);
changed |= dc.updateFocusedWindowLocked(mode, updateInputWindows, topFocusedDisplayId);
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index ae3b2f2..b887861 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -1444,7 +1444,7 @@
next.abortAndClearOptionsAnimation();
transaction.setLifecycleStateRequest(
ResumeActivityItem.obtain(next.app.getReportedProcState(),
- dc.isNextTransitionForward()));
+ dc.isNextTransitionForward(), next.shouldSendCompatFakeFocus()));
mAtmService.getLifecycleManager().scheduleTransaction(transaction);
ProtoLog.d(WM_DEBUG_STATES, "resumeTopActivity: Resumed %s", next);
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index ce41ae7..6737052 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -1305,6 +1305,11 @@
if (parent != null) {
parent.onChildVisibleRequestedChanged(this);
}
+
+ // Notify listeners about visibility change.
+ for (int i = mListeners.size() - 1; i >= 0; --i) {
+ mListeners.get(i).onVisibleRequestedChanged(mVisibleRequested);
+ }
return true;
}
diff --git a/services/core/java/com/android/server/wm/WindowContainerListener.java b/services/core/java/com/android/server/wm/WindowContainerListener.java
index ac1fe17..c1ee254 100644
--- a/services/core/java/com/android/server/wm/WindowContainerListener.java
+++ b/services/core/java/com/android/server/wm/WindowContainerListener.java
@@ -27,4 +27,13 @@
/** Called when {@link WindowContainer#removeImmediately()} is invoked. */
default void onRemoved() {}
+
+ /**
+ * Only invoked if the child successfully requested a visibility change.
+ *
+ * @param isVisibleRequested The current {@link WindowContainer#isVisibleRequested()} of this
+ * {@link WindowContainer} (not of the child).
+ * @see WindowContainer#onChildVisibleRequestedChanged(WindowContainer)
+ */
+ default void onVisibleRequestedChanged(boolean isVisibleRequested) { }
}
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 70bedc7..b83f423 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -563,7 +563,7 @@
/** Mapping from an InputWindowHandle token to the server's Window object. */
final HashMap<IBinder, WindowState> mInputToWindowMap = new HashMap<>();
- /** Global service lock used by the package the owns this service. */
+ /** Global service lock used by the package that owns this service. */
final WindowManagerGlobalLock mGlobalLock;
/**
@@ -3165,15 +3165,41 @@
}
@Override
- public void moveDisplayToTop(int displayId) {
+ public void moveDisplayToTopIfAllowed(int displayId) {
+ moveDisplayToTopInternal(displayId);
+ syncInputTransactions(true /* waitForAnimations */);
+ }
+
+ /**
+ * Moves the given display to the top. If it cannot be moved to the top this method does
+ * nothing.
+ */
+ void moveDisplayToTopInternal(int displayId) {
synchronized (mGlobalLock) {
final DisplayContent displayContent = mRoot.getDisplayContent(displayId);
if (displayContent != null && mRoot.getTopChild() != displayContent) {
+ // Check whether anything prevents us from moving the display to the top.
+ if (!displayContent.canStealTopFocus()) {
+ ProtoLog.i(WM_DEBUG_FOCUS_LIGHT,
+ "Not moving display (displayId=%d) to top. Top focused displayId=%d. "
+ + "Reason: FLAG_STEAL_TOP_FOCUS_DISABLED",
+ displayId, mRoot.getTopFocusedDisplayContent().getDisplayId());
+ return;
+ }
+
+ if (mPerDisplayFocusEnabled) {
+ ProtoLog.i(WM_DEBUG_FOCUS_LIGHT,
+ "Not moving display (displayId=%d) to top. Top focused displayId=%d. "
+ + "Reason: config_perDisplayFocusEnabled", displayId,
+ mRoot.getTopFocusedDisplayContent().getDisplayId());
+ return;
+ }
+
+ // Nothing prevented us from moving the display to the top. Let's do it!
displayContent.getParent().positionChildAt(WindowContainer.POSITION_TOP,
displayContent, true /* includingParents */);
}
}
- syncInputTransactions(true /* waitForAnimations */);
}
@Override
diff --git a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
index 060784d..e2c9c17 100644
--- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
+++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
@@ -53,6 +53,7 @@
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
+import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
@@ -821,54 +822,6 @@
return 0;
}
- private int runSetLetterboxIsHorizontalReachabilityEnabled(PrintWriter pw)
- throws RemoteException {
- 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.setIsHorizontalReachabilityEnabled(enabled);
- }
- return 0;
- }
-
- private int runSetLetterboxIsVerticalReachabilityEnabled(PrintWriter pw)
- throws RemoteException {
- 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.setIsVerticalReachabilityEnabled(enabled);
- }
- return 0;
- }
-
private int runSetLetterboxDefaultPositionForHorizontalReachability(PrintWriter pw)
throws RemoteException {
@LetterboxHorizontalReachabilityPosition final int position;
@@ -931,32 +884,13 @@
return 0;
}
- private int runSetLetterboxIsEducationEnabled(PrintWriter pw) throws RemoteException {
- 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.setIsEducationEnabled(enabled);
- }
- return 0;
- }
-
- private int runSetLetterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled(PrintWriter pw)
+ private int runSetBooleanFlag(PrintWriter pw, Consumer<Boolean> setter)
throws RemoteException {
String arg = getNextArg();
+ if (arg == null) {
+ getErrPrintWriter().println("Error: expected true, 1, false, 0, but got empty input.");
+ return -1;
+ }
final boolean enabled;
switch (arg) {
case "true":
@@ -973,30 +907,7 @@
}
synchronized (mInternal.mGlobalLock) {
- mLetterboxConfiguration.setIsSplitScreenAspectRatioForUnresizableAppsEnabled(enabled);
- }
- 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);
+ setter.accept(enabled);
}
return 0;
}
@@ -1039,10 +950,12 @@
runSetLetterboxVerticalPositionMultiplier(pw);
break;
case "--isHorizontalReachabilityEnabled":
- runSetLetterboxIsHorizontalReachabilityEnabled(pw);
+ runSetBooleanFlag(pw, mLetterboxConfiguration
+ ::setIsHorizontalReachabilityEnabled);
break;
case "--isVerticalReachabilityEnabled":
- runSetLetterboxIsVerticalReachabilityEnabled(pw);
+ runSetBooleanFlag(pw, mLetterboxConfiguration
+ ::setIsVerticalReachabilityEnabled);
break;
case "--defaultPositionForHorizontalReachability":
runSetLetterboxDefaultPositionForHorizontalReachability(pw);
@@ -1051,13 +964,23 @@
runSetLetterboxDefaultPositionForVerticalReachability(pw);
break;
case "--isEducationEnabled":
- runSetLetterboxIsEducationEnabled(pw);
+ runSetBooleanFlag(pw, mLetterboxConfiguration::setIsEducationEnabled);
break;
case "--isSplitScreenAspectRatioForUnresizableAppsEnabled":
- runSetLetterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled(pw);
+ runSetBooleanFlag(pw, mLetterboxConfiguration
+ ::setIsSplitScreenAspectRatioForUnresizableAppsEnabled);
break;
case "--isTranslucentLetterboxingEnabled":
- runSetTranslucentLetterboxingEnabled(pw);
+ runSetBooleanFlag(pw, mLetterboxConfiguration
+ ::setTranslucentLetterboxingOverrideEnabled);
+ break;
+ case "--isCameraCompatRefreshEnabled":
+ runSetBooleanFlag(pw, enabled -> mLetterboxConfiguration
+ .setCameraCompatRefreshEnabled(enabled));
+ break;
+ case "--isCameraCompatRefreshCycleThroughStopEnabled":
+ runSetBooleanFlag(pw, enabled -> mLetterboxConfiguration
+ .setCameraCompatRefreshCycleThroughStopEnabled(enabled));
break;
default:
getErrPrintWriter().println(
@@ -1104,27 +1027,34 @@
mLetterboxConfiguration.resetLetterboxVerticalPositionMultiplier();
break;
case "isHorizontalReachabilityEnabled":
- mLetterboxConfiguration.getIsHorizontalReachabilityEnabled();
+ mLetterboxConfiguration.resetIsHorizontalReachabilityEnabled();
break;
case "isVerticalReachabilityEnabled":
- mLetterboxConfiguration.getIsVerticalReachabilityEnabled();
+ mLetterboxConfiguration.resetIsVerticalReachabilityEnabled();
break;
case "defaultPositionForHorizontalReachability":
- mLetterboxConfiguration.getDefaultPositionForHorizontalReachability();
+ mLetterboxConfiguration.resetDefaultPositionForHorizontalReachability();
break;
case "defaultPositionForVerticalReachability":
- mLetterboxConfiguration.getDefaultPositionForVerticalReachability();
+ mLetterboxConfiguration.resetDefaultPositionForVerticalReachability();
break;
case "isEducationEnabled":
- mLetterboxConfiguration.getIsEducationEnabled();
+ mLetterboxConfiguration.resetIsEducationEnabled();
break;
case "isSplitScreenAspectRatioForUnresizableAppsEnabled":
mLetterboxConfiguration
- .getIsSplitScreenAspectRatioForUnresizableAppsEnabled();
+ .resetIsSplitScreenAspectRatioForUnresizableAppsEnabled();
break;
case "isTranslucentLetterboxingEnabled":
mLetterboxConfiguration.resetTranslucentLetterboxingEnabled();
break;
+ case "isCameraCompatRefreshEnabled":
+ mLetterboxConfiguration.resetCameraCompatRefreshEnabled();
+ break;
+ case "isCameraCompatRefreshCycleThroughStopEnabled":
+ mLetterboxConfiguration
+ .resetCameraCompatRefreshCycleThroughStopEnabled();
+ break;
default:
getErrPrintWriter().println(
"Error: Unrecognized letterbox style option: " + arg);
@@ -1226,6 +1156,8 @@
mLetterboxConfiguration.resetIsEducationEnabled();
mLetterboxConfiguration.resetIsSplitScreenAspectRatioForUnresizableAppsEnabled();
mLetterboxConfiguration.resetTranslucentLetterboxingEnabled();
+ mLetterboxConfiguration.resetCameraCompatRefreshEnabled();
+ mLetterboxConfiguration.resetCameraCompatRefreshCycleThroughStopEnabled();
}
}
@@ -1234,9 +1166,17 @@
pw.println("Corner radius: "
+ mLetterboxConfiguration.getLetterboxActivityCornersRadius());
pw.println("Horizontal position multiplier: "
- + mLetterboxConfiguration.getLetterboxHorizontalPositionMultiplier());
+ + mLetterboxConfiguration.getLetterboxHorizontalPositionMultiplier(
+ false /* isInBookMode */));
pw.println("Vertical position multiplier: "
- + mLetterboxConfiguration.getLetterboxVerticalPositionMultiplier());
+ + mLetterboxConfiguration.getLetterboxVerticalPositionMultiplier(
+ false /* isInTabletopMode */));
+ pw.println("Horizontal position multiplier (book mode): "
+ + mLetterboxConfiguration.getLetterboxHorizontalPositionMultiplier(
+ true /* isInBookMode */));
+ pw.println("Vertical position multiplier (tabletop mode): "
+ + mLetterboxConfiguration.getLetterboxVerticalPositionMultiplier(
+ true /* isInTabletopMode */));
pw.println("Aspect ratio: "
+ mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio());
pw.println("Default min aspect ratio for unresizable apps: "
@@ -1253,15 +1193,21 @@
mLetterboxConfiguration.getDefaultPositionForVerticalReachability()));
pw.println("Current position for horizontal reachability:"
+ LetterboxConfiguration.letterboxHorizontalReachabilityPositionToString(
- mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability()));
+ mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability(false)));
pw.println("Current position for vertical reachability:"
+ LetterboxConfiguration.letterboxVerticalReachabilityPositionToString(
- mLetterboxConfiguration.getLetterboxPositionForVerticalReachability()));
+ mLetterboxConfiguration.getLetterboxPositionForVerticalReachability(false)));
pw.println("Is education enabled: "
+ mLetterboxConfiguration.getIsEducationEnabled());
pw.println("Is using split screen aspect ratio as aspect ratio for unresizable apps: "
+ mLetterboxConfiguration
.getIsSplitScreenAspectRatioForUnresizableAppsEnabled());
+
+ pw.println(" Is activity \"refresh\" in camera compatibility treatment enabled: "
+ + mLetterboxConfiguration.isCameraCompatRefreshEnabled());
+ pw.println(" Refresh using \"stopped -> resumed\" cycle: "
+ + mLetterboxConfiguration.isCameraCompatRefreshCycleThroughStopEnabled());
+
pw.println("Background type: "
+ LetterboxConfiguration.letterboxBackgroundTypeToString(
mLetterboxConfiguration.getLetterboxBackgroundType()));
@@ -1471,7 +1417,12 @@
pw.println(" unresizable apps.");
pw.println(" --isTranslucentLetterboxingEnabled [true|1|false|0]");
pw.println(" Whether letterboxing for translucent activities is enabled.");
-
+ pw.println(" --isCameraCompatRefreshEnabled [true|1|false|0]");
+ pw.println(" Whether camera compatibility refresh is enabled.");
+ pw.println(" --isCameraCompatRefreshCycleThroughStopEnabled [true|1|false|0]");
+ pw.println(" Whether activity \"refresh\" in camera compatibility treatment should");
+ pw.println(" happen using the \"stopped -> resumed\" cycle rather than");
+ pw.println(" \"paused -> resumed\" cycle.");
pw.println(" reset-letterbox-style [aspectRatio|cornerRadius|backgroundType");
pw.println(" |backgroundColor|wallpaperBlurRadius|wallpaperDarkScrimAlpha");
pw.println(" |horizontalPositionMultiplier|verticalPositionMultiplier");
diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java
index dcd30bb..3e1dc7e 100644
--- a/services/core/java/com/android/server/wm/WindowProcessController.java
+++ b/services/core/java/com/android/server/wm/WindowProcessController.java
@@ -42,6 +42,7 @@
import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.server.wm.ActivityTaskManagerService.INSTRUMENTATION_KEY_DISPATCHING_TIMEOUT_MILLIS;
import static com.android.server.wm.ActivityTaskManagerService.RELAUNCH_REASON_NONE;
+import static com.android.server.wm.BackgroundActivityStartController.BAL_BLOCK;
import static com.android.server.wm.WindowManagerService.MY_PID;
import android.Manifest;
@@ -52,6 +53,7 @@
import android.app.IApplicationThread;
import android.app.ProfilerInfo;
import android.app.servertransaction.ConfigurationChangeItem;
+import android.companion.virtual.VirtualDeviceManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
@@ -206,6 +208,7 @@
/** Whether {@link #mLastReportedConfiguration} is deferred by the cached state. */
private volatile boolean mHasCachedConfiguration;
+ private int mTopActivityDeviceId = VirtualDeviceManager.DEVICE_ID_DEFAULT;
/**
* Registered {@link DisplayArea} as a listener to override config changes. {@code null} if not
* registered.
@@ -559,15 +562,17 @@
@HotPath(caller = HotPath.START_SERVICE)
public boolean areBackgroundFgsStartsAllowed() {
return areBackgroundActivityStartsAllowed(mAtm.getBalAppSwitchesState(),
- true /* isCheckingForFgsStart */);
+ true /* isCheckingForFgsStart */) != BAL_BLOCK;
}
- boolean areBackgroundActivityStartsAllowed(int appSwitchState) {
+ @BackgroundActivityStartController.BalCode
+ int areBackgroundActivityStartsAllowed(int appSwitchState) {
return areBackgroundActivityStartsAllowed(appSwitchState,
false /* isCheckingForFgsStart */);
}
- private boolean areBackgroundActivityStartsAllowed(int appSwitchState,
+ @BackgroundActivityStartController.BalCode
+ private int areBackgroundActivityStartsAllowed(int appSwitchState,
boolean isCheckingForFgsStart) {
return mBgLaunchController.areBackgroundActivityStartsAllowed(mPid, mUid, mInfo.packageName,
appSwitchState, isCheckingForFgsStart, hasActivityInVisibleTask(),
@@ -868,13 +873,13 @@
// TODO(b/199277729): Consider whether we need to add special casing for edge cases like
// activity-embeddings etc.
void updateAppSpecificSettingsForAllActivitiesInPackage(String packageName, Integer nightMode,
- LocaleList localesOverride) {
+ LocaleList localesOverride, @Configuration.GrammaticalGender int gender) {
for (int i = mActivities.size() - 1; i >= 0; --i) {
final ActivityRecord r = mActivities.get(i);
// Activities from other packages could be sharing this process. Only propagate updates
// to those activities that are part of the package whose app-specific settings changed
if (packageName.equals(r.packageName)
- && r.applyAppSpecificConfig(nightMode, localesOverride)
+ && r.applyAppSpecificConfig(nightMode, localesOverride, gender)
&& r.isVisibleRequested()) {
r.ensureActivityConfiguration(0 /* globalChanges */, true /* preserveWindow */);
}
@@ -1381,8 +1386,16 @@
@Override
public void onConfigurationChanged(Configuration newGlobalConfig) {
super.onConfigurationChanged(newGlobalConfig);
+
+ // If deviceId for the top-activity changed, schedule passing it to the app process.
+ boolean topActivityDeviceChanged = false;
+ int deviceId = getTopActivityDeviceId();
+ if (deviceId != mTopActivityDeviceId) {
+ topActivityDeviceChanged = true;
+ }
+
final Configuration config = getConfiguration();
- if (mLastReportedConfiguration.equals(config)) {
+ if (mLastReportedConfiguration.equals(config) & !topActivityDeviceChanged) {
// Nothing changed.
if (Build.IS_DEBUGGABLE && mHasImeService) {
// TODO (b/135719017): Temporary log for debugging IME service.
@@ -1396,7 +1409,34 @@
mHasPendingConfigurationChange = true;
return;
}
- dispatchConfiguration(config);
+
+ // TODO(b/263402938): Add tests that capture the deviceId dispatch to the client.
+ mTopActivityDeviceId = deviceId;
+ dispatchConfiguration(config, topActivityDeviceChanged ? mTopActivityDeviceId
+ : VirtualDeviceManager.DEVICE_ID_INVALID);
+ }
+
+ private int getTopActivityDeviceId() {
+ ActivityRecord topActivity = getTopNonFinishingActivity();
+ int updatedDeviceId = mTopActivityDeviceId;
+ if (topActivity != null && topActivity.mDisplayContent != null) {
+ updatedDeviceId = mAtm.mTaskSupervisor.getDeviceIdForDisplayId(
+ topActivity.mDisplayContent.mDisplayId);
+ }
+ return updatedDeviceId;
+ }
+
+ @Nullable
+ private ActivityRecord getTopNonFinishingActivity() {
+ if (mActivities.isEmpty()) {
+ return null;
+ }
+ for (int i = mActivities.size() - 1; i >= 0; i--) {
+ if (!mActivities.get(i).finishing) {
+ return mActivities.get(i);
+ }
+ }
+ return null;
}
@Override
@@ -1423,6 +1463,10 @@
}
void dispatchConfiguration(Configuration config) {
+ dispatchConfiguration(config, getTopActivityDeviceId());
+ }
+
+ void dispatchConfiguration(Configuration config, int deviceId) {
mHasPendingConfigurationChange = false;
if (mThread == null) {
if (Build.IS_DEBUGGABLE && mHasImeService) {
@@ -1449,10 +1493,16 @@
}
}
- scheduleConfigurationChange(mThread, config);
+ scheduleConfigurationChange(mThread, config, deviceId);
}
private void scheduleConfigurationChange(IApplicationThread thread, Configuration config) {
+ // By default send invalid deviceId as no-op signal so it's not updated on the client side.
+ scheduleConfigurationChange(thread, config, VirtualDeviceManager.DEVICE_ID_INVALID);
+ }
+
+ private void scheduleConfigurationChange(IApplicationThread thread, Configuration config,
+ int deviceId) {
ProtoLog.v(WM_DEBUG_CONFIGURATION, "Sending to proc %s new config %s", mName,
config);
if (Build.IS_DEBUGGABLE && mHasImeService) {
@@ -1462,7 +1512,7 @@
mHasCachedConfiguration = false;
try {
mAtm.getLifecycleManager().scheduleTransaction(thread,
- ConfigurationChangeItem.obtain(config));
+ ConfigurationChangeItem.obtain(config, deviceId));
} catch (Exception e) {
Slog.e(TAG_CONFIGURATION, "Failed to schedule configuration change: " + mOwner, e);
}
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 746f983..ae31ee8 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -6238,10 +6238,7 @@
@Override
public void handleTapOutsideFocusInsideSelf() {
final DisplayContent displayContent = getDisplayContent();
- if (!displayContent.isOnTop()) {
- displayContent.getParent().positionChildAt(WindowContainer.POSITION_TOP, displayContent,
- true /* includingParents */);
- }
+ mWmService.moveDisplayToTopInternal(getDisplayId());
mWmService.handleTaskFocusChange(getTask(), mActivityRecord);
}
diff --git a/services/core/java/com/android/server/wm/utils/StateMachine.java b/services/core/java/com/android/server/wm/utils/StateMachine.java
index 91a5dc4..350784e 100644
--- a/services/core/java/com/android/server/wm/utils/StateMachine.java
+++ b/services/core/java/com/android/server/wm/utils/StateMachine.java
@@ -177,19 +177,18 @@
}
/**
- * Process an event. Search handler for a given event and {@link Handler#handle(int)}. If the
- * handler cannot handle the event, delegate it to a handler for a parent of the given state.
+ * Process an event. Search handler for a given event and {@link Handler#handle(int, Object)}.
+ * If the handler cannot handle the event, delegate it to a handler for a parent of the given
+ * state.
*
* @param event Type of an event.
*/
public void handle(int event, @Nullable Object param) {
- int state = mState;
- while (state != 0) {
+ for (int state = mState;; state >>= 4) {
final Handler h = mStateHandlers.get(state);
- if (h != null && h.handle(event, param)) {
+ if ((h != null && h.handle(event, param)) || state == 0) {
return;
}
- state >>= 4;
}
}
diff --git a/services/core/jni/gnss/GnssMeasurementCallback.cpp b/services/core/jni/gnss/GnssMeasurementCallback.cpp
index d37f3bd..a1c5708 100644
--- a/services/core/jni/gnss/GnssMeasurementCallback.cpp
+++ b/services/core/jni/gnss/GnssMeasurementCallback.cpp
@@ -58,6 +58,7 @@
jmethodID method_gnssAgcBuilderBuild;
jmethodID method_gnssMeasurementsEventBuilderCtor;
jmethodID method_gnssMeasurementsEventBuilderSetClock;
+jmethodID method_gnssMeasurementsEventBuilderSetFullTracking;
jmethodID method_gnssMeasurementsEventBuilderSetMeasurements;
jmethodID method_gnssMeasurementsEventBuilderSetGnssAutomaticGainControls;
jmethodID method_gnssMeasurementsEventBuilderBuild;
@@ -109,6 +110,10 @@
env->GetMethodID(class_gnssMeasurementsEventBuilder, "setGnssAutomaticGainControls",
"([Landroid/location/GnssAutomaticGainControl;)"
"Landroid/location/GnssMeasurementsEvent$Builder;");
+ method_gnssMeasurementsEventBuilderSetFullTracking =
+ env->GetMethodID(class_gnssMeasurementsEventBuilder, "setFullTracking",
+ "(Z)"
+ "Landroid/location/GnssMeasurementsEvent$Builder;");
method_gnssMeasurementsEventBuilderBuild =
env->GetMethodID(class_gnssMeasurementsEventBuilder, "build",
"()Landroid/location/GnssMeasurementsEvent;");
@@ -228,7 +233,8 @@
}
void setMeasurementData(JNIEnv* env, jobject& callbacksObj, jobject clock,
- jobjectArray measurementArray, jobjectArray gnssAgcArray) {
+ jobjectArray measurementArray, jobjectArray gnssAgcArray,
+ bool hasFullTracking, jboolean isFullTracking) {
jobject gnssMeasurementsEventBuilderObject =
env->NewObject(class_gnssMeasurementsEventBuilder,
method_gnssMeasurementsEventBuilderCtor);
@@ -240,6 +246,11 @@
callObjectMethodIgnoringResult(env, gnssMeasurementsEventBuilderObject,
method_gnssMeasurementsEventBuilderSetGnssAutomaticGainControls,
gnssAgcArray);
+ if (hasFullTracking) {
+ callObjectMethodIgnoringResult(env, gnssMeasurementsEventBuilderObject,
+ method_gnssMeasurementsEventBuilderSetFullTracking,
+ isFullTracking);
+ }
jobject gnssMeasurementsEventObject =
env->CallObjectMethod(gnssMeasurementsEventBuilderObject,
method_gnssMeasurementsEventBuilderBuild);
@@ -381,7 +392,14 @@
jobjectArray gnssAgcArray = nullptr;
gnssAgcArray = translateAllGnssAgcs(env, data.gnssAgcs);
- setMeasurementData(env, mCallbacksObj, clock, measurementArray, gnssAgcArray);
+ if (this->getInterfaceVersion() >= 3) {
+ setMeasurementData(env, mCallbacksObj, clock, measurementArray, gnssAgcArray,
+ /*hasFullTracking=*/true, data.isFullTracking);
+ } else {
+ setMeasurementData(env, mCallbacksObj, clock, measurementArray, gnssAgcArray,
+ /*hasFullTracking=*/false,
+ /*isFullTracking=*/JNI_FALSE);
+ }
env->DeleteLocalRef(clock);
env->DeleteLocalRef(measurementArray);
diff --git a/services/core/jni/gnss/GnssMeasurementCallback.h b/services/core/jni/gnss/GnssMeasurementCallback.h
index c8f1803..fde56881 100644
--- a/services/core/jni/gnss/GnssMeasurementCallback.h
+++ b/services/core/jni/gnss/GnssMeasurementCallback.h
@@ -48,7 +48,8 @@
void GnssMeasurement_class_init_once(JNIEnv* env, jclass& clazz);
void setMeasurementData(JNIEnv* env, jobject& callbacksObj, jobject clock,
- jobjectArray measurementArray, jobjectArray gnssAgcArray);
+ jobjectArray measurementArray, jobjectArray gnssAgcArray,
+ bool hasFullTracking, jboolean isFullTracking);
class GnssMeasurementCallbackAidl : public hardware::gnss::BnGnssMeasurementCallback {
public:
@@ -140,7 +141,9 @@
size_t count = getMeasurementCount(data);
jobjectArray measurementArray =
translateAllGnssMeasurements(env, data.measurements.data(), count);
- setMeasurementData(env, mCallbacksObj, clock, measurementArray, nullptr);
+ setMeasurementData(env, mCallbacksObj, clock, measurementArray, /*gnssAgcArray=*/nullptr,
+ /*hasFullTracking=*/false,
+ /*isFullTracking=*/JNI_FALSE);
env->DeleteLocalRef(clock);
env->DeleteLocalRef(measurementArray);
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 7100020..8c2065e 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -19572,6 +19572,11 @@
}
admin.mtePolicy = flags;
saveSettingsLocked(caller.getUserId());
+
+ DevicePolicyEventLogger.createEvent(DevicePolicyEnums.SET_MTE_POLICY)
+ .setInt(flags)
+ .setAdmin(admin.info.getPackageName())
+ .write();
}
}
}
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index bcb4ec9..5b9460a 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -132,6 +132,7 @@
import com.android.server.display.color.ColorDisplayService;
import com.android.server.dreams.DreamManagerService;
import com.android.server.emergency.EmergencyAffordanceService;
+import com.android.server.grammaticalinflection.GrammaticalInflectionService;
import com.android.server.gpu.GpuService;
import com.android.server.graphics.fonts.FontManagerService;
import com.android.server.hdmi.HdmiControlService;
@@ -425,6 +426,8 @@
"com.android.server.sdksandbox.SdkSandboxManagerService$Lifecycle";
private static final String AD_SERVICES_MANAGER_SERVICE_CLASS =
"com.android.server.adservices.AdServicesManagerService$Lifecycle";
+ private static final String UPDATABLE_DEVICE_CONFIG_SERVICE_CLASS =
+ "com.android.server.deviceconfig.DeviceConfigInit$Lifecycle";
private static final String TETHERING_CONNECTOR_CLASS = "android.net.ITetheringConnector";
@@ -1049,6 +1052,17 @@
private void startBootstrapServices(@NonNull TimingsTraceAndSlog t) {
t.traceBegin("startBootstrapServices");
+ t.traceBegin("ArtModuleServiceInitializer");
+ // This needs to happen before DexUseManagerLocal init. We do it here to avoid colliding
+ // with a GC. ArtModuleServiceInitializer is a class from a separate dex file
+ // "service-art.jar", so referencing it involves the class linker. The class linker and the
+ // GC are mutually exclusive (b/263486535). Therefore, we do this here to force trigger the
+ // class linker earlier. If we did this later, especially after PackageManagerService init,
+ // the class linker would be consistently blocked by a GC because PackageManagerService
+ // allocates a lot of memory and almost certainly triggers a GC.
+ ArtModuleServiceInitializer.setArtModuleServiceManager(new ArtModuleServiceManager());
+ t.traceEnd();
+
// Start the watchdog as early as possible so we can crash the system server
// if we deadlock during early boot
t.traceBegin("StartWatchdog");
@@ -1235,8 +1249,6 @@
t.traceBegin("DexUseManagerLocal");
// 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());
t.traceEnd();
@@ -1512,6 +1524,8 @@
t.traceBegin("InstallSystemProviders");
mActivityManagerService.getContentProviderHelper().installSystemProviders();
+ // Device configuration used to be part of System providers
+ mSystemServiceManager.startService(UPDATABLE_DEVICE_CONFIG_SERVICE_CLASS);
// Now that SettingsProvider is ready, reactivate SQLiteCompatibilityWalFlags
SQLiteCompatibilityWalFlags.reset();
t.traceEnd();
@@ -1765,6 +1779,14 @@
}
t.traceEnd();
+ t.traceBegin("StartGrammarInflectionService");
+ try {
+ mSystemServiceManager.startService(GrammaticalInflectionService.class);
+ } catch (Throwable e) {
+ reportWtf("starting GrammarInflectionService service", e);
+ }
+ t.traceEnd();
+
t.traceBegin("UpdatePackagesIfNeeded");
try {
Watchdog.getInstance().pauseWatchingCurrentThread("dexopt");
diff --git a/services/people/java/com/android/server/people/data/ConversationInfo.java b/services/people/java/com/android/server/people/data/ConversationInfo.java
index 6ead44a..a539fdd 100644
--- a/services/people/java/com/android/server/people/data/ConversationInfo.java
+++ b/services/people/java/com/android/server/people/data/ConversationInfo.java
@@ -424,6 +424,7 @@
case (int) ConversationInfoProto.CREATION_TIMESTAMP:
builder.setCreationTimestamp(protoInputStream.readLong(
ConversationInfoProto.CREATION_TIMESTAMP));
+ break;
case (int) ConversationInfoProto.SHORTCUT_FLAGS:
builder.setShortcutFlags(protoInputStream.readInt(
ConversationInfoProto.SHORTCUT_FLAGS));
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 3b277f8..f549797 100644
--- a/services/permission/java/com/android/server/permission/access/AccessCheckingService.kt
+++ b/services/permission/java/com/android/server/permission/access/AccessCheckingService.kt
@@ -88,6 +88,7 @@
configPermissions, privilegedPermissionAllowlistPackages, permissionAllowlist,
implicitToSourcePermissions
)
+ persistence.initialize()
persistence.read(state)
this.state = state
@@ -99,43 +100,6 @@
permissionService.initialize()
}
- private val PackageManagerInternal.knownPackages: IntMap<Array<String>>
- get() = IntMap<Array<String>>().apply {
- this[KnownPackages.PACKAGE_INSTALLER] = getKnownPackageNames(
- KnownPackages.PACKAGE_INSTALLER, UserHandle.USER_SYSTEM
- )
- this[KnownPackages.PACKAGE_PERMISSION_CONTROLLER] = getKnownPackageNames(
- KnownPackages.PACKAGE_PERMISSION_CONTROLLER, UserHandle.USER_SYSTEM
- )
- this[KnownPackages.PACKAGE_VERIFIER] = getKnownPackageNames(
- KnownPackages.PACKAGE_VERIFIER, UserHandle.USER_SYSTEM
- )
- this[KnownPackages.PACKAGE_SETUP_WIZARD] = getKnownPackageNames(
- KnownPackages.PACKAGE_SETUP_WIZARD, UserHandle.USER_SYSTEM
- )
- this[KnownPackages.PACKAGE_SYSTEM_TEXT_CLASSIFIER] = getKnownPackageNames(
- KnownPackages.PACKAGE_SYSTEM_TEXT_CLASSIFIER, UserHandle.USER_SYSTEM
- )
- this[KnownPackages.PACKAGE_CONFIGURATOR] = getKnownPackageNames(
- KnownPackages.PACKAGE_CONFIGURATOR, UserHandle.USER_SYSTEM
- )
- this[KnownPackages.PACKAGE_INCIDENT_REPORT_APPROVER] = getKnownPackageNames(
- KnownPackages.PACKAGE_INCIDENT_REPORT_APPROVER, UserHandle.USER_SYSTEM
- )
- this[KnownPackages.PACKAGE_APP_PREDICTOR] = getKnownPackageNames(
- KnownPackages.PACKAGE_APP_PREDICTOR, UserHandle.USER_SYSTEM
- )
- this[KnownPackages.PACKAGE_COMPANION] = getKnownPackageNames(
- KnownPackages.PACKAGE_COMPANION, UserHandle.USER_SYSTEM
- )
- this[KnownPackages.PACKAGE_RETAIL_DEMO] = getKnownPackageNames(
- KnownPackages.PACKAGE_RETAIL_DEMO, UserHandle.USER_SYSTEM
- )
- this[KnownPackages.PACKAGE_RECENTS] = getKnownPackageNames(
- KnownPackages.PACKAGE_RECENTS, UserHandle.USER_SYSTEM
- )
- }
-
private val SystemConfig.isLeanback: Boolean
get() = PackageManager.FEATURE_LEANBACK in availableFeatures
@@ -187,10 +151,12 @@
internal fun onStorageVolumeMounted(volumeUuid: String?, isSystemUpdated: Boolean) {
val (packageStates, disabledSystemPackageStates) = packageManagerLocal.allPackageStates
+ val knownPackages = packageManagerInternal.knownPackages
mutateState {
with(policy) {
onStorageVolumeMounted(
- packageStates, disabledSystemPackageStates, volumeUuid, isSystemUpdated
+ packageStates, disabledSystemPackageStates, knownPackages, volumeUuid,
+ isSystemUpdated
)
}
}
@@ -198,35 +164,48 @@
internal fun onPackageAdded(packageName: String) {
val (packageStates, disabledSystemPackageStates) = packageManagerLocal.allPackageStates
+ val knownPackages = packageManagerInternal.knownPackages
mutateState {
- with(policy) { onPackageAdded(packageStates, disabledSystemPackageStates, packageName) }
+ with(policy) {
+ onPackageAdded(
+ packageStates, disabledSystemPackageStates, knownPackages, packageName
+ )
+ }
}
}
internal fun onPackageRemoved(packageName: String, appId: Int) {
val (packageStates, disabledSystemPackageStates) = packageManagerLocal.allPackageStates
+ val knownPackages = packageManagerInternal.knownPackages
mutateState {
with(policy) {
- onPackageRemoved(packageStates, disabledSystemPackageStates, packageName, appId)
+ onPackageRemoved(
+ packageStates, disabledSystemPackageStates, knownPackages, packageName, appId
+ )
}
}
}
internal fun onPackageInstalled(packageName: String, userId: Int) {
val (packageStates, disabledSystemPackageStates) = packageManagerLocal.allPackageStates
+ val knownPackages = packageManagerInternal.knownPackages
mutateState {
with(policy) {
- onPackageInstalled(packageStates, disabledSystemPackageStates, packageName, userId)
+ onPackageInstalled(
+ packageStates, disabledSystemPackageStates, knownPackages, packageName, userId
+ )
}
}
}
internal fun onPackageUninstalled(packageName: String, appId: Int, userId: Int) {
val (packageStates, disabledSystemPackageStates) = packageManagerLocal.allPackageStates
+ val knownPackages = packageManagerInternal.knownPackages
mutateState {
with(policy) {
onPackageUninstalled(
- packageStates, disabledSystemPackageStates, packageName, appId, userId
+ packageStates, disabledSystemPackageStates, knownPackages, packageName, appId,
+ userId
)
}
}
@@ -236,6 +215,43 @@
Pair<Map<String, PackageState>, Map<String, PackageState>>
get() = withUnfilteredSnapshot().use { it.packageStates to it.disabledSystemPackageStates }
+ private val PackageManagerInternal.knownPackages: IntMap<Array<String>>
+ get() = IntMap<Array<String>>().apply {
+ this[KnownPackages.PACKAGE_INSTALLER] = getKnownPackageNames(
+ KnownPackages.PACKAGE_INSTALLER, UserHandle.USER_SYSTEM
+ )
+ this[KnownPackages.PACKAGE_PERMISSION_CONTROLLER] = getKnownPackageNames(
+ KnownPackages.PACKAGE_PERMISSION_CONTROLLER, UserHandle.USER_SYSTEM
+ )
+ this[KnownPackages.PACKAGE_VERIFIER] = getKnownPackageNames(
+ KnownPackages.PACKAGE_VERIFIER, UserHandle.USER_SYSTEM
+ )
+ this[KnownPackages.PACKAGE_SETUP_WIZARD] = getKnownPackageNames(
+ KnownPackages.PACKAGE_SETUP_WIZARD, UserHandle.USER_SYSTEM
+ )
+ this[KnownPackages.PACKAGE_SYSTEM_TEXT_CLASSIFIER] = getKnownPackageNames(
+ KnownPackages.PACKAGE_SYSTEM_TEXT_CLASSIFIER, UserHandle.USER_SYSTEM
+ )
+ this[KnownPackages.PACKAGE_CONFIGURATOR] = getKnownPackageNames(
+ KnownPackages.PACKAGE_CONFIGURATOR, UserHandle.USER_SYSTEM
+ )
+ this[KnownPackages.PACKAGE_INCIDENT_REPORT_APPROVER] = getKnownPackageNames(
+ KnownPackages.PACKAGE_INCIDENT_REPORT_APPROVER, UserHandle.USER_SYSTEM
+ )
+ this[KnownPackages.PACKAGE_APP_PREDICTOR] = getKnownPackageNames(
+ KnownPackages.PACKAGE_APP_PREDICTOR, UserHandle.USER_SYSTEM
+ )
+ this[KnownPackages.PACKAGE_COMPANION] = getKnownPackageNames(
+ KnownPackages.PACKAGE_COMPANION, UserHandle.USER_SYSTEM
+ )
+ this[KnownPackages.PACKAGE_RETAIL_DEMO] = getKnownPackageNames(
+ KnownPackages.PACKAGE_RETAIL_DEMO, UserHandle.USER_SYSTEM
+ )
+ this[KnownPackages.PACKAGE_RECENTS] = getKnownPackageNames(
+ KnownPackages.PACKAGE_RECENTS, UserHandle.USER_SYSTEM
+ )
+ }
+
@OptIn(ExperimentalContracts::class)
internal inline fun <T> getState(action: GetStateScope.() -> T): T {
contract { callsInPlace(action, InvocationKind.EXACTLY_ONCE) }
diff --git a/services/permission/java/com/android/server/permission/access/AccessPersistence.kt b/services/permission/java/com/android/server/permission/access/AccessPersistence.kt
index 91239c6..a25b720 100644
--- a/services/permission/java/com/android/server/permission/access/AccessPersistence.kt
+++ b/services/permission/java/com/android/server/permission/access/AccessPersistence.kt
@@ -16,8 +16,15 @@
package com.android.server.permission.access
+import android.os.Handler
+import android.os.Looper
+import android.os.Message
+import android.os.SystemClock
+import android.os.UserHandle
import android.util.AtomicFile
import android.util.Log
+import com.android.internal.annotations.GuardedBy
+import com.android.internal.os.BackgroundThread
import com.android.modules.utils.BinaryXmlPullParser
import com.android.modules.utils.BinaryXmlSerializer
import com.android.server.permission.access.collection.* // ktlint-disable no-wildcard-imports
@@ -32,6 +39,20 @@
class AccessPersistence(
private val policy: AccessPolicy
) {
+ private val scheduleLock = Any()
+ @GuardedBy("scheduleLock")
+ private val pendingMutationTimesMillis = IntLongMap()
+ @GuardedBy("scheduleLock")
+ private val pendingStates = IntMap<AccessState>()
+ @GuardedBy("scheduleLock")
+ private lateinit var writeHandler: WriteHandler
+
+ private val writeLock = Any()
+
+ fun initialize() {
+ writeHandler = WriteHandler(BackgroundThread.getHandler().looper)
+ }
+
fun read(state: AccessState) {
readSystemState(state)
state.systemState.userIds.forEachIndexed { _, userId ->
@@ -64,21 +85,61 @@
}
fun write(state: AccessState) {
- writeState(state.systemState) { writeSystemState(state) }
+ state.systemState.write(state, UserHandle.USER_ALL)
state.userStates.forEachIndexed { _, userId, userState ->
- writeState(userState) { writeUserState(state, userId) }
+ userState.write(state, userId)
}
}
- private inline fun <T : WritableState> writeState(state: T, write: () -> Unit) {
- when (val writeMode = state.writeMode) {
+ private fun WritableState.write(state: AccessState, userId: Int) {
+ when (val writeMode = writeMode) {
WriteMode.NONE -> {}
- WriteMode.SYNC -> write()
- WriteMode.ASYNC -> TODO()
+ WriteMode.SYNC -> {
+ synchronized(scheduleLock) { pendingStates[userId] = state }
+ writePendingState(userId)
+ }
+ WriteMode.ASYNC -> {
+ synchronized(scheduleLock) {
+ writeHandler.removeMessages(userId)
+ pendingStates[userId] = state
+ // SystemClock.uptimeMillis() is used in Handler.sendMessageDelayed().
+ val currentTimeMillis = SystemClock.uptimeMillis()
+ val pendingMutationTimeMillis =
+ pendingMutationTimesMillis.getOrPut(userId) { currentTimeMillis }
+ val currentDelayMillis = currentTimeMillis - pendingMutationTimeMillis
+ val message = writeHandler.obtainMessage(userId)
+ if (currentDelayMillis > MAX_WRITE_DELAY_MILLIS) {
+ message.sendToTarget()
+ } else {
+ val newDelayMillis = WRITE_DELAY_TIME_MILLIS
+ .coerceAtMost(MAX_WRITE_DELAY_MILLIS - currentDelayMillis)
+ writeHandler.sendMessageDelayed(message, newDelayMillis)
+ }
+ }
+ }
else -> error(writeMode)
}
}
+ private fun writePendingState(userId: Int) {
+ synchronized(writeLock) {
+ val state: AccessState?
+ synchronized(scheduleLock) {
+ pendingMutationTimesMillis -= userId
+ state = pendingStates.removeReturnOld(userId)
+ writeHandler.removeMessages(userId)
+ }
+ if (state == null) {
+ return
+ }
+ if (userId == UserHandle.USER_ALL) {
+ writeSystemState(state)
+ } else {
+ writeUserState(state, userId)
+ }
+ }
+ }
+
private fun writeSystemState(state: AccessState) {
systemFile.serialize {
with(policy) { serializeSystemState(state) }
@@ -109,5 +170,25 @@
private val LOG_TAG = AccessPersistence::class.java.simpleName
private const val FILE_NAME = "access.abx"
+
+ private const val WRITE_DELAY_TIME_MILLIS = 1000L
+ private const val MAX_WRITE_DELAY_MILLIS = 2000L
+ }
+
+ private inner class WriteHandler(looper: Looper) : Handler(looper) {
+ fun writeAtTime(userId: Int, timeMillis: Long) {
+ removeMessages(userId)
+ val message = obtainMessage(userId)
+ sendMessageDelayed(message, timeMillis)
+ }
+
+ fun cancelWrite(userId: Int) {
+ removeMessages(userId)
+ }
+
+ override fun handleMessage(message: Message) {
+ val userId = message.what
+ writePendingState(userId)
+ }
}
}
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 2d83bfd..e0f94c7 100644
--- a/services/permission/java/com/android/server/permission/access/AccessPolicy.kt
+++ b/services/permission/java/com/android/server/permission/access/AccessPolicy.kt
@@ -82,6 +82,11 @@
this.permissionAllowlist = permissionAllowlist
this.implicitToSourcePermissions = implicitToSourcePermissions
}
+ state.userStates.apply {
+ userIds.forEachIndexed { _, userId ->
+ this[userId] = UserState()
+ }
+ }
}
fun GetStateScope.onStateMutated() {
@@ -115,12 +120,29 @@
fun MutateStateScope.onStorageVolumeMounted(
packageStates: Map<String, PackageState>,
disabledSystemPackageStates: Map<String, PackageState>,
+ knownPackages: IntMap<Array<String>>,
volumeUuid: String?,
isSystemUpdated: Boolean
) {
+ val addedAppIds = IntSet()
newState.systemState.apply {
this.packageStates = packageStates
this.disabledSystemPackageStates = disabledSystemPackageStates
+ packageStates.forEach { (packageName, packageState) ->
+ if (packageState.volumeUuid == volumeUuid) {
+ val appId = packageState.appId
+ appIds.getOrPut(appId) {
+ addedAppIds += appId
+ IndexedListSet()
+ } += packageName
+ }
+ }
+ this.knownPackages = knownPackages
+ }
+ addedAppIds.forEachIndexed { _, appId ->
+ forEachSchemePolicy {
+ with(it) { onAppIdAdded(appId) }
+ }
}
forEachSchemePolicy {
with(it) { onStorageVolumeMounted(volumeUuid, isSystemUpdated) }
@@ -130,6 +152,7 @@
fun MutateStateScope.onPackageAdded(
packageStates: Map<String, PackageState>,
disabledSystemPackageStates: Map<String, PackageState>,
+ knownPackages: IntMap<Array<String>>,
packageName: String
) {
val packageState = packageStates[packageName]
@@ -145,7 +168,8 @@
appIds.getOrPut(appId) {
isAppIdAdded = true
IndexedListSet()
- }.add(packageName)
+ } += packageName
+ this.knownPackages = knownPackages
}
if (isAppIdAdded) {
forEachSchemePolicy {
@@ -160,6 +184,7 @@
fun MutateStateScope.onPackageRemoved(
packageStates: Map<String, PackageState>,
disabledSystemPackageStates: Map<String, PackageState>,
+ knownPackages: IntMap<Array<String>>,
packageName: String,
appId: Int
) {
@@ -178,6 +203,7 @@
isAppIdRemoved = true
}
}
+ this.knownPackages = knownPackages
}
forEachSchemePolicy {
with(it) { onPackageRemoved(packageName, appId) }
@@ -192,12 +218,14 @@
fun MutateStateScope.onPackageInstalled(
packageStates: Map<String, PackageState>,
disabledSystemPackageStates: Map<String, PackageState>,
+ knownPackages: IntMap<Array<String>>,
packageName: String,
userId: Int
) {
newState.systemState.apply {
this.packageStates = packageStates
this.disabledSystemPackageStates = disabledSystemPackageStates
+ this.knownPackages = knownPackages
}
val packageState = packageStates[packageName]
// TODO(zhanghai): STOPSHIP: Remove check before feature enable.
@@ -212,6 +240,7 @@
fun MutateStateScope.onPackageUninstalled(
packageStates: Map<String, PackageState>,
disabledSystemPackageStates: Map<String, PackageState>,
+ knownPackages: IntMap<Array<String>>,
packageName: String,
appId: Int,
userId: Int
@@ -219,6 +248,7 @@
newState.systemState.apply {
this.packageStates = packageStates
this.disabledSystemPackageStates = disabledSystemPackageStates
+ this.knownPackages = knownPackages
}
forEachSchemePolicy {
with(it) { onPackageUninstalled(packageName, appId, userId) }
diff --git a/services/permission/java/com/android/server/permission/access/collection/IntLongMap.kt b/services/permission/java/com/android/server/permission/access/collection/IntLongMap.kt
new file mode 100644
index 0000000..692bbd6
--- /dev/null
+++ b/services/permission/java/com/android/server/permission/access/collection/IntLongMap.kt
@@ -0,0 +1,171 @@
+/*
+ * 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.permission.access.collection
+
+import android.util.SparseLongArray
+
+typealias IntLongMap = SparseLongArray
+
+inline fun IntLongMap.allIndexed(predicate: (Int, Int, Long) -> Boolean): Boolean {
+ forEachIndexed { index, key, value ->
+ if (!predicate(index, key, value)) {
+ return false
+ }
+ }
+ return true
+}
+
+inline fun IntLongMap.anyIndexed(predicate: (Int, Int, Long) -> Boolean): Boolean {
+ forEachIndexed { index, key, value ->
+ if (predicate(index, key, value)) {
+ return true
+ }
+ }
+ return false
+}
+
+@Suppress("NOTHING_TO_INLINE")
+inline fun IntLongMap.copy(): IntLongMap = clone()
+
+inline fun <R> IntLongMap.firstNotNullOfOrNullIndexed(transform: (Int, Int, Long) -> R): R? {
+ forEachIndexed { index, key, value ->
+ transform(index, key, value)?.let { return it }
+ }
+ return null
+}
+
+inline fun IntLongMap.forEachIndexed(action: (Int, Int, Long) -> Unit) {
+ for (index in 0 until size) {
+ action(index, keyAt(index), valueAt(index))
+ }
+}
+
+inline fun IntLongMap.forEachKeyIndexed(action: (Int, Int) -> Unit) {
+ for (index in 0 until size) {
+ action(index, keyAt(index))
+ }
+}
+
+inline fun IntLongMap.forEachReversedIndexed(action: (Int, Int, Long) -> Unit) {
+ for (index in lastIndex downTo 0) {
+ action(index, keyAt(index), valueAt(index))
+ }
+}
+
+inline fun IntLongMap.forEachValueIndexed(action: (Int, Long) -> Unit) {
+ for (index in 0 until size) {
+ action(index, valueAt(index))
+ }
+}
+
+inline fun IntLongMap.getOrPut(key: Int, defaultValue: () -> Long): Long {
+ val index = indexOfKey(key)
+ return if (index >= 0) {
+ valueAt(index)
+ } else {
+ defaultValue().also { put(key, it) }
+ }
+}
+
+@Suppress("NOTHING_TO_INLINE")
+inline fun IntLongMap?.getWithDefault(key: Int, defaultValue: Long): Long {
+ this ?: return defaultValue
+ return get(key, defaultValue)
+}
+
+inline val IntLongMap.lastIndex: Int
+ get() = size - 1
+
+@Suppress("NOTHING_TO_INLINE")
+inline operator fun IntLongMap.minusAssign(key: Int) {
+ delete(key)
+}
+
+inline fun IntLongMap.noneIndexed(predicate: (Int, Int, Long) -> Boolean): Boolean {
+ forEachIndexed { index, key, value ->
+ if (predicate(index, key, value)) {
+ return false
+ }
+ }
+ return true
+}
+
+@Suppress("NOTHING_TO_INLINE")
+inline fun IntLongMap.putWithDefault(key: Int, value: Long, defaultValue: Long): Long {
+ val index = indexOfKey(key)
+ if (index >= 0) {
+ val oldValue = valueAt(index)
+ if (value != oldValue) {
+ if (value == defaultValue) {
+ removeAt(index)
+ } else {
+ setValueAt(index, value)
+ }
+ }
+ return oldValue
+ } else {
+ if (value != defaultValue) {
+ put(key, value)
+ }
+ return defaultValue
+ }
+}
+
+fun IntLongMap.remove(key: Int) {
+ delete(key)
+}
+
+fun IntLongMap.remove(key: Int, defaultValue: Long): Long {
+ val index = indexOfKey(key)
+ return if (index >= 0) {
+ val oldValue = valueAt(index)
+ removeAt(index)
+ oldValue
+ } else {
+ defaultValue
+ }
+}
+
+inline fun IntLongMap.removeAllIndexed(predicate: (Int, Int, Long) -> Boolean): Boolean {
+ var isChanged = false
+ forEachReversedIndexed { index, key, value ->
+ if (predicate(index, key, value)) {
+ removeAt(index)
+ isChanged = true
+ }
+ }
+ return isChanged
+}
+
+inline fun IntLongMap.retainAllIndexed(predicate: (Int, Int, Long) -> Boolean): Boolean {
+ var isChanged = false
+ forEachReversedIndexed { index, key, value ->
+ if (!predicate(index, key, value)) {
+ removeAt(index)
+ isChanged = true
+ }
+ }
+ return isChanged
+}
+
+@Suppress("NOTHING_TO_INLINE")
+inline operator fun IntLongMap.set(key: Int, value: Long) {
+ put(key, value)
+}
+
+inline val IntLongMap.size: Int
+ get() = size()
diff --git a/services/permission/java/com/android/server/permission/access/permission/Permission.kt b/services/permission/java/com/android/server/permission/access/permission/Permission.kt
index 35f00a7..7bfca12 100644
--- a/services/permission/java/com/android/server/permission/access/permission/Permission.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/Permission.kt
@@ -157,7 +157,7 @@
if (areGidsPerUser) {
IntArray(gids.size) { i -> UserHandle.getUid(userId, gids[i]) }
} else {
- gids.clone()
+ gids.copyOf()
}
companion object {
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 e2c2c49..dd36c38 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
@@ -137,6 +137,7 @@
userManagerService = UserManagerService.getInstance()
handlerThread = ServiceThread(LOG_TAG, Process.THREAD_PRIORITY_BACKGROUND, true)
+ .apply { start() }
handler = Handler(handlerThread.looper)
onPermissionsChangeListeners = OnPermissionsChangeListeners(FgThread.get().looper)
onPermissionFlagsChangedListener = OnPermissionFlagsChangedListener()
@@ -578,7 +579,7 @@
// more consistent with the pre-S-refactor behavior. This is also because we are now
// actively trimming the per-UID objects when empty.
val permissionFlags = with(policy) { getUidPermissionFlags(appId, userId) }
- ?: return globalGids.clone()
+ ?: return globalGids.copyOf()
val gids = GrowingIntArray.wrap(globalGids)
permissionFlags.forEachIndexed { _, permissionName, flags ->
@@ -654,6 +655,11 @@
)
}
+ if (!userManagerInternal.exists(userId)) {
+ Log.w(LOG_TAG, "$methodName: Unknown user $userId")
+ return
+ }
+
enforceCallingOrSelfCrossUserPermission(
userId, enforceFullPermission = true, enforceShellRestriction = true, methodName
)
@@ -664,11 +670,6 @@
}
context.enforceCallingOrSelfPermission(enforcedPermissionName, methodName)
- if (!userManagerInternal.exists(userId)) {
- Log.w(LOG_TAG, "$methodName: Unknown user $userId")
- return
- }
-
val packageState: PackageState?
val permissionControllerPackageName = packageManagerInternal.getKnownPackageNames(
KnownPackages.PACKAGE_PERMISSION_CONTROLLER, UserHandle.USER_SYSTEM
@@ -870,6 +871,11 @@
}
override fun getPermissionFlags(packageName: String, permissionName: String, userId: Int): Int {
+ if (!userManagerInternal.exists(userId)) {
+ Log.w(LOG_TAG, "getPermissionFlags: Unknown user $userId")
+ return 0
+ }
+
enforceCallingOrSelfCrossUserPermission(
userId, enforceFullPermission = true, enforceShellRestriction = false,
"getPermissionFlags"
@@ -880,11 +886,6 @@
Manifest.permission.GET_RUNTIME_PERMISSIONS
)
- if (!userManagerInternal.exists(userId)) {
- Log.w(LOG_TAG, "getPermissionFlags: Unknown user $userId")
- return 0
- }
-
val packageState = packageManagerLocal.withFilteredSnapshot()
.use { it.getPackageState(packageName) }
if (packageState == null) {
@@ -910,16 +911,16 @@
permissionName: String,
userId: Int
): Boolean {
- enforceCallingOrSelfCrossUserPermission(
- userId, enforceFullPermission = true, enforceShellRestriction = false,
- "isPermissionRevokedByPolicy"
- )
-
if (!userManagerInternal.exists(userId)) {
Log.w(LOG_TAG, "isPermissionRevokedByPolicy: Unknown user $userId")
return false
}
+ enforceCallingOrSelfCrossUserPermission(
+ userId, enforceFullPermission = true, enforceShellRestriction = false,
+ "isPermissionRevokedByPolicy"
+ )
+
val packageState = packageManagerLocal.withFilteredSnapshot(Binder.getCallingUid(), userId)
.use { it.getPackageState(packageName) } ?: return false
@@ -954,16 +955,16 @@
permissionName: String,
userId: Int
): Boolean {
- enforceCallingOrSelfCrossUserPermission(
- userId, enforceFullPermission = true, enforceShellRestriction = false,
- "shouldShowRequestPermissionRationale"
- )
-
if (!userManagerInternal.exists(userId)) {
Log.w(LOG_TAG, "shouldShowRequestPermissionRationale: Unknown user $userId")
return false
}
+ enforceCallingOrSelfCrossUserPermission(
+ userId, enforceFullPermission = true, enforceShellRestriction = false,
+ "shouldShowRequestPermissionRationale"
+ )
+
val callingUid = Binder.getCallingUid()
val packageState = packageManagerLocal.withFilteredSnapshot(callingUid, userId)
.use { it.getPackageState(packageName) } ?: return false
@@ -1030,6 +1031,11 @@
)
}
+ if (!userManagerInternal.exists(userId)) {
+ Log.w(LOG_TAG, "updatePermissionFlags: Unknown user $userId")
+ return
+ }
+
enforceCallingOrSelfCrossUserPermission(
userId, enforceFullPermission = true, enforceShellRestriction = true,
"updatePermissionFlags"
@@ -1062,11 +1068,6 @@
}
}
- if (!userManagerInternal.exists(userId)) {
- Log.w(LOG_TAG, "updatePermissionFlags: Unknown user $userId")
- return
- }
-
// Using PackageManagerInternal instead of PackageManagerLocal for now due to need to access
// shared user packages.
// TODO: We probably shouldn't check the share user packages, since the package name is
@@ -1124,6 +1125,11 @@
)
}
+ if (!userManagerInternal.exists(userId)) {
+ Log.w(LOG_TAG, "updatePermissionFlagsForAllApps: Unknown user $userId")
+ return
+ }
+
enforceCallingOrSelfCrossUserPermission(
userId, enforceFullPermission = true, enforceShellRestriction = true,
"updatePermissionFlagsForAllApps"
@@ -1133,11 +1139,6 @@
Manifest.permission.REVOKE_RUNTIME_PERMISSIONS
)
- if (!userManagerInternal.exists(userId)) {
- Log.w(LOG_TAG, "updatePermissionFlagsForAllApps: Unknown user $userId")
- return
- }
-
val packageStates = packageManagerLocal.withUnfilteredSnapshot()
.use { it.packageStates }
service.mutateState {
@@ -1229,16 +1230,16 @@
Preconditions.checkFlagsArgument(allowlistedFlags, PERMISSION_ALLOWLIST_MASK)
Preconditions.checkArgumentNonnegative(userId, "userId cannot be null")
- enforceCallingOrSelfCrossUserPermission(
- userId, enforceFullPermission = false, enforceShellRestriction = false,
- "getAllowlistedRestrictedPermissions"
- )
-
if (!userManagerInternal.exists(userId)) {
Log.w(LOG_TAG, "AllowlistedRestrictedPermission api: Unknown user $userId")
return null
}
+ enforceCallingOrSelfCrossUserPermission(
+ userId, enforceFullPermission = false, enforceShellRestriction = false,
+ "getAllowlistedRestrictedPermissions"
+ )
+
val callingUid = Binder.getCallingUid()
val packageState = packageManagerLocal.withFilteredSnapshot(callingUid, userId)
.use { it.getPackageState(packageName) } ?: return null
@@ -1517,11 +1518,11 @@
}
override fun resetRuntimePermissions(androidPackage: AndroidPackage, userId: Int) {
- TODO("Not yet implemented")
+ // TODO("Not yet implemented")
}
override fun resetRuntimePermissionsForUser(userId: Int) {
- TODO("Not yet implemented")
+ // TODO("Not yet implemented")
}
override fun addOnPermissionsChangeListener(listener: IOnPermissionsChangeListener) {
@@ -1657,33 +1658,36 @@
override fun getPermissionTEMP(
permissionName: String
): com.android.server.pm.permission.Permission? {
- TODO("Not yet implemented")
+ // TODO("Not yet implemented")
+ return null
}
override fun getLegacyPermissions(): List<LegacyPermission> {
- TODO("Not yet implemented")
+ // TODO("Not yet implemented")
+ return emptyList()
}
override fun readLegacyPermissionsTEMP(legacyPermissionSettings: LegacyPermissionSettings) {
// Package settings has been read when this method is called.
service.initialize()
- TODO("Not yet implemented")
+ // TODO("Not yet implemented")
}
override fun writeLegacyPermissionsTEMP(legacyPermissionSettings: LegacyPermissionSettings) {
- TODO("Not yet implemented")
+ // TODO("Not yet implemented")
}
override fun getLegacyPermissionState(appId: Int): LegacyPermissionState {
- TODO("Not yet implemented")
+ // TODO("Not yet implemented")
+ return LegacyPermissionState()
}
override fun readLegacyPermissionStateTEMP() {
- TODO("Not yet implemented")
+ // TODO("Not yet implemented")
}
override fun writeLegacyPermissionStateTEMP() {
- TODO("Not yet implemented")
+ // TODO("Not yet implemented")
}
override fun onSystemReady() {
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 4c3ffde..73fc0b2 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
@@ -271,7 +271,12 @@
this.packageName = packageName
protectionLevel = oldPermission.permissionInfo.protectionLevel
}
- val newPermission = Permission(newPermissionInfo, false, oldPermission.type, 0)
+ // Different from the old implementation, which removes the GIDs upon permission
+ // adoption, but adds them back on the next boot, we now just consistently keep the
+ // GIDs.
+ val newPermission = oldPermission.copy(
+ permissionInfo = newPermissionInfo, isReconciled = false, appId = 0
+ )
permissions.setValueAt(permissionIndex, newPermission)
systemState.requestWrite()
changedPermissionNames += permissionName
@@ -385,7 +390,10 @@
}
if (oldPermission.type == Permission.TYPE_CONFIG && !oldPermission.isReconciled) {
// It's a config permission and has no owner, take ownership now.
- Permission(newPermissionInfo, true, Permission.TYPE_CONFIG, packageState.appId)
+ oldPermission.copy(
+ permissionInfo = newPermissionInfo, isReconciled = true,
+ appId = packageState.appId
+ )
} else if (systemState.packageStates[oldPackageName]?.isSystem != true) {
Log.w(
LOG_TAG, "Overriding permission $permissionName with new declaration in" +
@@ -398,8 +406,12 @@
setPermissionFlags(appId, userId, permissionName, 0)
}
}
+ // Different from the old implementation, which removes the GIDs upon permission
+ // override, but adds them back on the next boot, we now just consistently keep
+ // the GIDs.
Permission(
- newPermissionInfo, true, Permission.TYPE_MANIFEST, packageState.appId
+ newPermissionInfo, true, Permission.TYPE_MANIFEST, packageState.appId,
+ oldPermission.gids, oldPermission.areGidsPerUser
)
} else {
Log.w(
@@ -413,7 +425,17 @@
// Different from the old implementation, which doesn't update the permission
// definition upon app update, but does update it on the next boot, we now
// consistently update the permission definition upon app update.
- Permission(newPermissionInfo, true, Permission.TYPE_MANIFEST, packageState.appId)
+ @Suppress("IfThenToElvis")
+ if (oldPermission != null) {
+ oldPermission.copy(
+ permissionInfo = newPermissionInfo, isReconciled = true,
+ appId = packageState.appId
+ )
+ } else {
+ Permission(
+ newPermissionInfo, true, Permission.TYPE_MANIFEST, packageState.appId
+ )
+ }
}
if (parsedPermission.isTree) {
@@ -498,7 +520,7 @@
// TODO: STOPSHIP: Retain permissions requested by disabled system packages.
}
newState.userStates.forEachIndexed { _, userId, userState ->
- userState.uidPermissionFlags[appId].forEachReversedIndexed { _, permissionName, _ ->
+ userState.uidPermissionFlags[appId]?.forEachReversedIndexed { _, permissionName, _ ->
if (permissionName !in requestedPermissions) {
setPermissionFlags(appId, userId, permissionName, 0)
}
@@ -852,10 +874,12 @@
permissionName: String
): 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.
+ // TODO(b/261913353): STOPSHIP: Add AndroidPackage.apexModuleName.
// val apexModuleName = androidPackage.apexModuleName
- val apexModuleName = packageState.packageName
+ val apexModuleName = permissionAllowlist.apexPrivilegedAppAllowlists
+ .firstNotNullOfOrNullIndexed { _, apexModuleName, apexAllowlist ->
+ if (packageState.packageName in apexAllowlist) apexModuleName else null
+ }
val packageName = packageState.packageName
return when {
packageState.isVendor -> permissionAllowlist.getVendorPrivilegedAppAllowlistState(
@@ -901,7 +925,7 @@
return targetSdkVersion
}
- private fun MutateStateScope.anyPackageInAppId(
+ private inline fun MutateStateScope.anyPackageInAppId(
appId: Int,
state: AccessState = newState,
predicate: (PackageState) -> Boolean
@@ -913,7 +937,7 @@
}
}
- private fun MutateStateScope.forEachPackageInAppId(
+ private inline fun MutateStateScope.forEachPackageInAppId(
appId: Int,
state: AccessState = newState,
action: (PackageState) -> Unit
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
index 3727d66..b20e1dd 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
@@ -92,8 +92,11 @@
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.security.PublicKey;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.PriorityQueue;
@@ -141,8 +144,7 @@
/** make sure our initialized KeySetManagerService metadata matches packages.xml */
@Test
- public void testReadKeySetSettings()
- throws ReflectiveOperationException, IllegalAccessException {
+ public void testReadKeySetSettings() throws Exception {
/* write out files and read */
writeOldFiles();
Settings settings = makeSettings();
@@ -150,6 +152,29 @@
verifyKeySetMetaData(settings);
}
+ // Same as above but use the reserve copy.
+ @Test
+ public void testReadReserveCopyKeySetSettings() throws Exception {
+ /* write out files and read */
+ writeReserveCopyOldFiles();
+ Settings settings = makeSettings();
+ assertThat(settings.readLPw(computer, createFakeUsers()), is(true));
+ verifyKeySetMetaData(settings);
+ }
+
+ // Same as above but packages.xml is malformed.
+ @Test
+ public void testReadMalformedPackagesXmlKeySetSettings() throws Exception {
+ // write out files
+ writeReserveCopyOldFiles();
+ // write corrupted packages.xml
+ writeCorruptedPackagesXml();
+
+ Settings settings = makeSettings();
+ assertThat(settings.readLPw(computer, createFakeUsers()), is(true));
+ verifyKeySetMetaData(settings);
+ }
+
/** read in data, write it out, and read it back in. Verify same. */
@Test
public void testWriteKeySetSettings()
@@ -165,6 +190,39 @@
verifyKeySetMetaData(settings);
}
+ // Same as above, but corrupt the primary.xml in process.
+ @Test
+ public void testWriteCorruptReadKeySetSettings() throws Exception {
+ // write out files and read
+ writeOldFiles();
+ Settings settings = makeSettings();
+ assertThat(settings.readLPw(computer, createFakeUsers()), is(true));
+
+ // write out
+ settings.writeLPr(computer, /*sync=*/true);
+
+ File filesDir = InstrumentationRegistry.getContext().getFilesDir();
+ File packageXml = new File(filesDir, "system/packages.xml");
+ File packagesReserveCopyXml = new File(filesDir, "system/packages.xml.reservecopy");
+ // Primary.
+ assertTrue(packageXml.exists());
+ // Reserve copy.
+ assertTrue(packagesReserveCopyXml.exists());
+ // Temporary backup.
+ assertFalse(new File(filesDir, "packages-backup.xml").exists());
+
+ // compare two copies, make sure they are the same
+ assertTrue(Arrays.equals(Files.readAllBytes(Path.of(packageXml.getAbsolutePath())),
+ Files.readAllBytes(Path.of(packagesReserveCopyXml.getAbsolutePath()))));
+
+ // write corrupted packages.xml
+ writeCorruptedPackagesXml();
+
+ // read back in and verify the same
+ assertThat(settings.readLPw(computer, createFakeUsers()), is(true));
+ verifyKeySetMetaData(settings);
+ }
+
@Test
public void testSettingsReadOld() {
// Write delegateshellthe package files and make sure they're parsed properly the first time
@@ -1572,9 +1630,19 @@
}
}
- private void writePackagesXml() {
+ private void writeCorruptedPackagesXml() {
writeFile(new File(InstrumentationRegistry.getContext().getFilesDir(), "system/packages.xml"),
("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>"
+ + "<packages>"
+ + "<last-platform-version internal=\"15\" external=\"0\" />"
+ + "<permission-trees>"
+ + "<item name=\"com.google.android.permtree\""
+ ).getBytes());
+ }
+
+ private void writePackagesXml(String fileName) {
+ writeFile(new File(InstrumentationRegistry.getContext().getFilesDir(), fileName),
+ ("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>"
+ "<packages>"
+ "<last-platform-version internal=\"15\" external=\"0\" fingerprint=\"foo\" />"
+ "<permission-trees>"
@@ -1715,7 +1783,14 @@
private void writeOldFiles() {
deleteSystemFolder();
- writePackagesXml();
+ writePackagesXml("system/packages.xml");
+ writeStoppedPackagesXml();
+ writePackagesList();
+ }
+
+ private void writeReserveCopyOldFiles() {
+ deleteSystemFolder();
+ writePackagesXml("system/packages.xml.reservecopy");
writeStoppedPackagesXml();
writePackagesList();
}
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java
index c8e2676..c6b7736 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java
@@ -753,7 +753,7 @@
.setPVersionCode(pkg.getLongVersionCode())
.setPkgFlags(PackageInfoUtils.appInfoFlags(pkg, null))
.setPrivateFlags(PackageInfoUtils.appInfoPrivateFlags(pkg, null))
- .setSharedUserId(pkg.getSharedUserLabel())
+ .setSharedUserId(pkg.getSharedUserLabelRes())
.build();
}
@@ -761,9 +761,9 @@
public static void assertPackagesEqual(AndroidPackage a, AndroidPackage b) {
assertEquals(a.getBaseRevisionCode(), b.getBaseRevisionCode());
- assertEquals(a.isBaseHardwareAccelerated(), b.isBaseHardwareAccelerated());
+ assertEquals(a.isHardwareAccelerated(), b.isHardwareAccelerated());
assertEquals(a.getLongVersionCode(), b.getLongVersionCode());
- assertEquals(a.getSharedUserLabel(), b.getSharedUserLabel());
+ assertEquals(a.getSharedUserLabelRes(), b.getSharedUserLabelRes());
assertEquals(a.getInstallLocation(), b.getInstallLocation());
assertEquals(a.isCoreApp(), b.isCoreApp());
assertEquals(a.isRequiredForAllUsers(), b.isRequiredForAllUsers());
@@ -1036,8 +1036,8 @@
permission.setParsedPermissionGroup(new ParsedPermissionGroupImpl());
((ParsedPackage) pkg.setBaseRevisionCode(100)
- .setBaseHardwareAccelerated(true)
- .setSharedUserLabel(100)
+ .setHardwareAccelerated(true)
+ .setSharedUserLabelRes(100)
.setInstallLocation(100)
.setRequiredForAllUsers(true)
.asSplit(
@@ -1062,7 +1062,7 @@
.setSdkLibVersionMajor(42)
.addUsesSdkLibrary("sdk23", 200, new String[]{"digest2"})
.setStaticSharedLibraryName("foo23")
- .setStaticSharedLibVersion(100)
+ .setStaticSharedLibraryVersion(100)
.addUsesStaticLibrary("foo23", 100, new String[]{"digest"})
.addLibraryName("foo10")
.addUsesLibrary("foo11")
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/ScanTests.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/ScanTests.java
index b6f1b87..b5bd869 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/ScanTests.java
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/ScanTests.java
@@ -273,7 +273,7 @@
public void installStaticSharedLibrary() throws Exception {
final ParsedPackage pkg = ((ParsedPackage) createBasicPackage("static.lib.pkg")
.setStaticSharedLibraryName("static.lib")
- .setStaticSharedLibVersion(123L)
+ .setStaticSharedLibraryVersion(123L)
.hideAsParsed())
.setPackageName("static.lib.pkg.123")
.setVersionCodeMajor(1)
diff --git a/services/tests/PackageManagerServiceTests/unit/Android.bp b/services/tests/PackageManagerServiceTests/unit/Android.bp
index 1c6ba33..9b3b8c35 100644
--- a/services/tests/PackageManagerServiceTests/unit/Android.bp
+++ b/services/tests/PackageManagerServiceTests/unit/Android.bp
@@ -33,11 +33,16 @@
"junit",
"kotlin-test",
"kotlin-reflect",
+ "mockito-target-extended-minus-junit4",
"services.core",
"servicestests-utils",
"servicestests-core-utils",
"truth-prebuilt",
],
+ jni_libs: [
+ "libdexmakerjvmtiagent",
+ "libstaticjvmtiagent",
+ ],
platform_apis: true,
test_suites: ["device-tests"],
}
diff --git a/services/tests/PackageManagerServiceTests/unit/AndroidManifest.xml b/services/tests/PackageManagerServiceTests/unit/AndroidManifest.xml
index 2ef7a1f..81f6c82 100644
--- a/services/tests/PackageManagerServiceTests/unit/AndroidManifest.xml
+++ b/services/tests/PackageManagerServiceTests/unit/AndroidManifest.xml
@@ -18,6 +18,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.server.pm.test">
+ <!--required for using Mockito-->
+ <application android:debuggable="true" />
+
<instrumentation
android:name="androidx.test.runner.AndroidJUnitRunner"
android:targetPackage="com.android.server.pm.test"
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt
index c439639..1619856 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt
@@ -147,27 +147,27 @@
)
override val baseParams = listOf(
+ AndroidPackage::getApplicationClassName,
AndroidPackage::getAppComponentFactory,
AndroidPackage::getAutoRevokePermissions,
AndroidPackage::getBackupAgentName,
- AndroidPackage::getBanner,
+ AndroidPackage::getBannerRes,
AndroidPackage::getBaseApkPath,
AndroidPackage::getBaseRevisionCode,
AndroidPackage::getCategory,
AndroidPackage::getClassLoaderName,
- AndroidPackage::getClassName,
AndroidPackage::getCompatibleWidthLimitDp,
AndroidPackage::getCompileSdkVersion,
AndroidPackage::getCompileSdkVersionCodeName,
- AndroidPackage::getDataExtractionRules,
+ AndroidPackage::getDataExtractionRulesRes,
AndroidPackage::getDescriptionRes,
- AndroidPackage::getFullBackupContent,
+ AndroidPackage::getFullBackupContentRes,
AndroidPackage::getGwpAsanMode,
AndroidPackage::getIconRes,
AndroidPackage::getInstallLocation,
AndroidPackage::getLabelRes,
AndroidPackage::getLargestWidthLimitDp,
- AndroidPackage::getLogo,
+ AndroidPackage::getLogoRes,
AndroidPackage::getLocaleConfigRes,
AndroidPackage::getManageSpaceActivityName,
AndroidPackage::getMaxSdkVersion,
@@ -195,15 +195,15 @@
PackageImpl::getSecondaryCpuAbi,
AndroidPackage::getSecondaryNativeLibraryDir,
AndroidPackage::getSharedUserId,
- AndroidPackage::getSharedUserLabel,
+ AndroidPackage::getSharedUserLabelRes,
AndroidPackage::getSdkLibraryName,
AndroidPackage::getSdkLibVersionMajor,
AndroidPackage::getStaticSharedLibraryName,
- AndroidPackage::getStaticSharedLibVersion,
+ AndroidPackage::getStaticSharedLibraryVersion,
AndroidPackage::getTargetSandboxVersion,
AndroidPackage::getTargetSdkVersion,
AndroidPackage::getTaskAffinity,
- AndroidPackage::getTheme,
+ AndroidPackage::getThemeRes,
AndroidPackage::getUiOptions,
AndroidPackage::getUid,
AndroidPackage::getVersionName,
@@ -215,7 +215,7 @@
AndroidPackage::isAllowNativeHeapPointerTagging,
AndroidPackage::isAllowTaskReparenting,
AndroidPackage::isBackupInForeground,
- AndroidPackage::isBaseHardwareAccelerated,
+ AndroidPackage::isHardwareAccelerated,
AndroidPackage::isCantSaveState,
AndroidPackage::isCoreApp,
AndroidPackage::isCrossProfile,
@@ -260,7 +260,7 @@
AndroidPackage::isUsesNonSdkApi,
AndroidPackage::isVisibleToInstantApps,
AndroidPackage::isVmSafeMode,
- AndroidPackage::isLeavingSharedUid,
+ AndroidPackage::isLeavingSharedUser,
AndroidPackage::isResetEnabledSettingsOnAppDataCleared,
AndroidPackage::getMaxAspectRatio,
AndroidPackage::getMinAspectRatio,
@@ -293,7 +293,7 @@
adder(AndroidPackage::getUsesOptionalLibraries, "testUsesOptionalLibrary"),
adder(AndroidPackage::getUsesOptionalNativeLibraries, "testUsesOptionalNativeLibrary"),
getSetByValue(
- AndroidPackage::areAttributionsUserVisible,
+ AndroidPackage::isAttributionsUserVisible,
PackageImpl::setAttributionsAreUserVisible,
true
),
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/uninstall/UninstallCompleteCallbackTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/uninstall/UninstallCompleteCallbackTest.kt
new file mode 100644
index 0000000..52fc91d
--- /dev/null
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/uninstall/UninstallCompleteCallbackTest.kt
@@ -0,0 +1,90 @@
+/*
+ * 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.test.uninstall
+
+import android.app.PackageDeleteObserver
+import android.content.Intent
+import android.content.pm.IPackageDeleteObserver2
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.UninstallCompleteCallback
+import android.os.Parcel
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations.initMocks
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+
+class UninstallCompleteCallbackTest {
+
+ val PACKAGE_NAME: String = "com.example.package"
+ val ERROR_MSG: String = "no error"
+
+ @get:Rule
+ val mockito: MockitoRule = MockitoJUnit.rule()
+
+ @Mock
+ lateinit var mockAdapter: PackageDeleteObserver
+
+ val mockBinder: IPackageDeleteObserver2.Stub = object : IPackageDeleteObserver2.Stub() {
+ override fun onUserActionRequired(intent: Intent) {
+ mockAdapter.onUserActionRequired(intent)
+ }
+ override fun onPackageDeleted(basePackageName: String, returnCode: Int, msg: String) {
+ mockAdapter.onPackageDeleted(basePackageName, returnCode, msg)
+ }
+ }
+
+ @Before
+ fun setUp() {
+ initMocks(this)
+ }
+
+ @Test
+ fun testCallDelegation () {
+ doReturn(mockBinder).`when`(mockAdapter).binder
+
+ val callback = UninstallCompleteCallback(mockAdapter.binder.asBinder())
+ callback.onUninstallComplete(PACKAGE_NAME, PackageManager.DELETE_SUCCEEDED, ERROR_MSG)
+
+ verify(mockAdapter, times(1)).onPackageDeleted(PACKAGE_NAME,
+ PackageManager.DELETE_SUCCEEDED, ERROR_MSG)
+ }
+
+ @Test
+ fun testClassIsParcelable() {
+ doReturn(mockBinder).`when`(mockAdapter).binder
+
+ val callback = UninstallCompleteCallback(mockAdapter.binder.asBinder())
+
+ val parcel = Parcel.obtain()
+ callback.writeToParcel(parcel, callback.describeContents())
+ parcel.setDataPosition(0)
+
+ val callbackFromParcel = UninstallCompleteCallback.CREATOR.createFromParcel(parcel)
+
+ callbackFromParcel.onUninstallComplete(PACKAGE_NAME, PackageManager.DELETE_SUCCEEDED,
+ ERROR_MSG)
+
+ verify(mockAdapter, times(1)).onPackageDeleted(PACKAGE_NAME,
+ PackageManager.DELETE_SUCCEEDED, ERROR_MSG)
+ }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceInjectorTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceInjectorTest.java
index 09df96f..e01a9a9 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceInjectorTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceInjectorTest.java
@@ -69,70 +69,70 @@
}
@Test
- public void testGetSecondaryDisplayIdsForStartingBackgroundUsers_notSupported() {
+ public void testGetDisplayIdsForStartingBackgroundUsers_notSupported() {
mockUmIsUsersOnSecondaryDisplaysEnabled(false);
- int [] displayIds = mInjector.getSecondaryDisplayIdsForStartingBackgroundUsers();
+ int [] displayIds = mInjector.getDisplayIdsForStartingVisibleBackgroundUsers();
- assertWithMessage("mAms.getSecondaryDisplayIdsForStartingBackgroundUsers()")
+ assertWithMessage("mAms.getDisplayIdsForStartingBackgroundUsers()")
.that(displayIds).isNull();
}
@Test
- public void testGetSecondaryDisplayIdsForStartingBackgroundUsers_noDisplaysAtAll() {
+ public void testGetDisplayIdsForStartingBackgroundUsers_noDisplaysAtAll() {
mockUmIsUsersOnSecondaryDisplaysEnabled(true);
mockGetDisplays();
- int[] displayIds = mInjector.getSecondaryDisplayIdsForStartingBackgroundUsers();
+ int[] displayIds = mInjector.getDisplayIdsForStartingVisibleBackgroundUsers();
- assertWithMessage("mAms.getSecondaryDisplayIdsForStartingBackgroundUsers()")
+ assertWithMessage("mAms.getDisplayIdsForStartingBackgroundUsers()")
.that(displayIds).isNull();
}
@Test
- public void testGetSecondaryDisplayIdsForStartingBackgroundUsers_defaultDisplayOnly() {
+ public void testGetDisplayIdsForStartingBackgroundUsers_defaultDisplayOnly() {
mockUmIsUsersOnSecondaryDisplaysEnabled(true);
mockGetDisplays(mDefaultDisplay);
- int[] displayIds = mInjector.getSecondaryDisplayIdsForStartingBackgroundUsers();
+ int[] displayIds = mInjector.getDisplayIdsForStartingVisibleBackgroundUsers();
- assertWithMessage("mAms.getSecondaryDisplayIdsForStartingBackgroundUsers()")
+ assertWithMessage("mAms.getDisplayIdsForStartingBackgroundUsers()")
.that(displayIds).isNull();
}
@Test
- public void testGetSecondaryDisplayIdsForStartingBackgroundUsers_noDefaultDisplay() {
+ public void testGetDisplayIdsForStartingBackgroundUsers_noDefaultDisplay() {
mockUmIsUsersOnSecondaryDisplaysEnabled(true);
mockGetDisplays(validDisplay(42));
- int[] displayIds = mInjector.getSecondaryDisplayIdsForStartingBackgroundUsers();
+ int[] displayIds = mInjector.getDisplayIdsForStartingVisibleBackgroundUsers();
- assertWithMessage("mAms.getSecondaryDisplayIdsForStartingBackgroundUsers()")
+ assertWithMessage("mAms.getDisplayIdsForStartingBackgroundUsers()")
.that(displayIds).isNull();
}
@Test
- public void testGetSecondaryDisplayIdsForStartingBackgroundUsers_mixed() {
+ public void testGetDisplayIdsForStartingBackgroundUsers_mixed() {
mockUmIsUsersOnSecondaryDisplaysEnabled(true);
mockGetDisplays(mDefaultDisplay, validDisplay(42), invalidDisplay(108));
- int[] displayIds = mInjector.getSecondaryDisplayIdsForStartingBackgroundUsers();
+ int[] displayIds = mInjector.getDisplayIdsForStartingVisibleBackgroundUsers();
- assertWithMessage("mAms.getSecondaryDisplayIdsForStartingBackgroundUsers()")
+ assertWithMessage("mAms.getDisplayIdsForStartingBackgroundUsers()")
.that(displayIds).isNotNull();
- assertWithMessage("mAms.getSecondaryDisplayIdsForStartingBackgroundUsers()")
+ assertWithMessage("mAms.getDisplayIdsForStartingBackgroundUsers()")
.that(displayIds).asList().containsExactly(42);
}
// Extra test to make sure the array is properly copied...
@Test
- public void testGetSecondaryDisplayIdsForStartingBackgroundUsers_mixed_invalidFirst() {
+ public void testGetDisplayIdsForStartingBackgroundUsers_mixed_invalidFirst() {
mockUmIsUsersOnSecondaryDisplaysEnabled(true);
mockGetDisplays(invalidDisplay(108), mDefaultDisplay, validDisplay(42));
- int[] displayIds = mInjector.getSecondaryDisplayIdsForStartingBackgroundUsers();
+ int[] displayIds = mInjector.getDisplayIdsForStartingVisibleBackgroundUsers();
- assertWithMessage("mAms.getSecondaryDisplayIdsForStartingBackgroundUsers()")
+ assertWithMessage("mAms.getDisplayIdsForStartingBackgroundUsers()")
.that(displayIds).asList().containsExactly(42);
}
@@ -160,6 +160,6 @@
private void mockUmIsUsersOnSecondaryDisplaysEnabled(boolean enabled) {
Log.d(TAG, "Mocking UserManager.isUsersOnSecondaryDisplaysEnabled() to return " + enabled);
- doReturn(enabled).when(() -> UserManager.isUsersOnSecondaryDisplaysEnabled());
+ doReturn(enabled).when(() -> UserManager.isVisibleBackgroundUsersEnabled());
}
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
index 8d78cd6..f56b0b4 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
@@ -197,7 +197,7 @@
private void enqueueOrReplaceBroadcast(BroadcastProcessQueue queue,
BroadcastRecord record, int recordIndex, long enqueueTime) {
- queue.enqueueOrReplaceBroadcast(record, recordIndex);
+ queue.enqueueOrReplaceBroadcast(record, recordIndex, null /* replacedBroadcastConsumer */);
record.enqueueTime = enqueueTime;
}
@@ -327,7 +327,7 @@
final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
final BroadcastRecord airplaneRecord = makeBroadcastRecord(airplane,
List.of(makeMockRegisteredReceiver()));
- queue.enqueueOrReplaceBroadcast(airplaneRecord, 0);
+ queue.enqueueOrReplaceBroadcast(airplaneRecord, 0, null /* replacedBroadcastConsumer */);
queue.setProcessCached(false);
final long notCachedRunnableAt = queue.getRunnableAt();
@@ -349,12 +349,12 @@
// enqueue a bg-priority broadcast then a fg-priority one
final Intent timezone = new Intent(Intent.ACTION_TIMEZONE_CHANGED);
final BroadcastRecord timezoneRecord = makeBroadcastRecord(timezone);
- queue.enqueueOrReplaceBroadcast(timezoneRecord, 0);
+ queue.enqueueOrReplaceBroadcast(timezoneRecord, 0, null /* replacedBroadcastConsumer */);
final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
airplane.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
final BroadcastRecord airplaneRecord = makeBroadcastRecord(airplane);
- queue.enqueueOrReplaceBroadcast(airplaneRecord, 0);
+ queue.enqueueOrReplaceBroadcast(airplaneRecord, 0, null /* replacedBroadcastConsumer */);
// verify that:
// (a) the queue is immediately runnable by existence of a fg-priority broadcast
@@ -385,7 +385,7 @@
final BroadcastRecord airplaneRecord = makeBroadcastRecord(airplane, null,
List.of(withPriority(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN), 10),
withPriority(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN), 0)), true);
- queue.enqueueOrReplaceBroadcast(airplaneRecord, 1);
+ queue.enqueueOrReplaceBroadcast(airplaneRecord, 1, null /* replacedBroadcastConsumer */);
assertFalse(queue.isRunnable());
assertEquals(BroadcastProcessQueue.REASON_BLOCKED, queue.getRunnableAtReason());
@@ -408,7 +408,7 @@
final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
final BroadcastRecord airplaneRecord = makeBroadcastRecord(airplane,
List.of(makeMockRegisteredReceiver()));
- queue.enqueueOrReplaceBroadcast(airplaneRecord, 0);
+ queue.enqueueOrReplaceBroadcast(airplaneRecord, 0, null /* replacedBroadcastConsumer */);
mConstants.MAX_PENDING_BROADCASTS = 128;
queue.invalidateRunnableAt();
@@ -434,11 +434,11 @@
new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED),
List.of(makeMockRegisteredReceiver()));
- queue.enqueueOrReplaceBroadcast(lazyRecord, 0);
+ queue.enqueueOrReplaceBroadcast(lazyRecord, 0, null /* replacedBroadcastConsumer */);
assertThat(queue.getRunnableAt()).isGreaterThan(lazyRecord.enqueueTime);
assertThat(queue.getRunnableAtReason()).isNotEqualTo(testRunnableAtReason);
- queue.enqueueOrReplaceBroadcast(testRecord, 0);
+ queue.enqueueOrReplaceBroadcast(testRecord, 0, null /* replacedBroadcastConsumer */);
assertThat(queue.getRunnableAt()).isAtMost(testRecord.enqueueTime);
assertThat(queue.getRunnableAtReason()).isEqualTo(testRunnableAtReason);
}
@@ -507,20 +507,26 @@
queue.enqueueOrReplaceBroadcast(
makeBroadcastRecord(new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED)
- .addFlags(Intent.FLAG_RECEIVER_OFFLOAD)), 0);
+ .addFlags(Intent.FLAG_RECEIVER_OFFLOAD)), 0,
+ null /* replacedBroadcastConsumer */);
queue.enqueueOrReplaceBroadcast(
- makeBroadcastRecord(new Intent(Intent.ACTION_TIMEZONE_CHANGED)), 0);
+ makeBroadcastRecord(new Intent(Intent.ACTION_TIMEZONE_CHANGED)), 0,
+ null /* replacedBroadcastConsumer */);
queue.enqueueOrReplaceBroadcast(
makeBroadcastRecord(new Intent(Intent.ACTION_LOCKED_BOOT_COMPLETED)
- .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)), 0);
+ .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)), 0,
+ null /* replacedBroadcastConsumer */);
queue.enqueueOrReplaceBroadcast(
makeBroadcastRecord(new Intent(Intent.ACTION_ALARM_CHANGED)
- .addFlags(Intent.FLAG_RECEIVER_OFFLOAD)), 0);
+ .addFlags(Intent.FLAG_RECEIVER_OFFLOAD)), 0,
+ null /* replacedBroadcastConsumer */);
queue.enqueueOrReplaceBroadcast(
- makeBroadcastRecord(new Intent(Intent.ACTION_TIME_TICK)), 0);
+ makeBroadcastRecord(new Intent(Intent.ACTION_TIME_TICK)), 0,
+ null /* replacedBroadcastConsumer */);
queue.enqueueOrReplaceBroadcast(
makeBroadcastRecord(new Intent(Intent.ACTION_LOCALE_CHANGED)
- .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)), 0);
+ .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)), 0,
+ null /* replacedBroadcastConsumer */);
queue.makeActiveNextPending();
assertEquals(Intent.ACTION_LOCKED_BOOT_COMPLETED, queue.getActive().intent.getAction());
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
index d79c4d8..6800d52 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
@@ -154,6 +154,7 @@
private ActivityManagerService mAms;
private BroadcastQueue mQueue;
+ BroadcastConstants mConstants;
/**
* Desired behavior of the next
@@ -277,10 +278,9 @@
}).when(mAms).getProcessRecordLocked(any(), anyInt());
doNothing().when(mAms).appNotResponding(any(), any());
- final BroadcastConstants constants = new BroadcastConstants(
- Settings.Global.BROADCAST_FG_CONSTANTS);
- constants.TIMEOUT = 100;
- constants.ALLOW_BG_ACTIVITY_START_TIMEOUT = 0;
+ mConstants = new BroadcastConstants(Settings.Global.BROADCAST_FG_CONSTANTS);
+ mConstants.TIMEOUT = 100;
+ mConstants.ALLOW_BG_ACTIVITY_START_TIMEOUT = 0;
final BroadcastSkipPolicy emptySkipPolicy = new BroadcastSkipPolicy(mAms) {
public boolean shouldSkip(BroadcastRecord r, Object o) {
// Ignored
@@ -291,7 +291,7 @@
return null;
}
};
- final BroadcastHistory emptyHistory = new BroadcastHistory(constants) {
+ final BroadcastHistory emptyHistory = new BroadcastHistory(mConstants) {
public void addBroadcastToHistoryLocked(BroadcastRecord original) {
// Ignored
}
@@ -299,13 +299,13 @@
if (mImpl == Impl.DEFAULT) {
var q = new BroadcastQueueImpl(mAms, mHandlerThread.getThreadHandler(), TAG,
- constants, emptySkipPolicy, emptyHistory, false,
+ mConstants, emptySkipPolicy, emptyHistory, false,
ProcessList.SCHED_GROUP_DEFAULT);
q.mReceiverBatch.mDeepReceiverCopy = true;
mQueue = q;
} else if (mImpl == Impl.MODERN) {
var q = new BroadcastQueueModernImpl(mAms, mHandlerThread.getThreadHandler(),
- constants, constants, emptySkipPolicy, emptyHistory);
+ mConstants, mConstants, emptySkipPolicy, emptyHistory);
q.mReceiverBatch.mDeepReceiverCopy = true;
mQueue = q;
} else {
@@ -1703,6 +1703,28 @@
}
@Test
+ public void testReplacePending_withPrioritizedBroadcasts() throws Exception {
+ mConstants.MAX_RUNNING_ACTIVE_BROADCASTS = 1;
+ final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_GREEN);
+
+ final Intent userPresent = new Intent(Intent.ACTION_USER_PRESENT)
+ .addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
+
+ final List receivers = List.of(
+ withPriority(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN), 100),
+ withPriority(makeManifestReceiver(PACKAGE_GREEN, CLASS_RED), 50),
+ withPriority(makeManifestReceiver(PACKAGE_GREEN, CLASS_YELLOW), 10),
+ withPriority(makeManifestReceiver(PACKAGE_GREEN, CLASS_BLUE), 0));
+
+ // Enqueue the broadcast a few times and verify that broadcast queues are not stuck
+ // and are emptied eventually.
+ for (int i = 0; i < 6; ++i) {
+ enqueueBroadcast(makeBroadcastRecord(userPresent, callerApp, receivers));
+ }
+ waitForIdle();
+ }
+
+ @Test
public void testIdleAndBarrier() throws Exception {
final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
final ProcessRecord receiverApp = makeActiveProcessRecord(PACKAGE_GREEN);
diff --git a/services/tests/servicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java
similarity index 80%
rename from services/tests/servicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java
rename to services/tests/mockingservicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java
index cd2f205..3480af6 100644
--- a/services/tests/servicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2020 The Android Open Source Project
+ * 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.
@@ -18,6 +18,9 @@
import static com.google.common.truth.Truth.assertThat;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -25,11 +28,11 @@
import static org.mockito.ArgumentMatchers.anyString;
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.app.backup.BackupAgent;
import android.app.backup.BackupAnnotations.BackupDestination;
+import android.app.backup.BackupRestoreEventLogger.DataTypeResult;
import android.app.backup.IBackupManagerMonitor;
import android.app.backup.IBackupObserver;
import android.content.Context;
@@ -37,6 +40,7 @@
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.platform.test.annotations.Presubmit;
+import android.util.FeatureFlagUtils;
import androidx.test.filters.FlakyTest;
import androidx.test.runner.AndroidJUnit4;
@@ -47,15 +51,21 @@
import com.android.server.backup.transport.BackupTransportClient;
import com.android.server.backup.transport.TransportConnection;
import com.android.server.backup.utils.BackupEligibilityRules;
+import com.android.server.backup.utils.BackupManagerMonitorUtils;
import com.google.common.collect.ImmutableSet;
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+import java.util.Arrays;
+import java.util.List;
import java.util.function.IntConsumer;
@Presubmit
@@ -63,6 +73,7 @@
public class UserBackupManagerServiceTest {
private static final String TEST_PACKAGE = "package1";
private static final String[] TEST_PACKAGES = new String[] { TEST_PACKAGE };
+ private static final String TEST_TRANSPORT = "transport";
private static final int WORKER_THREAD_TIMEOUT_MILLISECONDS = 1;
@Mock Context mContext;
@@ -70,21 +81,38 @@
@Mock IBackupObserver mBackupObserver;
@Mock PackageManager mPackageManager;
@Mock TransportConnection mTransportConnection;
+ @Mock TransportManager mTransportManager;
@Mock BackupTransportClient mBackupTransport;
@Mock BackupEligibilityRules mBackupEligibilityRules;
@Mock LifecycleOperationStorage mOperationStorage;
+ private MockitoSession mSession;
private TestBackupService mService;
@Before
public void setUp() throws Exception {
+ mSession = mockitoSession()
+ .initMocks(this)
+ .mockStatic(BackupManagerMonitorUtils.class)
+ .mockStatic(FeatureFlagUtils.class)
+ // TODO(b/263239775): Remove unnecessary stubbing.
+ .strictness(Strictness.LENIENT)
+ .startMocking();
MockitoAnnotations.initMocks(this);
- mService = new TestBackupService(mContext, mPackageManager, mOperationStorage);
+ mService = new TestBackupService(mContext, mPackageManager, mOperationStorage,
+ mTransportManager);
mService.setEnabled(true);
mService.setSetupComplete(true);
}
+ @After
+ public void tearDown() {
+ if (mSession != null) {
+ mSession.finishMocking();
+ }
+ }
+
@Test
public void initializeBackupEnableState_doesntWriteStateToDisk() {
mService.initializeBackupEnableState();
@@ -201,6 +229,26 @@
.cancelOperation(anyInt(), anyBoolean(), any(IntConsumer.class));
}
+ @Test
+ public void testReportDelayedRestoreResult_sendsLogsToMonitor() throws Exception {
+ PackageInfo packageInfo = getPackageInfo(TEST_PACKAGE);
+ when(mPackageManager.getPackageInfoAsUser(anyString(),
+ any(PackageManager.PackageInfoFlags.class), anyInt())).thenReturn(packageInfo);
+ when(mTransportManager.getCurrentTransportName()).thenReturn(TEST_TRANSPORT);
+ when(mTransportManager.getTransportClientOrThrow(eq(TEST_TRANSPORT), anyString()))
+ .thenReturn(mTransportConnection);
+ when(mTransportConnection.connectOrThrow(any())).thenReturn(mBackupTransport);
+ when(mBackupTransport.getBackupManagerMonitor()).thenReturn(mBackupManagerMonitor);
+
+
+ List<DataTypeResult> results = Arrays.asList(new DataTypeResult(/* dataType */ "type_1"),
+ new DataTypeResult(/* dataType */ "type_2"));
+ mService.reportDelayedRestoreResult(TEST_PACKAGE, results);
+
+ verify(() -> BackupManagerMonitorUtils.sendAgentLoggingResults(
+ eq(mBackupManagerMonitor), eq(packageInfo), eq(results)));
+ }
+
private static PackageInfo getPackageInfo(String packageName) {
PackageInfo packageInfo = new PackageInfo();
packageInfo.applicationInfo = new ApplicationInfo();
@@ -215,8 +263,8 @@
private volatile Thread mWorkerThread = null;
TestBackupService(Context context, PackageManager packageManager,
- LifecycleOperationStorage operationStorage) {
- super(context, packageManager, operationStorage);
+ LifecycleOperationStorage operationStorage, TransportManager transportManager) {
+ super(context, packageManager, operationStorage, transportManager);
}
@Override
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 0dfad43..79fbc87 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/JobConcurrencyManagerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/JobConcurrencyManagerTest.java
@@ -839,7 +839,7 @@
private static JobStatus createJob(int uid, int jobId, @Nullable String sourcePackageName) {
return JobStatus.createFromJobInfo(
new JobInfo.Builder(jobId, new ComponentName("foo", "bar")).build(), uid,
- sourcePackageName, UserHandle.getUserId(uid), "JobConcurrencyManagerTest");
+ sourcePackageName, UserHandle.getUserId(uid), "JobConcurrencyManagerTest", null);
}
private static final class TypeConfig {
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
index 6374c66..a8b8f91 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
@@ -197,7 +197,7 @@
private JobStatus createJobStatus(String testTag, JobInfo.Builder jobInfoBuilder,
int callingUid) {
return JobStatus.createFromJobInfo(
- jobInfoBuilder.build(), callingUid, "com.android.test", 0, testTag);
+ jobInfoBuilder.build(), callingUid, "com.android.test", 0, "JSSTest", testTag);
}
private void grantRunLongJobsPermission(boolean grant) {
@@ -254,6 +254,12 @@
ConnectivityController connectivityController = mService.getConnectivityController();
spyOn(connectivityController);
+ mService.mConstants.RUNTIME_MIN_GUARANTEE_MS = 10 * MINUTE_IN_MILLIS;
+ mService.mConstants.RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS = 15 * MINUTE_IN_MILLIS;
+ mService.mConstants.RUNTIME_DATA_TRANSFER_LIMIT_MS = 60 * MINUTE_IN_MILLIS;
+ mService.mConstants.RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR = 1.5f;
+ mService.mConstants.RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS = HOUR_IN_MILLIS;
+ mService.mConstants.RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS = 6 * HOUR_IN_MILLIS;
assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS,
mService.getMinJobExecutionGuaranteeMs(ejMax));
@@ -268,7 +274,7 @@
assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS,
mService.getMinJobExecutionGuaranteeMs(jobDef));
grantRunLongJobsPermission(false); // Without permission
- assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS,
+ assertEquals(mService.mConstants.RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS,
mService.getMinJobExecutionGuaranteeMs(jobDT));
grantRunLongJobsPermission(true); // With permission
doReturn(ConnectivityController.UNKNOWN_TIME)
@@ -288,12 +294,16 @@
assertEquals(mService.mConstants.RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS,
mService.getMinJobExecutionGuaranteeMs(jobDT));
// UserInitiated
+ grantRunLongJobsPermission(false);
+ // Permission isn't granted, so it should just be treated as a regular data transfer job.
+ assertEquals(mService.mConstants.RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS,
+ mService.getMinJobExecutionGuaranteeMs(jobUIDT));
+ // Permission isn't granted, so it should just be treated as a regular job.
+ assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS,
+ mService.getMinJobExecutionGuaranteeMs(jobUI));
+ grantRunLongJobsPermission(true); // With permission
assertEquals(mService.mConstants.RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS,
mService.getMinJobExecutionGuaranteeMs(jobUI));
- grantRunLongJobsPermission(false);
- assertEquals(mService.mConstants.RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS,
- mService.getMinJobExecutionGuaranteeMs(jobUIDT));
- grantRunLongJobsPermission(true); // With permission
doReturn(ConnectivityController.UNKNOWN_TIME)
.when(connectivityController).getEstimatedTransferTimeMs(any());
assertEquals(mService.mConstants.RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS,
@@ -1111,7 +1121,7 @@
i < 300 ? JobScheduler.RESULT_SUCCESS : JobScheduler.RESULT_FAILURE;
assertEquals("Got unexpected result for schedule #" + (i + 1),
expected,
- mService.scheduleAsPackage(job, null, 10123, null, 0, ""));
+ mService.scheduleAsPackage(job, null, 10123, null, 0, "JSSTest", ""));
}
}
@@ -1132,7 +1142,7 @@
for (int i = 0; i < 500; ++i) {
assertEquals("Got unexpected result for schedule #" + (i + 1),
JobScheduler.RESULT_SUCCESS,
- mService.scheduleAsPackage(job, null, 10123, null, 0, ""));
+ mService.scheduleAsPackage(job, null, 10123, null, 0, "JSSTest", ""));
}
}
@@ -1153,7 +1163,8 @@
for (int i = 0; i < 500; ++i) {
assertEquals("Got unexpected result for schedule #" + (i + 1),
JobScheduler.RESULT_SUCCESS,
- mService.scheduleAsPackage(job, null, 10123, "proxied.package", 0, ""));
+ mService.scheduleAsPackage(job, null, 10123, "proxied.package", 0, "JSSTest",
+ ""));
}
}
@@ -1177,7 +1188,7 @@
assertEquals("Got unexpected result for schedule #" + (i + 1),
expected,
mService.scheduleAsPackage(job, null, 10123, job.getService().getPackageName(),
- 0, ""));
+ 0, "JSSTest", ""));
}
}
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/BatteryControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/BatteryControllerTest.java
index 7c435be..f334a6a 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/BatteryControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/BatteryControllerTest.java
@@ -194,7 +194,7 @@
private JobStatus createJobStatus(String testTag, String packageName, int callingUid,
JobInfo jobInfo) {
JobStatus js = JobStatus.createFromJobInfo(
- jobInfo, callingUid, packageName, SOURCE_USER_ID, testTag);
+ jobInfo, callingUid, packageName, SOURCE_USER_ID, "BCTest", testTag);
js.serviceProcessName = "testProcess";
// Make sure tests aren't passing just because the default bucket is likely ACTIVE.
js.setStandbyBucket(FREQUENT_INDEX);
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java
index 42e22f3..32e5c83 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java
@@ -1316,7 +1316,7 @@
private static JobStatus createJobStatus(JobInfo.Builder job, int uid,
long earliestRunTimeElapsedMillis, long latestRunTimeElapsedMillis) {
- return new JobStatus(job.build(), uid, null, -1, 0, null,
+ return new JobStatus(job.build(), uid, null, -1, 0, null, null,
earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis, 0, 0, null, 0, 0);
}
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java
index 398acb8..1e65b38 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java
@@ -195,7 +195,7 @@
private JobStatus createJobStatus(String testTag, JobInfo.Builder job) {
JobInfo jobInfo = job.build();
JobStatus js = JobStatus.createFromJobInfo(
- jobInfo, 1000, SOURCE_PACKAGE, SOURCE_USER_ID, testTag);
+ jobInfo, 1000, SOURCE_PACKAGE, SOURCE_USER_ID, "FCTest", testTag);
js.enqueueTime = FROZEN_TIME;
return js;
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java
index 7f522b0..d2ee9ff 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java
@@ -920,12 +920,12 @@
long latestRunTimeElapsedMillis) {
final JobInfo job = new JobInfo.Builder(101, new ComponentName("foo", "bar"))
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY).build();
- return new JobStatus(job, 0, null, -1, 0, null, earliestRunTimeElapsedMillis,
+ return new JobStatus(job, 0, null, -1, 0, null, null, earliestRunTimeElapsedMillis,
latestRunTimeElapsedMillis, 0, 0, null, 0, 0);
}
private static JobStatus createJobStatus(JobInfo job) {
- JobStatus jobStatus = JobStatus.createFromJobInfo(job, 0, null, -1, "JobStatusTest");
+ JobStatus jobStatus = JobStatus.createFromJobInfo(job, 0, null, -1, "JobStatusTest", null);
jobStatus.serviceProcessName = "testProcess";
return jobStatus;
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java
index b949b3b..fb59ea2 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java
@@ -184,7 +184,7 @@
private static JobStatus createJobStatus(String testTag, String packageName, int callingUid,
JobInfo jobInfo) {
JobStatus js = JobStatus.createFromJobInfo(
- jobInfo, callingUid, packageName, SOURCE_USER_ID, testTag);
+ jobInfo, callingUid, packageName, SOURCE_USER_ID, "PCTest", testTag);
js.serviceProcessName = "testProcess";
js.setStandbyBucket(FREQUENT_INDEX);
// Make sure Doze and background-not-restricted don't affect tests.
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
index 9aef674..6f713e0 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
@@ -383,7 +383,7 @@
private JobStatus createJobStatus(String testTag, String packageName, int callingUid,
JobInfo jobInfo) {
JobStatus js = JobStatus.createFromJobInfo(
- jobInfo, callingUid, packageName, SOURCE_USER_ID, testTag);
+ jobInfo, callingUid, packageName, SOURCE_USER_ID, "QCTest", testTag);
js.serviceProcessName = "testProcess";
// Make sure tests aren't passing just because the default bucket is likely ACTIVE.
js.setStandbyBucket(FREQUENT_INDEX);
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/StateControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/StateControllerTest.java
index 51d641b..b111757 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/StateControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/StateControllerTest.java
@@ -137,7 +137,7 @@
.setMinimumLatency(Math.abs(jobId) + 1)
.build();
return JobStatus.createFromJobInfo(
- jobInfo, CALLING_UID, SOURCE_PACKAGE, SOURCE_USER_ID, testTag);
+ jobInfo, CALLING_UID, SOURCE_PACKAGE, SOURCE_USER_ID, "SCTest", testTag);
}
@Test
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/TimeControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/TimeControllerTest.java
index d64c1b3..27efcfa 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/TimeControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/TimeControllerTest.java
@@ -137,7 +137,7 @@
private JobStatus createJobStatus(String testTag, JobInfo.Builder job) {
JobInfo jobInfo = job.build();
return JobStatus.createFromJobInfo(
- jobInfo, 1000, SOURCE_PACKAGE, SOURCE_USER_ID, testTag);
+ jobInfo, 1000, SOURCE_PACKAGE, SOURCE_USER_ID, "TCTest", testTag);
}
@Test
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/restrictions/ThermalStatusRestrictionTest.java b/services/tests/mockingservicestests/src/com/android/server/job/restrictions/ThermalStatusRestrictionTest.java
index 9067285..02fdfad 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/restrictions/ThermalStatusRestrictionTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/restrictions/ThermalStatusRestrictionTest.java
@@ -328,6 +328,6 @@
private JobStatus createJobStatus(String testTag, JobInfo jobInfo) {
return JobStatus.createFromJobInfo(
- jobInfo, CALLING_UID, SOURCE_PACKAGE, SOURCE_USER_ID, testTag);
+ jobInfo, CALLING_UID, SOURCE_PACKAGE, SOURCE_USER_ID, "TSRTest", testTag);
}
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/SharedLibrariesImplTest.kt b/services/tests/mockingservicestests/src/com/android/server/pm/SharedLibrariesImplTest.kt
index 28c78b2..cffd027 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/SharedLibrariesImplTest.kt
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/SharedLibrariesImplTest.kt
@@ -391,7 +391,7 @@
libraries?.forEach { pkg.addLibraryName(it) }
staticLibrary?.let {
pkg.setStaticSharedLibraryName(it)
- pkg.setStaticSharedLibVersion(staticLibraryVersion)
+ pkg.setStaticSharedLibraryVersion(staticLibraryVersion)
pkg.setStaticSharedLibrary(true)
}
usesLibraries?.forEach { pkg.addUsesLibrary(it) }
@@ -435,7 +435,7 @@
libraries?.forEach { addLibraryName(it) }
staticLibrary?.let {
setStaticSharedLibraryName(it)
- setStaticSharedLibVersion(staticLibraryVersion)
+ setStaticSharedLibraryVersion(staticLibraryVersion)
setStaticSharedLibrary(true)
}
usesLibraries?.forEach { addUsesLibrary(it) }
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 58cff94..579621c 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUMDTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUMDTest.java
@@ -15,19 +15,6 @@
*/
package com.android.server.pm;
-import static android.os.UserHandle.USER_NULL;
-import static android.view.Display.DEFAULT_DISPLAY;
-import static android.view.Display.INVALID_DISPLAY;
-
-import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_FAILURE;
-import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE;
-import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE;
-import static com.android.server.pm.UserVisibilityChangedEvent.onInvisible;
-import static com.android.server.pm.UserVisibilityChangedEvent.onVisible;
-import static com.android.server.pm.UserVisibilityMediator.INITIAL_CURRENT_USER_ID;
-
-import org.junit.Test;
-
/**
* Tests for {@link UserVisibilityMediator} tests for devices that support concurrent Multiple
* Users on Multiple Displays (A.K.A {@code MUMD}).
@@ -35,208 +22,10 @@
* <p>Run as
* {@code atest FrameworksMockingServicesTests:com.android.server.pm.UserVisibilityMediatorMUMDTest}
*/
-public final class UserVisibilityMediatorMUMDTest extends UserVisibilityMediatorTestCase {
+public final class UserVisibilityMediatorMUMDTest
+ extends UserVisibilityMediatorVisibleBackgroundUserTestCase {
public UserVisibilityMediatorMUMDTest() throws Exception {
super(/* usersOnSecondaryDisplaysEnabled= */ true);
}
-
- @Test
- public void testStartFgUser_onDefaultDisplay() throws Exception {
- AsyncUserVisibilityListener listener = addListenerForEvents(
- onInvisible(INITIAL_CURRENT_USER_ID),
- onVisible(USER_ID));
-
- int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, FG,
- DEFAULT_DISPLAY);
- assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE);
-
- expectUserIsVisible(USER_ID);
- expectUserIsVisibleOnDisplay(USER_ID, DEFAULT_DISPLAY);
- expectUserIsNotVisibleOnDisplay(USER_ID, INVALID_DISPLAY);
- expectUserIsNotVisibleOnDisplay(USER_ID, SECONDARY_DISPLAY_ID);
- expectVisibleUsers(USER_ID);
-
- expectDisplayAssignedToUser(USER_ID, DEFAULT_DISPLAY);
- expectUserAssignedToDisplay(DEFAULT_DISPLAY, USER_ID);
- expectUserAssignedToDisplay(INVALID_DISPLAY, USER_ID);
- expectUserAssignedToDisplay(SECONDARY_DISPLAY_ID, USER_ID);
-
- expectDisplayAssignedToUser(USER_NULL, INVALID_DISPLAY);
-
- listener.verify();
- }
-
- @Test
- public void testSwitchFgUser_onDefaultDisplay() throws Exception {
- int previousCurrentUserId = OTHER_USER_ID;
- int currentUserId = USER_ID;
- AsyncUserVisibilityListener listener = addListenerForEvents(
- onInvisible(INITIAL_CURRENT_USER_ID),
- onVisible(previousCurrentUserId),
- onInvisible(previousCurrentUserId),
- onVisible(currentUserId));
- startForegroundUser(previousCurrentUserId);
-
- int result = mMediator.assignUserToDisplayOnStart(currentUserId, currentUserId, FG,
- DEFAULT_DISPLAY);
- assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE);
-
- expectUserIsVisible(currentUserId);
- expectUserIsVisibleOnDisplay(currentUserId, DEFAULT_DISPLAY);
- expectUserIsNotVisibleOnDisplay(currentUserId, INVALID_DISPLAY);
- expectUserIsNotVisibleOnDisplay(currentUserId, SECONDARY_DISPLAY_ID);
- expectVisibleUsers(currentUserId);
-
- expectDisplayAssignedToUser(currentUserId, DEFAULT_DISPLAY);
- expectUserAssignedToDisplay(DEFAULT_DISPLAY, currentUserId);
- expectUserAssignedToDisplay(INVALID_DISPLAY, currentUserId);
- expectUserAssignedToDisplay(SECONDARY_DISPLAY_ID, currentUserId);
-
- expectUserIsNotVisibleAtAll(previousCurrentUserId);
- expectNoDisplayAssignedToUser(previousCurrentUserId);
-
- listener.verify();
- }
-
- @Test
- public void testStartBgProfile_onDefaultDisplay_whenParentIsCurrentUser() throws Exception {
- AsyncUserVisibilityListener listener = addListenerForEvents(
- onInvisible(INITIAL_CURRENT_USER_ID),
- onVisible(PARENT_USER_ID),
- onVisible(PROFILE_USER_ID));
- startForegroundUser(PARENT_USER_ID);
-
- 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);
- expectUserIsNotVisibleOnDisplay(PROFILE_USER_ID, INVALID_DISPLAY);
- expectUserIsNotVisibleOnDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID);
- expectUserIsVisibleOnDisplay(PROFILE_USER_ID, DEFAULT_DISPLAY);
- expectVisibleUsers(PARENT_USER_ID, PROFILE_USER_ID);
-
- expectDisplayAssignedToUser(PROFILE_USER_ID, DEFAULT_DISPLAY);
- expectUserAssignedToDisplay(DEFAULT_DISPLAY, PARENT_USER_ID);
-
- listener.verify();
- }
-
- @Test
- public void testStartFgUser_onInvalidDisplay() throws Exception {
- AsyncUserVisibilityListener listener = addListenerForNoEvents();
-
- int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, FG, INVALID_DISPLAY);
-
- assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE);
-
- listener.verify();
- }
-
- @Test
- public void testStartBgUser_onInvalidDisplay() throws Exception {
- AsyncUserVisibilityListener listener = addListenerForNoEvents();
-
- int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG_VISIBLE,
- INVALID_DISPLAY);
-
- assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE);
-
- expectUserIsNotVisibleAtAll(USER_ID);
-
- listener.verify();
- }
-
- @Test
- public void testStartBgUser_onSecondaryDisplay_displayAvailable() throws Exception {
- AsyncUserVisibilityListener listener = addListenerForEvents(onVisible(USER_ID));
-
- int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG_VISIBLE,
- SECONDARY_DISPLAY_ID);
- assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE);
-
- expectUserIsVisible(USER_ID);
- expectUserIsVisibleOnDisplay(USER_ID, SECONDARY_DISPLAY_ID);
- expectUserIsNotVisibleOnDisplay(USER_ID, INVALID_DISPLAY);
- expectUserIsNotVisibleOnDisplay(USER_ID, DEFAULT_DISPLAY);
- expectVisibleUsers(INITIAL_CURRENT_USER_ID, USER_ID);
-
- expectDisplayAssignedToUser(USER_ID, SECONDARY_DISPLAY_ID);
- expectUserAssignedToDisplay(SECONDARY_DISPLAY_ID, USER_ID);
-
- listener.verify();
- }
-
- @Test
- public void testVisibilityOfCurrentUserAndProfilesOnDisplayAssignedToAnotherUser()
- throws Exception {
- startDefaultProfile();
-
- // Make sure they were visible before
- 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_VISIBLE,
- SECONDARY_DISPLAY_ID);
- assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE);
-
- expectUserIsNotVisibleOnDisplay("after", PARENT_USER_ID, SECONDARY_DISPLAY_ID);
- expectUserIsNotVisibleOnDisplay("after", PROFILE_USER_ID, SECONDARY_DISPLAY_ID);
- }
-
- @Test
- public void testStartBgUser_onSecondaryDisplay_displayAlreadyAssigned() throws Exception {
- AsyncUserVisibilityListener listener = addListenerForEvents(onVisible(OTHER_USER_ID));
- startUserInSecondaryDisplay(OTHER_USER_ID, SECONDARY_DISPLAY_ID);
-
- int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG_VISIBLE,
- SECONDARY_DISPLAY_ID);
- assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE);
-
- expectUserIsNotVisibleAtAll(USER_ID);
- expectNoDisplayAssignedToUser(USER_ID);
- expectUserAssignedToDisplay(SECONDARY_DISPLAY_ID, OTHER_USER_ID);
-
- listener.verify();
- }
-
- @Test
- public void testStartBgUser_onSecondaryDisplay_userAlreadyAssigned() throws Exception {
- AsyncUserVisibilityListener listener = addListenerForEvents(onVisible(USER_ID));
- startUserInSecondaryDisplay(USER_ID, OTHER_SECONDARY_DISPLAY_ID);
-
- int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG_VISIBLE,
- SECONDARY_DISPLAY_ID);
- assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE);
-
- expectUserIsVisible(USER_ID);
- expectUserIsVisibleOnDisplay(USER_ID, OTHER_SECONDARY_DISPLAY_ID);
- expectUserIsNotVisibleOnDisplay(USER_ID, SECONDARY_DISPLAY_ID);
- expectUserIsNotVisibleOnDisplay(USER_ID, INVALID_DISPLAY);
- expectUserIsNotVisibleOnDisplay(USER_ID, DEFAULT_DISPLAY);
- expectVisibleUsers(INITIAL_CURRENT_USER_ID, USER_ID);
-
- expectDisplayAssignedToUser(USER_ID, OTHER_SECONDARY_DISPLAY_ID);
- expectUserAssignedToDisplay(OTHER_SECONDARY_DISPLAY_ID, USER_ID);
-
- listener.verify();
- }
-
- @Test
- public void testStartBgProfile_onDefaultDisplay_whenParentVisibleOnSecondaryDisplay()
- throws Exception {
- 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_VISIBLE, DEFAULT_DISPLAY);
- assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE);
-
- expectUserIsNotVisibleAtAll(PROFILE_USER_ID);
- expectNoDisplayAssignedToUser(PROFILE_USER_ID);
- expectUserAssignedToDisplay(OTHER_SECONDARY_DISPLAY_ID, PARENT_USER_ID);
-
- listener.verify();
- }
}
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 3d64c29..88709e1 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorSUSDTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorSUSDTest.java
@@ -61,7 +61,7 @@
expectUserAssignedToDisplay(INVALID_DISPLAY, USER_ID);
expectUserAssignedToDisplay(SECONDARY_DISPLAY_ID, USER_ID);
- expectDisplayAssignedToUser(USER_NULL, INVALID_DISPLAY);
+ expectNoDisplayAssignedToUser(USER_NULL);
listener.verify();
}
@@ -99,7 +99,8 @@
}
@Test
- public void testStartBgProfile_onDefaultDisplay_whenParentIsCurrentUser() throws Exception {
+ public void testStartVisibleBgProfile_onDefaultDisplay_whenParentIsCurrentUser()
+ throws Exception {
AsyncUserVisibilityListener listener = addListenerForEvents(
onInvisible(INITIAL_CURRENT_USER_ID),
onVisible(PARENT_USER_ID),
@@ -123,7 +124,7 @@
}
@Test
- public void testStartBgUser_onSecondaryDisplay() throws Exception {
+ public void testStartVisibleBgUser_onSecondaryDisplay() throws Exception {
AsyncUserVisibilityListener listener = addListenerForNoEvents();
int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG_VISIBLE,
@@ -133,7 +134,7 @@
expectUserIsNotVisibleAtAll(USER_ID);
expectNoDisplayAssignedToUser(USER_ID);
- expectNoUserAssignedToDisplay(SECONDARY_DISPLAY_ID);
+ expectInitialCurrentUserAssignedToDisplay(SECONDARY_DISPLAY_ID);
listener.verify();
}
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 74fd9ff..e4664d2 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java
@@ -109,12 +109,12 @@
private Handler mHandler;
protected AsyncUserVisibilityListener.Factory mListenerFactory;
- private final boolean mUsersOnSecondaryDisplaysEnabled;
+ private final boolean mBackgroundUsersOnDisplaysEnabled;
protected UserVisibilityMediator mMediator;
- protected UserVisibilityMediatorTestCase(boolean usersOnSecondaryDisplaysEnabled) {
- mUsersOnSecondaryDisplaysEnabled = usersOnSecondaryDisplaysEnabled;
+ protected UserVisibilityMediatorTestCase(boolean backgroundUsersOnDisplaysEnabled) {
+ mBackgroundUsersOnDisplaysEnabled = backgroundUsersOnDisplaysEnabled;
}
@Before
@@ -123,7 +123,7 @@
Thread thread = mHandler.getLooper().getThread();
Log.i(TAG, "setFixtures(): using thread " + thread + " (from handler " + mHandler + ")");
mListenerFactory = new AsyncUserVisibilityListener.Factory(mExpect, thread);
- mMediator = new UserVisibilityMediator(mUsersOnSecondaryDisplaysEnabled, mHandler);
+ mMediator = new UserVisibilityMediator(mBackgroundUsersOnDisplaysEnabled, mHandler);
mDumpableDumperRule.addDumpable(mMediator);
}
@@ -149,7 +149,7 @@
expectUserIsNotVisibleAtAll(USER_ID);
expectNoDisplayAssignedToUser(USER_ID);
- expectNoUserAssignedToDisplay(DEFAULT_DISPLAY);
+ expectInitialCurrentUserAssignedToDisplay(DEFAULT_DISPLAY);
listener.verify();
}
@@ -163,13 +163,17 @@
expectUserIsNotVisibleAtAll(USER_ID);
expectNoDisplayAssignedToUser(USER_ID);
- expectNoUserAssignedToDisplay(DEFAULT_DISPLAY);
+ expectInitialCurrentUserAssignedToDisplay(DEFAULT_DISPLAY);
listener.verify();
}
@Test
- public final void testStartBgUser_onDefaultDisplay_visible() throws Exception {
+ public final void testStartVisibleBgUser_onDefaultDisplay() throws Exception {
+ visibleBgUserCannotBeStartedOnDefaultDisplayTest();
+ }
+
+ protected final void visibleBgUserCannotBeStartedOnDefaultDisplayTest() throws Exception {
AsyncUserVisibilityListener listener = addListenerForNoEvents();
int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG_VISIBLE,
@@ -183,7 +187,7 @@
}
@Test
- public final void testStartBgUser_onSecondaryDisplay_invisible() throws Exception {
+ public final void testStartBgUser_onSecondaryDisplay() throws Exception {
AsyncUserVisibilityListener listener = addListenerForNoEvents();
int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG,
@@ -217,7 +221,7 @@
}
@Test
- public final void testStopVisibleProfile() throws Exception {
+ public final void testStopVisibleBgProfile() throws Exception {
AsyncUserVisibilityListener listener = addListenerForEvents(
onInvisible(INITIAL_CURRENT_USER_ID),
onVisible(PARENT_USER_ID),
@@ -235,7 +239,8 @@
}
@Test
- public final void testVisibleProfileBecomesInvisibleWhenParentIsSwitchedOut() throws Exception {
+ public final void testVisibleBgProfileBecomesInvisibleWhenParentIsSwitchedOut()
+ throws Exception {
AsyncUserVisibilityListener listener = addListenerForEvents(
onInvisible(INITIAL_CURRENT_USER_ID),
onVisible(PARENT_USER_ID),
@@ -255,7 +260,7 @@
}
@Test
- public final void testStartBgProfile_onDefaultDisplay_whenParentIsNotStarted()
+ public final void testStartVisibleBgProfile_onDefaultDisplay_whenParentIsNotStarted()
throws Exception {
AsyncUserVisibilityListener listener = addListenerForNoEvents();
@@ -270,7 +275,7 @@
}
@Test
- public final void testStartBgProfile_onDefaultDisplay_whenParentIsStartedOnBg()
+ public final void testStartVisibleBgProfile_onDefaultDisplay_whenParentIsStartedOnBg()
throws Exception {
AsyncUserVisibilityListener listener = addListenerForNoEvents();
startBackgroundUser(PARENT_USER_ID);
@@ -282,14 +287,14 @@
expectUserIsNotVisibleAtAll(PROFILE_USER_ID);
expectNoDisplayAssignedToUser(PROFILE_USER_ID);
- expectNoUserAssignedToDisplay(DEFAULT_DISPLAY);
+ expectInitialCurrentUserAssignedToDisplay(DEFAULT_DISPLAY);
listener.verify();
}
// Not supported - profiles can only be started on default display
@Test
- public final void testStartBgProfile_onSecondaryDisplay() throws Exception {
+ public final void testStartVisibleBgProfile_onSecondaryDisplay() throws Exception {
AsyncUserVisibilityListener listener = addListenerForNoEvents();
int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID,
@@ -298,13 +303,13 @@
expectUserIsNotVisibleAtAll(PROFILE_USER_ID);
expectNoDisplayAssignedToUser(PROFILE_USER_ID);
- expectNoUserAssignedToDisplay(SECONDARY_DISPLAY_ID);
+ expectInitialCurrentUserAssignedToDisplay(SECONDARY_DISPLAY_ID);
listener.verify();
}
@Test
- public final void testStartBgProfile_onSecondaryDisplay_invisible() throws Exception {
+ public final void testStartBgProfile_onSecondaryDisplay() throws Exception {
AsyncUserVisibilityListener listener = addListenerForNoEvents();
int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID, BG,
@@ -313,7 +318,7 @@
expectUserIsNotVisibleAtAll(PROFILE_USER_ID);
expectNoDisplayAssignedToUser(PROFILE_USER_ID);
- expectNoUserAssignedToDisplay(SECONDARY_DISPLAY_ID);
+ expectInitialCurrentUserAssignedToDisplay(SECONDARY_DISPLAY_ID);
listener.verify();
}
@@ -329,7 +334,7 @@
expectUserIsNotVisibleAtAll(PROFILE_USER_ID);
expectNoDisplayAssignedToUser(PROFILE_USER_ID);
- expectNoUserAssignedToDisplay(DEFAULT_DISPLAY);
+ expectInitialCurrentUserAssignedToDisplay(DEFAULT_DISPLAY);
listener.verify();
}
@@ -344,7 +349,7 @@
expectUserIsNotVisibleAtAll(PROFILE_USER_ID);
expectNoDisplayAssignedToUser(PROFILE_USER_ID);
- expectNoUserAssignedToDisplay(SECONDARY_DISPLAY_ID);
+ expectInitialCurrentUserAssignedToDisplay(SECONDARY_DISPLAY_ID);
listener.verify();
}
@@ -477,7 +482,7 @@
}
protected void expectUserIsVisible(@UserIdInt int userId) {
- expectWithMessage("mediator.isUserVisible(%s)", userId)
+ expectWithMessage("isUserVisible(%s)", userId)
.that(mMediator.isUserVisible(userId))
.isTrue();
}
@@ -490,13 +495,13 @@
}
protected void expectUserIsVisibleOnDisplay(@UserIdInt int userId, int displayId) {
- expectWithMessage("mediator.isUserVisible(%s, %s)", userId, displayId)
+ expectWithMessage("isUserVisible(%s, %s)", userId, displayId)
.that(mMediator.isUserVisible(userId, displayId))
.isTrue();
}
protected void expectUserIsNotVisibleOnDisplay(@UserIdInt int userId, int displayId) {
- expectWithMessage("mediator.isUserVisible(%s, %s)", userId, displayId)
+ expectWithMessage("isUserVisible(%s, %s)", userId, displayId)
.that(mMediator.isUserVisible(userId, displayId))
.isFalse();
}
@@ -504,13 +509,13 @@
protected void expectUserIsNotVisibleOnDisplay(String when, @UserIdInt int userId,
int displayId) {
String suffix = TextUtils.isEmpty(when) ? "" : " on " + when;
- expectWithMessage("mediator.isUserVisible(%s, %s)%s", userId, displayId, suffix)
+ expectWithMessage("isUserVisible(%s, %s)%s", userId, displayId, suffix)
.that(mMediator.isUserVisible(userId, displayId))
.isFalse();
}
protected void expectUserIsNotVisibleAtAll(@UserIdInt int userId) {
- expectWithMessage("mediator.isUserVisible(%s)", userId)
+ expectWithMessage("isUserVisible(%s)", userId)
.that(mMediator.isUserVisible(userId))
.isFalse();
expectUserIsNotVisibleOnDisplay(userId, DEFAULT_DISPLAY);
@@ -534,7 +539,7 @@
.that(mMediator.getUserAssignedToDisplay(displayId)).isEqualTo(userId);
}
- protected void expectNoUserAssignedToDisplay(int displayId) {
+ protected void expectInitialCurrentUserAssignedToDisplay(int displayId) {
expectWithMessage("getUserAssignedToDisplay(%s)", displayId)
.that(mMediator.getUserAssignedToDisplay(displayId))
.isEqualTo(INITIAL_CURRENT_USER_ID);
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorVisibleBackgroundUserTestCase.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorVisibleBackgroundUserTestCase.java
new file mode 100644
index 0000000..66d7eb6
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorVisibleBackgroundUserTestCase.java
@@ -0,0 +1,280 @@
+/*
+ * 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 android.os.UserHandle.USER_NULL;
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.Display.INVALID_DISPLAY;
+
+import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_FAILURE;
+import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE;
+import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE;
+import static com.android.server.pm.UserVisibilityChangedEvent.onInvisible;
+import static com.android.server.pm.UserVisibilityChangedEvent.onVisible;
+import static com.android.server.pm.UserVisibilityMediator.INITIAL_CURRENT_USER_ID;
+
+import android.annotation.UserIdInt;
+
+import org.junit.Test;
+
+/**
+ * Base class for {@link UserVisibilityMediator} test classe on devices that support starting
+ * background users on visible displays (as defined by
+ * {@link android.os.UserManagerInternal#isVisibleBackgroundUsersSupported}).
+ */
+abstract class UserVisibilityMediatorVisibleBackgroundUserTestCase
+ extends UserVisibilityMediatorTestCase {
+
+ UserVisibilityMediatorVisibleBackgroundUserTestCase(boolean backgroundUsersOnDisplaysEnabled)
+ throws Exception {
+ super(backgroundUsersOnDisplaysEnabled);
+ }
+
+ @Test
+ public final void testStartFgUser_onDefaultDisplay() throws Exception {
+ AsyncUserVisibilityListener listener = addListenerForEvents(
+ onInvisible(INITIAL_CURRENT_USER_ID),
+ onVisible(USER_ID));
+
+ int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, FG,
+ DEFAULT_DISPLAY);
+ assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE);
+
+ expectUserIsVisible(USER_ID);
+ expectUserIsVisibleOnDisplay(USER_ID, DEFAULT_DISPLAY);
+ expectUserIsNotVisibleOnDisplay(USER_ID, INVALID_DISPLAY);
+ expectUserIsNotVisibleOnDisplay(USER_ID, SECONDARY_DISPLAY_ID);
+ expectVisibleUsers(USER_ID);
+
+ expectDisplayAssignedToUser(USER_ID, DEFAULT_DISPLAY);
+ expectUserAssignedToDisplay(DEFAULT_DISPLAY, USER_ID);
+ expectUserAssignedToDisplay(INVALID_DISPLAY, USER_ID);
+ expectUserAssignedToDisplay(SECONDARY_DISPLAY_ID, USER_ID);
+
+ expectDisplayAssignedToUser(USER_NULL, INVALID_DISPLAY);
+
+ listener.verify();
+ }
+
+ @Test
+ public final void testSwitchFgUser_onDefaultDisplay() throws Exception {
+ int previousCurrentUserId = OTHER_USER_ID;
+ int currentUserId = USER_ID;
+ AsyncUserVisibilityListener listener = addListenerForEvents(
+ onInvisible(INITIAL_CURRENT_USER_ID),
+ onVisible(previousCurrentUserId),
+ onInvisible(previousCurrentUserId),
+ onVisible(currentUserId));
+ startForegroundUser(previousCurrentUserId);
+
+ int result = mMediator.assignUserToDisplayOnStart(currentUserId, currentUserId, FG,
+ DEFAULT_DISPLAY);
+ assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE);
+
+ expectUserIsVisible(currentUserId);
+ expectUserIsVisibleOnDisplay(currentUserId, DEFAULT_DISPLAY);
+ expectUserIsNotVisibleOnDisplay(currentUserId, INVALID_DISPLAY);
+ expectUserIsNotVisibleOnDisplay(currentUserId, SECONDARY_DISPLAY_ID);
+ expectVisibleUsers(currentUserId);
+
+ expectDisplayAssignedToUser(currentUserId, DEFAULT_DISPLAY);
+ expectUserAssignedToDisplay(DEFAULT_DISPLAY, currentUserId);
+ expectUserAssignedToDisplay(INVALID_DISPLAY, currentUserId);
+ expectUserAssignedToDisplay(SECONDARY_DISPLAY_ID, currentUserId);
+
+ expectUserIsNotVisibleAtAll(previousCurrentUserId);
+ expectNoDisplayAssignedToUser(previousCurrentUserId);
+
+ listener.verify();
+ }
+
+ @Test
+ public final void testStartVisibleBgProfile_onDefaultDisplay_whenParentIsCurrentUser()
+ throws Exception {
+ AsyncUserVisibilityListener listener = addListenerForEvents(
+ onInvisible(INITIAL_CURRENT_USER_ID),
+ onVisible(PARENT_USER_ID),
+ onVisible(PROFILE_USER_ID));
+ startForegroundUser(PARENT_USER_ID);
+
+ 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);
+ expectUserIsNotVisibleOnDisplay(PROFILE_USER_ID, INVALID_DISPLAY);
+ expectUserIsNotVisibleOnDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID);
+ expectUserIsVisibleOnDisplay(PROFILE_USER_ID, DEFAULT_DISPLAY);
+ expectVisibleUsers(PARENT_USER_ID, PROFILE_USER_ID);
+
+ expectDisplayAssignedToUser(PROFILE_USER_ID, DEFAULT_DISPLAY);
+ expectUserAssignedToDisplay(DEFAULT_DISPLAY, PARENT_USER_ID);
+
+ listener.verify();
+ }
+
+ @Test
+ public final void testStartFgUser_onInvalidDisplay() throws Exception {
+ AsyncUserVisibilityListener listener = addListenerForNoEvents();
+
+ int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, FG, INVALID_DISPLAY);
+
+ assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE);
+
+ listener.verify();
+ }
+
+ @Test
+ public final void testStartVisibleBgUser_onInvalidDisplay() throws Exception {
+ AsyncUserVisibilityListener listener = addListenerForNoEvents();
+
+ int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG_VISIBLE,
+ INVALID_DISPLAY);
+
+ assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE);
+
+ expectUserIsNotVisibleAtAll(USER_ID);
+
+ listener.verify();
+ }
+
+ @Test
+ public final void testStartVisibleBgUser_onSecondaryDisplay_displayAvailable()
+ throws Exception {
+ AsyncUserVisibilityListener listener = addListenerForEvents(onVisible(USER_ID));
+
+ int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG_VISIBLE,
+ SECONDARY_DISPLAY_ID);
+ assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE);
+
+ expectUserIsVisible(USER_ID);
+ expectUserIsVisibleOnDisplay(USER_ID, SECONDARY_DISPLAY_ID);
+ expectUserIsNotVisibleOnDisplay(USER_ID, INVALID_DISPLAY);
+ expectUserIsNotVisibleOnDisplay(USER_ID, DEFAULT_DISPLAY);
+ expectVisibleUsers(INITIAL_CURRENT_USER_ID, USER_ID);
+
+ expectDisplayAssignedToUser(USER_ID, SECONDARY_DISPLAY_ID);
+ expectUserAssignedToDisplay(SECONDARY_DISPLAY_ID, USER_ID);
+
+ listener.verify();
+ }
+
+ @Test
+ public final void testVisibilityOfCurrentUserAndProfilesOnDisplayAssignedToAnotherUser()
+ throws Exception {
+ startDefaultProfile();
+
+ // Make sure they were visible before
+ 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_VISIBLE,
+ SECONDARY_DISPLAY_ID);
+ assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE);
+
+ expectUserIsNotVisibleOnDisplay("after", PARENT_USER_ID, SECONDARY_DISPLAY_ID);
+ expectUserIsNotVisibleOnDisplay("after", PROFILE_USER_ID, SECONDARY_DISPLAY_ID);
+ }
+
+ @Test
+ public final void testStartVisibleBgUser_onSecondaryDisplay_displayAlreadyAssigned()
+ throws Exception {
+ AsyncUserVisibilityListener listener = addListenerForEvents(onVisible(OTHER_USER_ID));
+ startUserInSecondaryDisplay(OTHER_USER_ID, SECONDARY_DISPLAY_ID);
+
+ int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG_VISIBLE,
+ SECONDARY_DISPLAY_ID);
+ assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE);
+
+ expectUserIsNotVisibleAtAll(USER_ID);
+ expectNoDisplayAssignedToUser(USER_ID);
+ expectUserAssignedToDisplay(SECONDARY_DISPLAY_ID, OTHER_USER_ID);
+
+ listener.verify();
+ }
+
+ @Test
+ public final void testStartVisibleBgUser_onSecondaryDisplay_userAlreadyAssigned()
+ throws Exception {
+ AsyncUserVisibilityListener listener = addListenerForEvents(onVisible(USER_ID));
+ startUserInSecondaryDisplay(USER_ID, OTHER_SECONDARY_DISPLAY_ID);
+
+ int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG_VISIBLE,
+ SECONDARY_DISPLAY_ID);
+ assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE);
+
+ expectUserIsVisible(USER_ID);
+ expectUserIsVisibleOnDisplay(USER_ID, OTHER_SECONDARY_DISPLAY_ID);
+ expectUserIsNotVisibleOnDisplay(USER_ID, SECONDARY_DISPLAY_ID);
+ expectUserIsNotVisibleOnDisplay(USER_ID, INVALID_DISPLAY);
+ expectUserIsNotVisibleOnDisplay(USER_ID, DEFAULT_DISPLAY);
+ expectVisibleUsers(INITIAL_CURRENT_USER_ID, USER_ID);
+
+ expectDisplayAssignedToUser(USER_ID, OTHER_SECONDARY_DISPLAY_ID);
+ expectUserAssignedToDisplay(OTHER_SECONDARY_DISPLAY_ID, USER_ID);
+
+ listener.verify();
+ }
+
+ @Test
+ public final void
+ testStartVisibleBgProfile_onDefaultDisplay_whenParentVisibleOnSecondaryDisplay()
+ throws Exception {
+ 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_VISIBLE, DEFAULT_DISPLAY);
+ assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE);
+
+ expectUserIsNotVisibleAtAll(PROFILE_USER_ID);
+ expectNoDisplayAssignedToUser(PROFILE_USER_ID);
+ expectUserAssignedToDisplay(OTHER_SECONDARY_DISPLAY_ID, PARENT_USER_ID);
+
+ listener.verify();
+ }
+
+ private void currentUserVisibilityWhenNoDisplayIsAssignedTest(@UserIdInt int currentUserId) {
+ // Conditions below are asserted on other tests, but they're explicitly checked in the 2
+ // tests below as well
+ expectUserIsVisible(currentUserId);
+ expectUserIsVisibleOnDisplay(currentUserId, DEFAULT_DISPLAY);
+ expectUserIsNotVisibleOnDisplay(currentUserId, SECONDARY_DISPLAY_ID);
+ expectUserIsNotVisibleOnDisplay(currentUserId, OTHER_SECONDARY_DISPLAY_ID);
+ expectUserIsNotVisibleOnDisplay(currentUserId, INVALID_DISPLAY);
+
+ expectDisplayAssignedToUser(currentUserId, DEFAULT_DISPLAY);
+ expectUserAssignedToDisplay(DEFAULT_DISPLAY, currentUserId);
+ expectUserAssignedToDisplay(SECONDARY_DISPLAY_ID, currentUserId);
+ expectUserAssignedToDisplay(OTHER_SECONDARY_DISPLAY_ID, currentUserId);
+ expectUserAssignedToDisplay(INVALID_DISPLAY, currentUserId);
+ }
+
+ @Test
+ public final void testCurrentUserVisibilityWhenNoDisplayIsAssigned_onBoot() throws Exception {
+ currentUserVisibilityWhenNoDisplayIsAssignedTest(INITIAL_CURRENT_USER_ID);
+ }
+
+ @Test
+ public final void testCurrentUserVisibilityWhenNoDisplayIsAssigned_afterSwitch()
+ throws Exception {
+ startForegroundUser(USER_ID);
+
+ currentUserVisibilityWhenNoDisplayIsAssignedTest(USER_ID);
+ expectUserIsNotVisibleAtAll(INITIAL_CURRENT_USER_ID);
+ expectDisplayAssignedToUser(INITIAL_CURRENT_USER_ID, INVALID_DISPLAY);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/am/ActivityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/am/ActivityManagerServiceTest.java
index 0d6f326..e9dc082 100644
--- a/services/tests/servicestests/src/com/android/server/am/ActivityManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/ActivityManagerServiceTest.java
@@ -834,35 +834,35 @@
}
@Test
- public void testGetSecondaryDisplayIdsForStartingBackgroundUsers() {
+ public void testGetDisplayIdsForStartingBackgroundUsers() {
mInjector.secondaryDisplayIdsForStartingBackgroundUsers = new int[]{4, 8, 15, 16, 23, 42};
- int [] displayIds = mAms.getSecondaryDisplayIdsForStartingBackgroundUsers();
+ int [] displayIds = mAms.getDisplayIdsForStartingVisibleBackgroundUsers();
- assertWithMessage("mAms.getSecondaryDisplayIdsForStartingBackgroundUsers()")
+ assertWithMessage("mAms.getDisplayIdsForStartingVisibleBackgroundUsers()")
.that(displayIds).asList().containsExactly(4, 8, 15, 16, 23, 42);
}
@Test
- public void testStartUserInBackgroundOnSecondaryDisplay_invalidDisplay() {
+ public void testStartUserInBackgroundVisibleOnDisplay_invalidDisplay() {
mInjector.secondaryDisplayIdsForStartingBackgroundUsers = new int[]{4, 8, 15, 16, 23, 42};
assertThrows(IllegalArgumentException.class,
- () -> mAms.startUserInBackgroundOnSecondaryDisplay(USER_ID, 666));
+ () -> mAms.startUserInBackgroundVisibleOnDisplay(USER_ID, 666));
assertWithMessage("UserController.startUserOnSecondaryDisplay() calls")
.that(mInjector.usersStartedOnSecondaryDisplays).isEmpty();
}
@Test
- public void testStartUserInBackgroundOnSecondaryDisplay_validDisplay_failed() {
+ public void testStartUserInBackgroundVisibleOnDisplay_validDisplay_failed() {
mInjector.secondaryDisplayIdsForStartingBackgroundUsers = new int[]{ 4, 8, 15, 16, 23, 42 };
mInjector.returnValueForstartUserOnSecondaryDisplay = false;
- boolean started = mAms.startUserInBackgroundOnSecondaryDisplay(USER_ID, 42);
+ boolean started = mAms.startUserInBackgroundVisibleOnDisplay(USER_ID, 42);
Log.v(TAG, "Started: " + started);
- assertWithMessage("mAms.startUserInBackgroundOnSecondaryDisplay(%s, 42)", USER_ID)
+ assertWithMessage("mAms.startUserInBackgroundOnDisplay(%s, 42)", USER_ID)
.that(started).isFalse();
assertWithMessage("UserController.startUserOnSecondaryDisplay() calls")
.that(mInjector.usersStartedOnSecondaryDisplays)
@@ -870,16 +870,16 @@
}
@Test
- public void testStartUserInBackgroundOnSecondaryDisplay_validDisplay_success() {
+ public void testStartUserInBackgroundVisibleOnDisplay_validDisplay_success() {
mInjector.secondaryDisplayIdsForStartingBackgroundUsers = new int[]{ 4, 8, 15, 16, 23, 42 };
mInjector.returnValueForstartUserOnSecondaryDisplay = true;
- boolean started = mAms.startUserInBackgroundOnSecondaryDisplay(USER_ID, 42);
+ boolean started = mAms.startUserInBackgroundVisibleOnDisplay(USER_ID, 42);
Log.v(TAG, "Started: " + started);
- assertWithMessage("mAms.startUserInBackgroundOnSecondaryDisplay(%s, 42)", USER_ID)
+ assertWithMessage("mAms.startUserInBackgroundOnDisplay(%s, 42)", USER_ID)
.that(started).isTrue();
- assertWithMessage("UserController.startUserOnSecondaryDisplay() calls")
+ assertWithMessage("UserController.startUserOnDisplay() calls")
.that(mInjector.usersStartedOnSecondaryDisplays)
.containsExactly(new Pair<>(USER_ID, 42));
}
@@ -1004,12 +1004,12 @@
}
@Override
- public int[] getSecondaryDisplayIdsForStartingBackgroundUsers() {
+ public int[] getDisplayIdsForStartingVisibleBackgroundUsers() {
return secondaryDisplayIdsForStartingBackgroundUsers;
}
@Override
- public boolean startUserOnSecondaryDisplay(int userId, int displayId) {
+ public boolean startUserInBackgroundVisibleOnDisplay(int userId, int displayId) {
usersStartedOnSecondaryDisplays.add(new Pair<>(userId, displayId));
return returnValueForstartUserOnSecondaryDisplay;
}
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 0dfe664..b146c27 100644
--- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
@@ -243,11 +243,10 @@
}
@Test
- public void testStartUserOnSecondaryDisplay() {
- boolean started = mUserController.startUserOnSecondaryDisplay(TEST_USER_ID, 42);
+ public void testStartUserVisibleOnDisplay() {
+ boolean started = mUserController.startUserVisibleOnDisplay(TEST_USER_ID, 42);
- assertWithMessage("startUserOnSecondaryDisplay(%s, %s)", TEST_USER_ID, 42).that(started)
- .isTrue();
+ assertWithMessage("startUserOnDisplay(%s, %s)", TEST_USER_ID, 42).that(started).isTrue();
verifyUserAssignedToDisplay(TEST_USER_ID, 42);
verify(mInjector.getWindowManager(), never()).startFreezingScreen(anyInt(), anyInt());
diff --git a/services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java b/services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java
index ee9d59b..08a0878 100644
--- a/services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java
+++ b/services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java
@@ -75,7 +75,13 @@
}
@Override
- public int removeDevicesRoleForStrategy(int strategy, int role) {
+ public int removeDevicesRoleForStrategy(int strategy, int role,
+ @NonNull List<AudioDeviceAttributes> devices) {
+ return AudioSystem.AUDIO_STATUS_OK;
+ }
+
+ @Override
+ public int clearDevicesRoleForStrategy(int strategy, int role) {
return AudioSystem.AUDIO_STATUS_OK;
}
diff --git a/services/tests/servicestests/src/com/android/server/backup/utils/BackupManagerMonitorUtilsTest.java b/services/tests/servicestests/src/com/android/server/backup/utils/BackupManagerMonitorUtilsTest.java
index cadc890..87ade96 100644
--- a/services/tests/servicestests/src/com/android/server/backup/utils/BackupManagerMonitorUtilsTest.java
+++ b/services/tests/servicestests/src/com/android/server/backup/utils/BackupManagerMonitorUtilsTest.java
@@ -172,11 +172,30 @@
.when(agent)
.getLoggerResults(any());
- IBackupManagerMonitor result =
+ IBackupManagerMonitor monitor =
BackupManagerMonitorUtils.monitorAgentLoggingResults(
mMonitorMock, packageInfo, agent);
- assertThat(result).isEqualTo(mMonitorMock);
+ assertCorrectBundleSentToMonitor(monitor);
+ }
+
+ @Test
+ public void sendAgentLoggingResults_fillsBundleCorrectly() throws Exception {
+ PackageInfo packageInfo = new PackageInfo();
+ packageInfo.packageName = "test.package";
+ List<BackupRestoreEventLogger.DataTypeResult> loggingResults = new ArrayList<>();
+ loggingResults.add(new BackupRestoreEventLogger.DataTypeResult("testLoggingResult"));
+
+ IBackupManagerMonitor monitor = BackupManagerMonitorUtils.sendAgentLoggingResults(
+ mMonitorMock, packageInfo, loggingResults);
+
+ assertCorrectBundleSentToMonitor(monitor);
+ }
+
+ private void assertCorrectBundleSentToMonitor(IBackupManagerMonitor monitor) throws Exception {
+
+
+ assertThat(monitor).isEqualTo(mMonitorMock);
ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
verify(mMonitorMock).onEvent(bundleCaptor.capture());
Bundle eventBundle = bundleCaptor.getValue();
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 ac880ce..89eaa2c 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
@@ -35,7 +35,6 @@
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.argThat;
-import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@@ -97,8 +96,9 @@
import android.view.KeyEvent;
import android.view.WindowManager;
-import androidx.test.InstrumentationRegistry;
+import androidx.test.platform.app.InstrumentationRegistry;
+import com.android.compatibility.common.util.AdoptShellPermissionsRule;
import com.android.internal.app.BlockedAppStreamingActivity;
import com.android.server.LocalServices;
import com.android.server.input.InputManagerInternal;
@@ -107,6 +107,7 @@
import com.google.android.collect.Sets;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -177,17 +178,14 @@
.setAssociatedDisplayId(DISPLAY_ID_1)
.build();
private static final VirtualTouchscreenConfig TOUCHSCREEN_CONFIG =
- new VirtualTouchscreenConfig.Builder()
+ new VirtualTouchscreenConfig.Builder(WIDTH, HEIGHT)
.setVendorId(VENDOR_ID)
.setProductId(PRODUCT_ID)
.setInputDeviceName(DEVICE_NAME)
.setAssociatedDisplayId(DISPLAY_ID_1)
- .setWidthInPixels(WIDTH)
- .setHeightInPixels(HEIGHT)
.build();
private static final VirtualNavigationTouchpadConfig NAVIGATION_TOUCHPAD_CONFIG =
- new VirtualNavigationTouchpadConfig.Builder(
- /* touchpadHeight= */ HEIGHT, /* touchpadWidth= */ WIDTH)
+ new VirtualNavigationTouchpadConfig.Builder(WIDTH, HEIGHT)
.setVendorId(VENDOR_ID)
.setProductId(PRODUCT_ID)
.setInputDeviceName(DEVICE_NAME)
@@ -195,6 +193,11 @@
.build();
private static final String TEST_SITE = "http://test";
+ @Rule
+ public AdoptShellPermissionsRule mAdoptShellPermissionsRule = new AdoptShellPermissionsRule(
+ InstrumentationRegistry.getInstrumentation().getUiAutomation(),
+ Manifest.permission.CREATE_VIRTUAL_DEVICE);
+
private Context mContext;
private InputManagerMockHelper mInputManagerMockHelper;
private VirtualDeviceImpl mDeviceImpl;
@@ -304,10 +307,9 @@
LocalServices.removeServiceForTest(DisplayManagerInternal.class);
LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternalMock);
- mContext = Mockito.spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
+ mContext = Mockito.spy(new ContextWrapper(
+ InstrumentationRegistry.getInstrumentation().getTargetContext()));
doReturn(mContext).when(mContext).createContextAsUser(eq(Process.myUserHandle()), anyInt());
- doNothing().when(mContext).enforceCallingOrSelfPermission(
- eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString());
when(mContext.getSystemService(Context.DEVICE_POLICY_SERVICE)).thenReturn(
mDevicePolicyManagerMock);
@@ -726,48 +728,28 @@
@Test
public void createVirtualTouchscreen_zeroDisplayDimension_failsIllegalArgumentException() {
- mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1);
- final VirtualTouchscreenConfig zeroConfig =
- new VirtualTouchscreenConfig.Builder()
- .setVendorId(VENDOR_ID)
- .setProductId(PRODUCT_ID)
- .setInputDeviceName(DEVICE_NAME)
- .setAssociatedDisplayId(DISPLAY_ID_1)
- .setWidthInPixels(0)
- .setHeightInPixels(0)
- .build();
assertThrows(IllegalArgumentException.class,
- () -> mDeviceImpl.createVirtualTouchscreen(zeroConfig, BINDER));
+ () -> new VirtualTouchscreenConfig.Builder(
+ /* touchscrenWidth= */ 0, /* touchscreenHeight= */ 0));
}
@Test
public void createVirtualTouchscreen_negativeDisplayDimension_failsIllegalArgumentException() {
- mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1);
- final VirtualTouchscreenConfig negativeConfig =
- new VirtualTouchscreenConfig.Builder()
- .setVendorId(VENDOR_ID)
- .setProductId(PRODUCT_ID)
- .setInputDeviceName(DEVICE_NAME)
- .setAssociatedDisplayId(DISPLAY_ID_1)
- .setWidthInPixels(-100)
- .setHeightInPixels(-100)
- .build();
assertThrows(IllegalArgumentException.class,
- () -> mDeviceImpl.createVirtualTouchscreen(negativeConfig, BINDER));
-
+ () -> new VirtualTouchscreenConfig.Builder(
+ /* touchscrenWidth= */ -100, /* touchscreenHeight= */ -100));
}
@Test
public void createVirtualTouchscreen_positiveDisplayDimension_successful() {
mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1);
VirtualTouchscreenConfig positiveConfig =
- new VirtualTouchscreenConfig.Builder()
+ new VirtualTouchscreenConfig.Builder(
+ /* touchscrenWidth= */ 600, /* touchscreenHeight= */ 800)
.setVendorId(VENDOR_ID)
.setProductId(PRODUCT_ID)
.setInputDeviceName(DEVICE_NAME)
.setAssociatedDisplayId(DISPLAY_ID_1)
- .setWidthInPixels(600)
- .setHeightInPixels(800)
.build();
mDeviceImpl.createVirtualTouchscreen(positiveConfig, BINDER);
assertWithMessage(
@@ -784,36 +766,16 @@
@Test
public void createVirtualNavigationTouchpad_zeroDisplayDimension_failsWithException() {
- mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1);
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_1)
- .build();
- mDeviceImpl.createVirtualNavigationTouchpad(zeroConfig, BINDER);
- });
+ () -> new VirtualNavigationTouchpadConfig.Builder(
+ /* touchpadHeight= */ 0, /* touchpadWidth= */ 0));
}
@Test
public void createVirtualNavigationTouchpad_negativeDisplayDimension_failsWithException() {
- mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1);
assertThrows(IllegalArgumentException.class,
- () -> {
- final VirtualNavigationTouchpadConfig negativeConfig =
- new VirtualNavigationTouchpadConfig.Builder(
- /* touchpadHeight= */ -50, /* touchpadWidth= */ 50)
- .setVendorId(VENDOR_ID)
- .setProductId(PRODUCT_ID)
- .setInputDeviceName(DEVICE_NAME)
- .setAssociatedDisplayId(DISPLAY_ID_1)
- .build();
- mDeviceImpl.createVirtualNavigationTouchpad(negativeConfig, BINDER);
- });
+ () -> new VirtualNavigationTouchpadConfig.Builder(
+ /* touchpadHeight= */ -50, /* touchpadWidth= */ 50));
}
@Test
@@ -844,76 +806,76 @@
@Test
public void createVirtualDpad_noPermission_failsSecurityException() {
mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1);
- doCallRealMethod().when(mContext).enforceCallingOrSelfPermission(
- eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString());
- assertThrows(SecurityException.class,
- () -> mDeviceImpl.createVirtualDpad(DPAD_CONFIG, BINDER));
+ try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) {
+ assertThrows(SecurityException.class,
+ () -> mDeviceImpl.createVirtualDpad(DPAD_CONFIG, BINDER));
+ }
}
@Test
public void createVirtualKeyboard_noPermission_failsSecurityException() {
mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1);
- doCallRealMethod().when(mContext).enforceCallingOrSelfPermission(
- eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString());
- assertThrows(SecurityException.class,
- () -> mDeviceImpl.createVirtualKeyboard(KEYBOARD_CONFIG, BINDER));
+ try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) {
+ assertThrows(SecurityException.class,
+ () -> mDeviceImpl.createVirtualKeyboard(KEYBOARD_CONFIG, BINDER));
+ }
}
@Test
public void createVirtualMouse_noPermission_failsSecurityException() {
mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1);
- doCallRealMethod().when(mContext).enforceCallingOrSelfPermission(
- eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString());
- assertThrows(SecurityException.class,
- () -> mDeviceImpl.createVirtualMouse(MOUSE_CONFIG, BINDER));
+ try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) {
+ assertThrows(SecurityException.class,
+ () -> mDeviceImpl.createVirtualMouse(MOUSE_CONFIG, BINDER));
+ }
}
@Test
public void createVirtualTouchscreen_noPermission_failsSecurityException() {
mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1);
- doCallRealMethod().when(mContext).enforceCallingOrSelfPermission(
- eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString());
- assertThrows(SecurityException.class,
- () -> mDeviceImpl.createVirtualTouchscreen(TOUCHSCREEN_CONFIG, BINDER));
+ try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) {
+ assertThrows(SecurityException.class,
+ () -> mDeviceImpl.createVirtualTouchscreen(TOUCHSCREEN_CONFIG, BINDER));
+ }
}
@Test
public void createVirtualNavigationTouchpad_noPermission_failsSecurityException() {
mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1);
- doCallRealMethod().when(mContext).enforceCallingOrSelfPermission(
- eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString());
- assertThrows(SecurityException.class,
- () -> mDeviceImpl.createVirtualNavigationTouchpad(NAVIGATION_TOUCHPAD_CONFIG,
- BINDER));
+ try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) {
+ 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());
- assertThrows(
- SecurityException.class,
- () -> mDeviceImpl.createVirtualSensor(
- BINDER,
- new VirtualSensorConfig.Builder(
- Sensor.TYPE_ACCELEROMETER, DEVICE_NAME).build()));
+ try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) {
+ assertThrows(
+ SecurityException.class,
+ () -> mDeviceImpl.createVirtualSensor(
+ BINDER,
+ new VirtualSensorConfig.Builder(
+ Sensor.TYPE_ACCELEROMETER, DEVICE_NAME).build()));
+ }
}
@Test
public void onAudioSessionStarting_noPermission_failsSecurityException() {
mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1);
- doCallRealMethod().when(mContext).enforceCallingOrSelfPermission(
- eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString());
- assertThrows(SecurityException.class,
- () -> mDeviceImpl.onAudioSessionStarting(
- DISPLAY_ID_1, mRoutingCallback, mConfigChangedCallback));
+ try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) {
+ assertThrows(SecurityException.class,
+ () -> mDeviceImpl.onAudioSessionStarting(
+ DISPLAY_ID_1, mRoutingCallback, mConfigChangedCallback));
+ }
}
@Test
public void onAudioSessionEnded_noPermission_failsSecurityException() {
- doCallRealMethod().when(mContext).enforceCallingOrSelfPermission(
- eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString());
- assertThrows(SecurityException.class, () -> mDeviceImpl.onAudioSessionEnded());
+ try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) {
+ assertThrows(SecurityException.class, () -> mDeviceImpl.onAudioSessionEnded());
+ }
}
@Test
@@ -1593,4 +1555,18 @@
mVdms.addVirtualDevice(virtualDeviceImpl);
return virtualDeviceImpl;
}
+
+ /** Helper class to drop permissions temporarily and restore them at the end of a test. */
+ static final class DropShellPermissionsTemporarily implements AutoCloseable {
+ DropShellPermissionsTemporarily() {
+ InstrumentationRegistry.getInstrumentation().getUiAutomation()
+ .dropShellPermissionIdentity();
+ }
+
+ @Override
+ public void close() {
+ InstrumentationRegistry.getInstrumentation().getUiAutomation()
+ .adoptShellPermissionIdentity();
+ }
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
index bd4058a..69a0b87 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
@@ -40,6 +40,7 @@
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
@@ -73,6 +74,7 @@
import android.test.mock.MockContentResolver;
import android.util.Slog;
import android.util.SparseArray;
+import android.util.TypedValue;
import android.view.Display;
import android.view.SurfaceControl.RefreshRateRange;
import android.view.SurfaceControl.RefreshRateRanges;
@@ -102,6 +104,7 @@
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
+import org.mockito.stubbing.Answer;
import java.util.ArrayList;
import java.util.Arrays;
@@ -2293,6 +2296,61 @@
new int[]{20});
}
+ @Test
+ public void testSensorReloadOnDeviceSwitch() throws Exception {
+ // First, configure brightness zones or DMD won't register for sensor data.
+ final FakeDeviceConfig config = mInjector.getDeviceConfig();
+ config.setRefreshRateInHighZone(60);
+ config.setHighDisplayBrightnessThresholds(new int[] { 255 });
+ config.setHighAmbientBrightnessThresholds(new int[] { 8000 });
+
+ DisplayModeDirector director =
+ createDirectorFromRefreshRateArray(new float[] {60.f, 90.f}, 0);
+ setPeakRefreshRate(90 /*fps*/);
+ director.getSettingsObserver().setDefaultRefreshRate(90);
+ director.getBrightnessObserver().setDefaultDisplayState(Display.STATE_ON);
+
+ Sensor lightSensorOne = TestUtils.createSensor(Sensor.TYPE_LIGHT, Sensor.STRING_TYPE_LIGHT);
+ Sensor lightSensorTwo = TestUtils.createSensor(Sensor.TYPE_LIGHT, Sensor.STRING_TYPE_LIGHT);
+ SensorManager sensorManager = createMockSensorManager(lightSensorOne, lightSensorTwo);
+ when(sensorManager.getDefaultSensor(5)).thenReturn(lightSensorOne, lightSensorTwo);
+ director.start(sensorManager);
+ ArgumentCaptor<SensorEventListener> listenerCaptor =
+ ArgumentCaptor.forClass(SensorEventListener.class);
+ verify(sensorManager, Mockito.timeout(TimeUnit.SECONDS.toMillis(1)))
+ .registerListener(
+ listenerCaptor.capture(),
+ eq(lightSensorOne),
+ anyInt(),
+ any(Handler.class));
+
+ DisplayDeviceConfig ddcMock = mock(DisplayDeviceConfig.class);
+ when(ddcMock.getDefaultLowRefreshRate()).thenReturn(50);
+ when(ddcMock.getDefaultHighRefreshRate()).thenReturn(55);
+ when(ddcMock.getLowDisplayBrightnessThresholds()).thenReturn(new int[]{25});
+ when(ddcMock.getLowAmbientBrightnessThresholds()).thenReturn(new int[]{30});
+ when(ddcMock.getHighDisplayBrightnessThresholds()).thenReturn(new int[]{210});
+ when(ddcMock.getHighAmbientBrightnessThresholds()).thenReturn(new int[]{2100});
+
+ Resources resMock = mock(Resources.class);
+ when(resMock.getInteger(
+ com.android.internal.R.integer.config_displayWhiteBalanceBrightnessFilterHorizon))
+ .thenReturn(3);
+ ArgumentCaptor<TypedValue> valueArgumentCaptor = ArgumentCaptor.forClass(TypedValue.class);
+ doAnswer((Answer<Void>) invocation -> {
+ valueArgumentCaptor.getValue().type = 4;
+ valueArgumentCaptor.getValue().data = 13;
+ return null;
+ }).when(resMock).getValue(anyInt(), valueArgumentCaptor.capture(), eq(true));
+ when(mContext.getResources()).thenReturn(resMock);
+
+ director.defaultDisplayDeviceUpdated(ddcMock);
+
+ verify(sensorManager).unregisterListener(any(SensorEventListener.class));
+ verify(sensorManager).registerListener(any(SensorEventListener.class),
+ eq(lightSensorTwo), anyInt(), any(Handler.class));
+ }
+
private Temperature getSkinTemp(@Temperature.ThrottlingStatus int status) {
return new Temperature(30.0f, Temperature.TYPE_SKIN, "test_skin_temp", status);
}
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
index 5246107..5dd29fd 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
@@ -39,6 +39,7 @@
import android.hardware.hdmi.HdmiControlManager;
import android.hardware.hdmi.HdmiDeviceInfo;
import android.hardware.hdmi.HdmiPortInfo;
+import android.hardware.hdmi.IHdmiControlCallback;
import android.hardware.tv.cec.V1_0.SendMessageResult;
import android.media.AudioManager;
import android.os.Looper;
@@ -96,6 +97,7 @@
private int mTvPhysicalAddress;
private int mTvLogicalAddress;
private boolean mWokenUp;
+ private boolean mEarcBlocksArc;
private List<DeviceEventListener> mDeviceEventListeners = new ArrayList<>();
private class DeviceEventListener {
@@ -156,6 +158,11 @@
}
@Override
+ boolean isPowerStandbyOrTransient() {
+ return false;
+ }
+
+ @Override
AudioManager getAudioManager() {
return mAudioManager;
}
@@ -164,6 +171,11 @@
void invokeDeviceEventListeners(HdmiDeviceInfo device, int status) {
mDeviceEventListeners.add(new DeviceEventListener(device, status));
}
+
+ @Override
+ protected boolean earcBlocksArcConnection() {
+ return mEarcBlocksArc;
+ }
};
mHdmiControlService.setIoLooper(mMyLooper);
@@ -175,16 +187,18 @@
mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService));
HdmiPortInfo[] hdmiPortInfos = new HdmiPortInfo[2];
hdmiPortInfos[0] =
- new HdmiPortInfo(1, HdmiPortInfo.PORT_INPUT, 0x1000, true, false, false);
+ new HdmiPortInfo(1, HdmiPortInfo.PORT_INPUT, 0x1000, true, false, false, false);
hdmiPortInfos[1] =
- new HdmiPortInfo(2, HdmiPortInfo.PORT_INPUT, 0x2000, true, false, true);
+ new HdmiPortInfo(2, HdmiPortInfo.PORT_INPUT, 0x2000, true, false, true, true);
mNativeWrapper.setPortInfo(hdmiPortInfos);
mHdmiControlService.initService();
mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
mPowerManager = new FakePowerManagerWrapper(context);
mHdmiControlService.setPowerManager(mPowerManager);
mTvPhysicalAddress = 0x0000;
+ mEarcBlocksArc = false;
mNativeWrapper.setPhysicalAddress(mTvPhysicalAddress);
+ mHdmiControlService.setEarcEnabled(HdmiControlManager.EARC_FEATURE_DISABLED);
mTestLooper.dispatchAll();
mHdmiCecLocalDeviceTv = mHdmiControlService.tv();
mTvLogicalAddress = mHdmiCecLocalDeviceTv.getDeviceInfo().getLogicalAddress();
@@ -196,6 +210,20 @@
mNativeWrapper.clearResultMessages();
}
+ private static class TestCallback extends IHdmiControlCallback.Stub {
+ private final ArrayList<Integer> mCallbackResult = new ArrayList<Integer>();
+
+ @Override
+ public void onComplete(int result) {
+ mCallbackResult.add(result);
+ }
+
+ private int getResult() {
+ assertThat(mCallbackResult.size()).isEqualTo(1);
+ return mCallbackResult.get(0);
+ }
+ }
+
@Test
public void initialPowerStateIsStandby() {
assertThat(mHdmiCecLocalDeviceTv.getPowerStatus()).isEqualTo(
@@ -426,9 +454,10 @@
public void startArcAction_enable_portDoesNotSupportArc() {
// Emulate Audio device on port 0x1000 (does not support ARC)
mNativeWrapper.setPortConnectionStatus(1, true);
- HdmiCecMessage hdmiCecMessage = HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
- ADDR_AUDIO_SYSTEM, 0x1000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
- mNativeWrapper.onCecMessage(hdmiCecMessage);
+ HdmiCecMessage reportPhysicalAddress =
+ HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+ ADDR_AUDIO_SYSTEM, 0x1000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+ mNativeWrapper.onCecMessage(reportPhysicalAddress);
mHdmiCecLocalDeviceTv.startArcAction(true);
HdmiCecMessage requestArcInitiation = HdmiCecMessageBuilder.buildRequestArcInitiation(
@@ -445,9 +474,10 @@
public void startArcAction_disable_portDoesNotSupportArc() {
// Emulate Audio device on port 0x1000 (does not support ARC)
mNativeWrapper.setPortConnectionStatus(1, true);
- HdmiCecMessage hdmiCecMessage = HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
- ADDR_AUDIO_SYSTEM, 0x1000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
- mNativeWrapper.onCecMessage(hdmiCecMessage);
+ HdmiCecMessage reportPhysicalAddress =
+ HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+ ADDR_AUDIO_SYSTEM, 0x1000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+ mNativeWrapper.onCecMessage(reportPhysicalAddress);
mHdmiCecLocalDeviceTv.startArcAction(false);
HdmiCecMessage requestArcInitiation = HdmiCecMessageBuilder.buildRequestArcInitiation(
@@ -464,9 +494,10 @@
public void startArcAction_enable_portSupportsArc() {
// Emulate Audio device on port 0x2000 (supports ARC)
mNativeWrapper.setPortConnectionStatus(2, true);
- HdmiCecMessage hdmiCecMessage = HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
- ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
- mNativeWrapper.onCecMessage(hdmiCecMessage);
+ HdmiCecMessage reportPhysicalAddress =
+ HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+ ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+ mNativeWrapper.onCecMessage(reportPhysicalAddress);
mTestLooper.dispatchAll();
mHdmiCecLocalDeviceTv.startArcAction(true);
@@ -485,9 +516,10 @@
public void startArcAction_disable_portSupportsArc() {
// Emulate Audio device on port 0x2000 (supports ARC)
mNativeWrapper.setPortConnectionStatus(2, true);
- HdmiCecMessage hdmiCecMessage = HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
- ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
- mNativeWrapper.onCecMessage(hdmiCecMessage);
+ HdmiCecMessage reportPhysicalAddress =
+ HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+ ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+ mNativeWrapper.onCecMessage(reportPhysicalAddress);
mTestLooper.dispatchAll();
mHdmiCecLocalDeviceTv.startArcAction(false);
@@ -522,9 +554,10 @@
public void handleInitiateArc_portDoesNotSupportArc() {
// Emulate Audio device on port 0x1000 (does not support ARC)
mNativeWrapper.setPortConnectionStatus(1, true);
- HdmiCecMessage hdmiCecMessage = HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
- ADDR_AUDIO_SYSTEM, 0x1000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
- mNativeWrapper.onCecMessage(hdmiCecMessage);
+ HdmiCecMessage reportPhysicalAddress =
+ HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+ ADDR_AUDIO_SYSTEM, 0x1000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+ mNativeWrapper.onCecMessage(reportPhysicalAddress);
HdmiCecMessage requestArcInitiation = HdmiCecMessageBuilder.buildInitiateArc(
ADDR_AUDIO_SYSTEM,
@@ -544,9 +577,10 @@
public void handleInitiateArc_portSupportsArc() {
// Emulate Audio device on port 0x2000 (supports ARC)
mNativeWrapper.setPortConnectionStatus(2, true);
- HdmiCecMessage hdmiCecMessage = HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
- ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
- mNativeWrapper.onCecMessage(hdmiCecMessage);
+ HdmiCecMessage reportPhysicalAddress =
+ HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+ ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+ mNativeWrapper.onCecMessage(reportPhysicalAddress);
mTestLooper.dispatchAll();
HdmiCecMessage requestArcInitiation = HdmiCecMessageBuilder.buildInitiateArc(
@@ -575,6 +609,66 @@
}
@Test
+ public void handleTerminateArc_noAudioDevice() {
+ HdmiCecMessage terminateArc = HdmiCecMessageBuilder.buildTerminateArc(
+ ADDR_AUDIO_SYSTEM,
+ ADDR_TV);
+
+ mNativeWrapper.onCecMessage(terminateArc);
+ mTestLooper.dispatchAll();
+
+ HdmiCecMessage reportArcTerminated = HdmiCecMessageBuilder.buildReportArcTerminated(
+ ADDR_TV,
+ ADDR_AUDIO_SYSTEM);
+ assertThat(mNativeWrapper.getResultMessages()).contains(reportArcTerminated);
+ }
+
+ @Test
+ public void handleTerminateArc_portDoesNotSupportArc() {
+ // Emulate Audio device on port 0x1000 (does not support ARC)
+ mNativeWrapper.setPortConnectionStatus(1, true);
+ HdmiCecMessage reportPhysicalAddress =
+ HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+ ADDR_AUDIO_SYSTEM, 0x1000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+ mNativeWrapper.onCecMessage(reportPhysicalAddress);
+
+ HdmiCecMessage terminateArc = HdmiCecMessageBuilder.buildTerminateArc(
+ ADDR_AUDIO_SYSTEM,
+ ADDR_TV);
+
+ mNativeWrapper.onCecMessage(terminateArc);
+ mTestLooper.dispatchAll();
+
+ HdmiCecMessage reportArcTerminated = HdmiCecMessageBuilder.buildReportArcTerminated(
+ ADDR_TV,
+ ADDR_AUDIO_SYSTEM);
+ assertThat(mNativeWrapper.getResultMessages()).contains(reportArcTerminated);
+ }
+
+ @Test
+ public void handleTerminateArc_portSupportsArc() {
+ // Emulate Audio device on port 0x2000 (supports ARC)
+ mNativeWrapper.setPortConnectionStatus(2, true);
+ HdmiCecMessage reportPhysicalAddress =
+ HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+ ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+ mNativeWrapper.onCecMessage(reportPhysicalAddress);
+ mTestLooper.dispatchAll();
+
+ HdmiCecMessage terminateArc = HdmiCecMessageBuilder.buildTerminateArc(
+ ADDR_AUDIO_SYSTEM,
+ ADDR_TV);
+
+ mNativeWrapper.onCecMessage(terminateArc);
+ mTestLooper.dispatchAll();
+
+ HdmiCecMessage reportArcTerminated = HdmiCecMessageBuilder.buildReportArcTerminated(
+ ADDR_TV,
+ ADDR_AUDIO_SYSTEM);
+ assertThat(mNativeWrapper.getResultMessages()).contains(reportArcTerminated);
+ }
+
+ @Test
public void supportsRecordTvScreen() {
HdmiCecMessage recordTvScreen = HdmiCecMessage.build(ADDR_RECORDER_1, mTvLogicalAddress,
Constants.MESSAGE_RECORD_TV_SCREEN, HdmiCecMessage.EMPTY_PARAM);
@@ -595,9 +689,10 @@
HdmiControlManager.SYSTEM_AUDIO_CONTROL_ENABLED);
// Emulate Audio device on port 0x1000 (does not support ARC)
mNativeWrapper.setPortConnectionStatus(1, true);
- HdmiCecMessage hdmiCecMessage = HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
- ADDR_AUDIO_SYSTEM, 0x1000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
- mNativeWrapper.onCecMessage(hdmiCecMessage);
+ HdmiCecMessage reportPhysicalAddress =
+ HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+ ADDR_AUDIO_SYSTEM, 0x1000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+ mNativeWrapper.onCecMessage(reportPhysicalAddress);
mTestLooper.dispatchAll();
HdmiCecFeatureAction systemAudioAutoInitiationAction =
@@ -636,9 +731,9 @@
public void hotplugDetectionAction_discoversDeviceAfterMessageReceived() {
// Playback 1 sends a message before ACKing a poll
mNativeWrapper.setPollAddressResponse(ADDR_PLAYBACK_1, SendMessageResult.NACK);
- HdmiCecMessage hdmiCecMessage = HdmiCecMessageBuilder.buildActiveSource(
+ HdmiCecMessage activeSource = HdmiCecMessageBuilder.buildActiveSource(
ADDR_PLAYBACK_1, ADDR_TV);
- mNativeWrapper.onCecMessage(hdmiCecMessage);
+ mNativeWrapper.onCecMessage(activeSource);
mTestLooper.dispatchAll();
// Playback 1 begins ACKing polls, allowing detection by HotplugDetectionAction
@@ -812,12 +907,12 @@
mTestLooper.dispatchAll();
// <Feature Abort>[Not in correct mode] not sent
- HdmiCecMessage featureAbortMessage = HdmiCecMessageBuilder.buildFeatureAbortCommand(
+ HdmiCecMessage featureAbort = HdmiCecMessageBuilder.buildFeatureAbortCommand(
ADDR_TV,
ADDR_PLAYBACK_1,
Constants.MESSAGE_SET_AUDIO_VOLUME_LEVEL,
Constants.ABORT_NOT_IN_CORRECT_MODE);
- assertThat(mNativeWrapper.getResultMessages()).doesNotContain(featureAbortMessage);
+ assertThat(mNativeWrapper.getResultMessages()).doesNotContain(featureAbort);
// <Set Audio Volume Level> uses volume range [0, 100]; STREAM_MUSIC uses range [0, 25]
verify(mAudioManager).setStreamVolume(eq(AudioManager.STREAM_MUSIC), eq(5), anyInt());
@@ -838,12 +933,12 @@
mTestLooper.dispatchAll();
// <Feature Abort>[Not in correct mode] sent
- HdmiCecMessage featureAbortMessage = HdmiCecMessageBuilder.buildFeatureAbortCommand(
+ HdmiCecMessage featureAbort = HdmiCecMessageBuilder.buildFeatureAbortCommand(
ADDR_TV,
ADDR_PLAYBACK_1,
Constants.MESSAGE_SET_AUDIO_VOLUME_LEVEL,
Constants.ABORT_NOT_IN_CORRECT_MODE);
- assertThat(mNativeWrapper.getResultMessages()).contains(featureAbortMessage);
+ assertThat(mNativeWrapper.getResultMessages()).contains(featureAbort);
// AudioManager not notified of volume change
verify(mAudioManager, never()).setStreamVolume(eq(AudioManager.STREAM_MUSIC), anyInt(),
@@ -853,11 +948,11 @@
@Test
public void tvSendRequestArcTerminationOnSleep() {
// Emulate Audio device on port 0x2000 (supports ARC)
-
mNativeWrapper.setPortConnectionStatus(2, true);
- HdmiCecMessage hdmiCecMessage = HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
- ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
- mNativeWrapper.onCecMessage(hdmiCecMessage);
+ HdmiCecMessage reportPhysicalAddress =
+ HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+ ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+ mNativeWrapper.onCecMessage(reportPhysicalAddress);
mTestLooper.dispatchAll();
mHdmiCecLocalDeviceTv.startArcAction(true);
@@ -898,4 +993,560 @@
assertThat(mNativeWrapper.getResultMessages()).contains(requestArcTermination);
}
+ @Test
+ public void startArcAction_enable_earcBlocksArc() {
+ mHdmiControlService.setEarcEnabled(HdmiControlManager.EARC_FEATURE_ENABLED);
+ mTestLooper.dispatchAll();
+
+ mEarcBlocksArc = true;
+
+ // Emulate Audio device on port 0x2000 (supports ARC and eARC)
+ mNativeWrapper.setPortConnectionStatus(2, true);
+ HdmiCecMessage reportPhysicalAddress =
+ HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+ ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+ mNativeWrapper.onCecMessage(reportPhysicalAddress);
+ mTestLooper.dispatchAll();
+
+ mHdmiCecLocalDeviceTv.startArcAction(true);
+ mTestLooper.dispatchAll();
+ HdmiCecMessage requestArcInitiation = HdmiCecMessageBuilder.buildRequestArcInitiation(
+ ADDR_TV,
+ ADDR_AUDIO_SYSTEM);
+ HdmiCecMessage requestArcTermination = HdmiCecMessageBuilder.buildRequestArcTermination(
+ ADDR_TV,
+ ADDR_AUDIO_SYSTEM);
+ assertThat(mNativeWrapper.getResultMessages()).doesNotContain(requestArcInitiation);
+ assertThat(mNativeWrapper.getResultMessages()).doesNotContain(requestArcTermination);
+ }
+
+ @Test
+ public void startArcAction_enable_earcDoesNotBlockArc() {
+ mHdmiControlService.setEarcEnabled(HdmiControlManager.EARC_FEATURE_ENABLED);
+ mTestLooper.dispatchAll();
+
+ mEarcBlocksArc = false;
+
+ // Emulate Audio device on port 0x2000 (supports ARC and eARC)
+ mNativeWrapper.setPortConnectionStatus(2, true);
+ HdmiCecMessage reportPhysicalAddress =
+ HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+ ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+ mNativeWrapper.onCecMessage(reportPhysicalAddress);
+ mTestLooper.dispatchAll();
+
+ mHdmiCecLocalDeviceTv.startArcAction(true);
+ mTestLooper.dispatchAll();
+ HdmiCecMessage requestArcInitiation = HdmiCecMessageBuilder.buildRequestArcInitiation(
+ ADDR_TV,
+ ADDR_AUDIO_SYSTEM);
+ HdmiCecMessage requestArcTermination = HdmiCecMessageBuilder.buildRequestArcTermination(
+ ADDR_TV,
+ ADDR_AUDIO_SYSTEM);
+ assertThat(mNativeWrapper.getResultMessages()).contains(requestArcInitiation);
+ assertThat(mNativeWrapper.getResultMessages()).doesNotContain(requestArcTermination);
+ }
+
+ @Test
+ public void startArcAction_disable_earcBlocksArc() {
+ mHdmiControlService.setEarcEnabled(HdmiControlManager.EARC_FEATURE_ENABLED);
+ mTestLooper.dispatchAll();
+
+ mEarcBlocksArc = true;
+
+ // Emulate Audio device on port 0x2000 (supports ARC and eARC)
+ mNativeWrapper.setPortConnectionStatus(2, true);
+ HdmiCecMessage reportPhysicalAddress =
+ HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+ ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+ mNativeWrapper.onCecMessage(reportPhysicalAddress);
+ mTestLooper.dispatchAll();
+
+ mHdmiCecLocalDeviceTv.startArcAction(false);
+ mTestLooper.dispatchAll();
+ HdmiCecMessage requestArcInitiation = HdmiCecMessageBuilder.buildRequestArcInitiation(
+ ADDR_TV,
+ ADDR_AUDIO_SYSTEM);
+ HdmiCecMessage requestArcTermination = HdmiCecMessageBuilder.buildRequestArcTermination(
+ ADDR_TV,
+ ADDR_AUDIO_SYSTEM);
+ assertThat(mNativeWrapper.getResultMessages()).doesNotContain(requestArcInitiation);
+ assertThat(mNativeWrapper.getResultMessages()).contains(requestArcTermination);
+ }
+
+ @Test
+ public void handleInitiateArc_earcBlocksArc() {
+ mHdmiControlService.setEarcEnabled(HdmiControlManager.EARC_FEATURE_ENABLED);
+ mTestLooper.dispatchAll();
+
+ mEarcBlocksArc = true;
+
+ // Emulate Audio device on port 0x2000 (supports ARC and eARC)
+ mNativeWrapper.setPortConnectionStatus(2, true);
+ HdmiCecMessage reportPhysicalAddress =
+ HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+ ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+ mNativeWrapper.onCecMessage(reportPhysicalAddress);
+ mTestLooper.dispatchAll();
+
+ HdmiCecMessage requestArcInitiation = HdmiCecMessageBuilder.buildInitiateArc(
+ ADDR_AUDIO_SYSTEM,
+ ADDR_TV);
+
+ mNativeWrapper.onCecMessage(requestArcInitiation);
+ mTestLooper.dispatchAll();
+
+ HdmiCecMessage featureAbort = HdmiCecMessageBuilder.buildFeatureAbortCommand(
+ ADDR_TV,
+ ADDR_AUDIO_SYSTEM,
+ Constants.MESSAGE_INITIATE_ARC,
+ Constants.ABORT_NOT_IN_CORRECT_MODE);
+ assertThat(mNativeWrapper.getResultMessages()).contains(featureAbort);
+ }
+
+ @Test
+ public void handleInitiateArc_earcDoesNotBlockArc() {
+ mHdmiControlService.setEarcEnabled(HdmiControlManager.EARC_FEATURE_ENABLED);
+ mTestLooper.dispatchAll();
+
+ mEarcBlocksArc = false;
+
+ // Emulate Audio device on port 0x2000 (supports ARC and eARC)
+ mNativeWrapper.setPortConnectionStatus(2, true);
+ HdmiCecMessage reportPhysicalAddress =
+ HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+ ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+ mNativeWrapper.onCecMessage(reportPhysicalAddress);
+ mTestLooper.dispatchAll();
+
+ HdmiCecMessage requestArcInitiation = HdmiCecMessageBuilder.buildInitiateArc(
+ ADDR_AUDIO_SYSTEM,
+ ADDR_TV);
+
+ mNativeWrapper.onCecMessage(requestArcInitiation);
+ mTestLooper.dispatchAll();
+
+ HdmiCecMessage reportArcInitiated = HdmiCecMessageBuilder.buildReportArcInitiated(
+ ADDR_TV,
+ ADDR_AUDIO_SYSTEM);
+ // <Report ARC Initiated> should only be sent after SAD querying is done
+ assertThat(mNativeWrapper.getResultMessages()).doesNotContain(reportArcInitiated);
+
+ // Finish querying SADs
+ assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
+ mNativeWrapper.clearResultMessages();
+ mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
+ mTestLooper.dispatchAll();
+ assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
+ mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
+ mTestLooper.dispatchAll();
+
+ assertThat(mNativeWrapper.getResultMessages()).contains(reportArcInitiated);
+ }
+
+ @Test
+ public void handleTerminateArc_earcBlocksArc() {
+ mHdmiControlService.setEarcEnabled(HdmiControlManager.EARC_FEATURE_ENABLED);
+ mTestLooper.dispatchAll();
+
+ mEarcBlocksArc = true;
+
+ // Emulate Audio device on port 0x2000 (supports ARC and eARC)
+ mNativeWrapper.setPortConnectionStatus(2, true);
+ HdmiCecMessage reportPhysicalAddress =
+ HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+ ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+ mNativeWrapper.onCecMessage(reportPhysicalAddress);
+ mTestLooper.dispatchAll();
+
+ HdmiCecMessage terminateArc = HdmiCecMessageBuilder.buildTerminateArc(
+ ADDR_AUDIO_SYSTEM,
+ ADDR_TV);
+
+ mNativeWrapper.onCecMessage(terminateArc);
+ mTestLooper.dispatchAll();
+
+ HdmiCecMessage reportArcTerminated = HdmiCecMessageBuilder.buildReportArcTerminated(
+ ADDR_TV,
+ ADDR_AUDIO_SYSTEM);
+ assertThat(mNativeWrapper.getResultMessages()).contains(reportArcTerminated);
+ }
+
+ @Test
+ public void startArcAction_initiation_noAvr() {
+ TestCallback callback = new TestCallback();
+
+ mHdmiCecLocalDeviceTv.startArcAction(true, callback);
+ mTestLooper.dispatchAll();
+
+ assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
+ }
+
+ @Test
+ public void startArcAction_initiation_portNotConnected() {
+ // Emulate Audio device on port 0x2000 (supports ARC)
+ HdmiCecMessage reportPhysicalAddress =
+ HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+ ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+ mNativeWrapper.onCecMessage(reportPhysicalAddress);
+ mTestLooper.dispatchAll();
+ // Emulate port disconnect
+ mNativeWrapper.setPortConnectionStatus(2, false);
+
+ TestCallback callback = new TestCallback();
+
+ mHdmiCecLocalDeviceTv.startArcAction(true, callback);
+ mTestLooper.dispatchAll();
+
+ assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_INCORRECT_MODE);
+ }
+
+ @Test
+ public void startArcAction_initiation_portDoesNotSupportArc() {
+ // Emulate Audio device on port 0x1000 (Doesn´t support ARC)
+ mNativeWrapper.setPortConnectionStatus(1, true);
+ HdmiCecMessage reportPhysicalAddress =
+ HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+ ADDR_AUDIO_SYSTEM, 0x1000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+ mNativeWrapper.onCecMessage(reportPhysicalAddress);
+ mTestLooper.dispatchAll();
+
+ TestCallback callback = new TestCallback();
+
+ mHdmiCecLocalDeviceTv.startArcAction(true, callback);
+ mTestLooper.dispatchAll();
+
+ assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_INCORRECT_MODE);
+ }
+
+ @Test
+ public void startArcAction_initiation_indirectPhysicalAddress() {
+ // Emulate Audio device on port 0x2000 (Supports ARC)
+ mNativeWrapper.setPortConnectionStatus(2, true);
+ HdmiCecMessage reportPhysicalAddress =
+ HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+ ADDR_AUDIO_SYSTEM, 0x2320, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+ mNativeWrapper.onCecMessage(reportPhysicalAddress);
+ mTestLooper.dispatchAll();
+
+ TestCallback callback = new TestCallback();
+
+ mHdmiCecLocalDeviceTv.startArcAction(true, callback);
+ mTestLooper.dispatchAll();
+
+ assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_INCORRECT_MODE);
+ }
+
+ @Test
+ public void startArcAction_initiation_earcBlocksArc() {
+ // Emulate Audio device on port 0x2000 (Supports ARC)
+ mNativeWrapper.setPortConnectionStatus(2, true);
+ HdmiCecMessage reportPhysicalAddress =
+ HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+ ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+ mNativeWrapper.onCecMessage(reportPhysicalAddress);
+ mTestLooper.dispatchAll();
+
+ mEarcBlocksArc = true;
+
+ TestCallback callback = new TestCallback();
+
+ mHdmiCecLocalDeviceTv.startArcAction(true, callback);
+ mTestLooper.dispatchAll();
+
+ assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_INCORRECT_MODE);
+ }
+
+ @Test
+ public void startArcAction_initiation_messageNotAcked() {
+ // Emulate Audio device on port 0x2000 (Supports ARC)
+ mNativeWrapper.setPortConnectionStatus(2, true);
+ HdmiCecMessage reportPhysicalAddress =
+ HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+ ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+ mNativeWrapper.onCecMessage(reportPhysicalAddress);
+ mTestLooper.dispatchAll();
+
+ mNativeWrapper.setMessageSendResult(
+ Constants.MESSAGE_REQUEST_ARC_INITIATION, SendMessageResult.NACK);
+ mTestLooper.dispatchAll();
+
+ TestCallback callback = new TestCallback();
+
+ mHdmiCecLocalDeviceTv.startArcAction(true, callback);
+ mTestLooper.dispatchAll();
+
+ HdmiCecMessage requestArcInitiation = HdmiCecMessageBuilder.buildRequestArcInitiation(
+ ADDR_TV,
+ ADDR_AUDIO_SYSTEM);
+ assertThat(mNativeWrapper.getResultMessages()).contains(requestArcInitiation);
+ assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
+ }
+
+ @Test
+ public void startArcAction_initiation_timeout() {
+ // Emulate Audio device on port 0x2000 (Supports ARC)
+ mNativeWrapper.setPortConnectionStatus(2, true);
+ HdmiCecMessage reportPhysicalAddress =
+ HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+ ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+ mNativeWrapper.onCecMessage(reportPhysicalAddress);
+ mTestLooper.dispatchAll();
+
+ TestCallback callback = new TestCallback();
+
+ mHdmiCecLocalDeviceTv.startArcAction(true, callback);
+ mTestLooper.dispatchAll();
+
+ HdmiCecMessage requestArcInitiation = HdmiCecMessageBuilder.buildRequestArcInitiation(
+ ADDR_TV,
+ ADDR_AUDIO_SYSTEM);
+ assertThat(mNativeWrapper.getResultMessages()).contains(requestArcInitiation);
+ mTestLooper.moveTimeForward(TIMEOUT_MS);
+ mTestLooper.dispatchAll();
+ assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_TIMEOUT);
+ }
+
+ @Test
+ public void startArcAction_initiation_featureAbort() {
+ // Emulate Audio device on port 0x2000 (Supports ARC)
+ mNativeWrapper.setPortConnectionStatus(2, true);
+ HdmiCecMessage reportPhysicalAddress =
+ HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+ ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+ mNativeWrapper.onCecMessage(reportPhysicalAddress);
+ mTestLooper.dispatchAll();
+
+ TestCallback callback = new TestCallback();
+
+ mHdmiCecLocalDeviceTv.startArcAction(true, callback);
+ mTestLooper.dispatchAll();
+
+ HdmiCecMessage requestArcInitiation = HdmiCecMessageBuilder.buildRequestArcInitiation(
+ ADDR_TV,
+ ADDR_AUDIO_SYSTEM);
+ assertThat(mNativeWrapper.getResultMessages()).contains(requestArcInitiation);
+
+ HdmiCecMessage featureAbort = HdmiCecMessageBuilder.buildFeatureAbortCommand(
+ ADDR_AUDIO_SYSTEM,
+ ADDR_TV,
+ Constants.MESSAGE_REQUEST_ARC_INITIATION,
+ Constants.ABORT_NOT_IN_CORRECT_MODE);
+ mNativeWrapper.onCecMessage(featureAbort);
+ mTestLooper.dispatchAll();
+
+ assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
+ }
+
+ @Test
+ public void startArcAction_initiation_success() {
+ // Emulate Audio device on port 0x2000 (Supports ARC)
+ mNativeWrapper.setPortConnectionStatus(2, true);
+ HdmiCecMessage reportPhysicalAddress =
+ HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+ ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+ mNativeWrapper.onCecMessage(reportPhysicalAddress);
+ mTestLooper.dispatchAll();
+
+ TestCallback callback = new TestCallback();
+
+ mHdmiCecLocalDeviceTv.startArcAction(true, callback);
+ mTestLooper.dispatchAll();
+
+ HdmiCecMessage requestArcInitiation = HdmiCecMessageBuilder.buildRequestArcInitiation(
+ ADDR_TV,
+ ADDR_AUDIO_SYSTEM);
+ assertThat(mNativeWrapper.getResultMessages()).contains(requestArcInitiation);
+
+ HdmiCecMessage initiateArc = HdmiCecMessageBuilder.buildInitiateArc(
+ ADDR_AUDIO_SYSTEM,
+ ADDR_TV);
+ mNativeWrapper.onCecMessage(initiateArc);
+ mTestLooper.dispatchAll();
+
+ assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_SUCCESS);
+ }
+
+ @Test
+ public void startArcAction_termination_noAvr() {
+ TestCallback callback = new TestCallback();
+
+ mHdmiCecLocalDeviceTv.startArcAction(false, callback);
+ mTestLooper.dispatchAll();
+
+ assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
+ }
+
+ @Test
+ public void startArcAction_termination_portDoesNotSupportArc() {
+ // Emulate Audio device on port 0x1000 (Doesn´t support ARC)
+ mNativeWrapper.setPortConnectionStatus(1, true);
+ HdmiCecMessage reportPhysicalAddress =
+ HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+ ADDR_AUDIO_SYSTEM, 0x1000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+ mNativeWrapper.onCecMessage(reportPhysicalAddress);
+ mTestLooper.dispatchAll();
+
+ TestCallback callback = new TestCallback();
+
+ mHdmiCecLocalDeviceTv.startArcAction(false, callback);
+ mTestLooper.dispatchAll();
+
+ assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_INCORRECT_MODE);
+ }
+
+ @Test
+ public void startArcAction_termination_messageNotAcked() {
+ // Emulate Audio device on port 0x2000 (Supports ARC)
+ mNativeWrapper.setPortConnectionStatus(2, true);
+ HdmiCecMessage reportPhysicalAddress =
+ HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+ ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+ mNativeWrapper.onCecMessage(reportPhysicalAddress);
+ mTestLooper.dispatchAll();
+
+ mNativeWrapper.setMessageSendResult(
+ Constants.MESSAGE_REQUEST_ARC_TERMINATION, SendMessageResult.NACK);
+ mTestLooper.dispatchAll();
+
+ TestCallback callback = new TestCallback();
+
+ mHdmiCecLocalDeviceTv.startArcAction(false, callback);
+ mTestLooper.dispatchAll();
+
+ HdmiCecMessage requestArcTermination = HdmiCecMessageBuilder.buildRequestArcTermination(
+ ADDR_TV,
+ ADDR_AUDIO_SYSTEM);
+ assertThat(mNativeWrapper.getResultMessages()).contains(requestArcTermination);
+ assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
+ }
+
+ @Test
+ public void startArcAction_termination_timeout() {
+ // Emulate Audio device on port 0x2000 (Supports ARC)
+ mNativeWrapper.setPortConnectionStatus(2, true);
+ HdmiCecMessage reportPhysicalAddress =
+ HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+ ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+ mNativeWrapper.onCecMessage(reportPhysicalAddress);
+ mTestLooper.dispatchAll();
+
+ TestCallback callback = new TestCallback();
+
+ mHdmiCecLocalDeviceTv.startArcAction(false, callback);
+ mTestLooper.dispatchAll();
+
+ HdmiCecMessage requestArcTermination = HdmiCecMessageBuilder.buildRequestArcTermination(
+ ADDR_TV,
+ ADDR_AUDIO_SYSTEM);
+ assertThat(mNativeWrapper.getResultMessages()).contains(requestArcTermination);
+ mTestLooper.moveTimeForward(TIMEOUT_MS);
+ mTestLooper.dispatchAll();
+ assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_TIMEOUT);
+ }
+
+ @Test
+ public void startArcAction_termination_featureAbort() {
+ // Emulate Audio device on port 0x2000 (Supports ARC)
+ mNativeWrapper.setPortConnectionStatus(2, true);
+ HdmiCecMessage reportPhysicalAddress =
+ HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+ ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+ mNativeWrapper.onCecMessage(reportPhysicalAddress);
+ mTestLooper.dispatchAll();
+
+ TestCallback callback = new TestCallback();
+
+ mHdmiCecLocalDeviceTv.startArcAction(false, callback);
+ mTestLooper.dispatchAll();
+
+ HdmiCecMessage requestArcTermination = HdmiCecMessageBuilder.buildRequestArcTermination(
+ ADDR_TV,
+ ADDR_AUDIO_SYSTEM);
+ assertThat(mNativeWrapper.getResultMessages()).contains(requestArcTermination);
+
+ HdmiCecMessage featureAbort = HdmiCecMessageBuilder.buildFeatureAbortCommand(
+ ADDR_AUDIO_SYSTEM,
+ ADDR_TV,
+ Constants.MESSAGE_REQUEST_ARC_TERMINATION,
+ Constants.ABORT_NOT_IN_CORRECT_MODE);
+ mNativeWrapper.onCecMessage(featureAbort);
+ mTestLooper.dispatchAll();
+
+ assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
+ }
+
+ @Test
+ public void startArcAction_termination_success() {
+ // Emulate Audio device on port 0x2000 (Supports ARC)
+ mNativeWrapper.setPortConnectionStatus(2, true);
+ HdmiCecMessage reportPhysicalAddress =
+ HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+ ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+ mNativeWrapper.onCecMessage(reportPhysicalAddress);
+ mTestLooper.dispatchAll();
+
+ TestCallback callback = new TestCallback();
+
+ mHdmiCecLocalDeviceTv.startArcAction(false, callback);
+ mTestLooper.dispatchAll();
+
+ HdmiCecMessage requestArcTermination = HdmiCecMessageBuilder.buildRequestArcTermination(
+ ADDR_TV,
+ ADDR_AUDIO_SYSTEM);
+ assertThat(mNativeWrapper.getResultMessages()).contains(requestArcTermination);
+
+ HdmiCecMessage terminateArc = HdmiCecMessageBuilder.buildTerminateArc(
+ ADDR_AUDIO_SYSTEM,
+ ADDR_TV);
+ mNativeWrapper.onCecMessage(terminateArc);
+ mTestLooper.dispatchAll();
+
+ assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_SUCCESS);
+ }
+
+ @Test
+ public void enableEarc_terminateArc() {
+ // Emulate Audio device on port 0x2000 (supports ARC and eARC)
+ mNativeWrapper.setPortConnectionStatus(2, true);
+ HdmiCecMessage reportPhysicalAddress =
+ HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+ ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+ mNativeWrapper.onCecMessage(reportPhysicalAddress);
+ mTestLooper.dispatchAll();
+
+ HdmiCecMessage initiateArc = HdmiCecMessageBuilder.buildInitiateArc(
+ ADDR_AUDIO_SYSTEM,
+ ADDR_TV);
+
+ mNativeWrapper.onCecMessage(initiateArc);
+ mTestLooper.dispatchAll();
+
+ HdmiCecMessage reportArcInitiated = HdmiCecMessageBuilder.buildReportArcInitiated(
+ ADDR_TV,
+ ADDR_AUDIO_SYSTEM);
+ // <Report ARC Initiated> should only be sent after SAD querying is done
+ assertThat(mNativeWrapper.getResultMessages()).doesNotContain(reportArcInitiated);
+
+ // Finish querying SADs
+ assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
+ mNativeWrapper.clearResultMessages();
+ mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
+ mTestLooper.dispatchAll();
+ assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
+ mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
+ mTestLooper.dispatchAll();
+
+ assertThat(mNativeWrapper.getResultMessages()).contains(reportArcInitiated);
+ mNativeWrapper.clearResultMessages();
+
+ mHdmiControlService.setEarcEnabled(HdmiControlManager.EARC_FEATURE_ENABLED);
+ mTestLooper.dispatchAll();
+
+ HdmiCecMessage requestArcTermination = HdmiCecMessageBuilder.buildRequestArcTermination(
+ ADDR_TV,
+ ADDR_AUDIO_SYSTEM);
+
+ assertThat(mNativeWrapper.getResultMessages()).contains(requestArcTermination);
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecNetworkTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecNetworkTest.java
index b8a1ba3..a82a79f 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecNetworkTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecNetworkTest.java
@@ -605,4 +605,36 @@
assertThat(mHdmiCecNetwork.getSafeCecDevicesLocked()).hasSize(1);
}
+
+ @Test
+ public void disableCec_clearCecLocalDevices() {
+ mHdmiCecNetwork.clearLocalDevices();
+ mHdmiCecNetwork.addLocalDevice(HdmiDeviceInfo.DEVICE_TV,
+ new HdmiCecLocalDeviceTv(mHdmiControlService));
+
+ assertThat(mHdmiCecNetwork.getLocalDeviceList()).hasSize(1);
+ assertThat(mHdmiCecNetwork.getLocalDeviceList().get(0)).isInstanceOf(
+ HdmiCecLocalDeviceTv.class);
+ mHdmiControlService.setCecEnabled(HdmiControlManager.HDMI_CEC_CONTROL_DISABLED);
+ mTestLooper.dispatchAll();
+
+ assertThat(mHdmiCecNetwork.getLocalDeviceList()).hasSize(0);
+ }
+
+ @Test
+ public void disableEarc_doNotClearCecLocalDevices() {
+ mHdmiCecNetwork.clearLocalDevices();
+ mHdmiCecNetwork.addLocalDevice(HdmiDeviceInfo.DEVICE_TV,
+ new HdmiCecLocalDeviceTv(mHdmiControlService));
+
+ assertThat(mHdmiCecNetwork.getLocalDeviceList()).hasSize(1);
+ assertThat(mHdmiCecNetwork.getLocalDeviceList().get(0)).isInstanceOf(
+ HdmiCecLocalDeviceTv.class);
+ mHdmiControlService.setEarcEnabled(HdmiControlManager.EARC_FEATURE_DISABLED);
+ mTestLooper.dispatchAll();
+
+ assertThat(mHdmiCecNetwork.getLocalDeviceList()).hasSize(1);
+ assertThat(mHdmiCecNetwork.getLocalDeviceList().get(0)).isInstanceOf(
+ HdmiCecLocalDeviceTv.class);
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
index 49a0a9a..aa49a62 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
@@ -22,6 +22,7 @@
import static com.android.server.SystemService.PHASE_BOOT_COMPLETED;
import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
+import static com.android.server.hdmi.HdmiControlService.WAKE_UP_SCREEN_ON;
import static com.google.common.truth.Truth.assertThat;
@@ -29,7 +30,11 @@
import static junit.framework.Assert.assertTrue;
import static junit.framework.TestCase.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
@@ -127,19 +132,20 @@
mLocalDevices.add(mPlaybackDeviceSpy);
mHdmiPortInfo = new HdmiPortInfo[4];
mHdmiPortInfo[0] =
- new HdmiPortInfo(1, HdmiPortInfo.PORT_INPUT, 0x2100, true, false, false);
+ new HdmiPortInfo(1, HdmiPortInfo.PORT_INPUT, 0x2100, true, false, false, false);
mHdmiPortInfo[1] =
- new HdmiPortInfo(2, HdmiPortInfo.PORT_INPUT, 0x2200, true, false, false);
+ new HdmiPortInfo(2, HdmiPortInfo.PORT_INPUT, 0x2200, true, false, false, false);
mHdmiPortInfo[2] =
- new HdmiPortInfo(3, HdmiPortInfo.PORT_INPUT, 0x2000, true, false, false);
+ new HdmiPortInfo(3, HdmiPortInfo.PORT_INPUT, 0x2000, true, false, true, true);
mHdmiPortInfo[3] =
- new HdmiPortInfo(4, HdmiPortInfo.PORT_INPUT, 0x3000, true, false, false);
+ new HdmiPortInfo(4, HdmiPortInfo.PORT_INPUT, 0x3000, true, false, false, false);
mNativeWrapper.setPortInfo(mHdmiPortInfo);
mHdmiControlServiceSpy.initService();
mPowerManager = new FakePowerManagerWrapper(mContextSpy);
mHdmiControlServiceSpy.setPowerManager(mPowerManager);
mHdmiControlServiceSpy.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
mHdmiControlServiceSpy.setAudioManager(mAudioManager);
+ mHdmiControlServiceSpy.setEarcSupported(true);
mTestLooper.dispatchAll();
}
@@ -1082,6 +1088,259 @@
assertThat(mHdmiControlServiceSpy.audioSystem()).isNull();
}
+ @Test
+ public void disableEarc_clearEarcLocalDevice() {
+ mHdmiControlServiceSpy.clearEarcLocalDevice();
+ mHdmiControlServiceSpy.addEarcLocalDevice(
+ new HdmiEarcLocalDeviceTx(mHdmiControlServiceSpy));
+ assertThat(mHdmiControlServiceSpy.getEarcLocalDevice()).isNotNull();
+
+ mHdmiControlServiceSpy.setEarcEnabled(HdmiControlManager.EARC_FEATURE_DISABLED);
+ mTestLooper.dispatchAll();
+ assertThat(mHdmiControlServiceSpy.getEarcLocalDevice()).isNull();
+ }
+
+ @Test
+ public void disableCec_doNotClearEarcLocalDevice() {
+ mHdmiControlServiceSpy.clearEarcLocalDevice();
+ mHdmiControlServiceSpy.addEarcLocalDevice(
+ new HdmiEarcLocalDeviceTx(mHdmiControlServiceSpy));
+ assertThat(mHdmiControlServiceSpy.getEarcLocalDevice()).isNotNull();
+
+ mHdmiControlServiceSpy.setCecEnabled(HdmiControlManager.HDMI_CEC_CONTROL_DISABLED);
+ mTestLooper.dispatchAll();
+ assertThat(mHdmiControlServiceSpy.getEarcLocalDevice()).isNotNull();
+ }
+
+ @Test
+ public void enableCec_initializeCecLocalDevices() {
+ Mockito.clearInvocations(mHdmiControlServiceSpy);
+ mHdmiControlServiceSpy.setCecEnabled(HdmiControlManager.HDMI_CEC_CONTROL_DISABLED);
+ mTestLooper.dispatchAll();
+ mHdmiControlServiceSpy.setCecEnabled(HdmiControlManager.HDMI_CEC_CONTROL_ENABLED);
+ mTestLooper.dispatchAll();
+ verify(mHdmiControlServiceSpy, times(1)).initializeCecLocalDevices(anyInt());
+ verify(mHdmiControlServiceSpy, times(0)).initializeEarcLocalDevice(anyInt());
+ }
+
+ @Test
+ public void enableEarc_initializeEarcLocalDevices() {
+ Mockito.clearInvocations(mHdmiControlServiceSpy);
+ mHdmiControlServiceSpy.setEarcEnabled(HdmiControlManager.EARC_FEATURE_DISABLED);
+ mTestLooper.dispatchAll();
+ mHdmiControlServiceSpy.setEarcEnabled(HdmiControlManager.EARC_FEATURE_ENABLED);
+ mTestLooper.dispatchAll();
+ verify(mHdmiControlServiceSpy, times(0)).initializeCecLocalDevices(anyInt());
+ verify(mHdmiControlServiceSpy, times(1)).initializeEarcLocalDevice(anyInt());
+ }
+
+ @Test
+ public void disableCec_DoNotInformHalAboutEarc() {
+ mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue(
+ HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_ENABLED,
+ HdmiControlManager.HDMI_CEC_CONTROL_ENABLED);
+ mTestLooper.dispatchAll();
+ Mockito.clearInvocations(mHdmiControlServiceSpy);
+ mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue(
+ HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_ENABLED,
+ HdmiControlManager.HDMI_CEC_CONTROL_DISABLED);
+ mTestLooper.dispatchAll();
+ verify(mHdmiControlServiceSpy, times(0)).setEarcEnabledInHal(anyBoolean(), anyBoolean());
+ }
+
+ @Test
+ public void disableEarc_informHalAboutEarc() {
+ mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue(
+ HdmiControlManager.SETTING_NAME_EARC_ENABLED,
+ HdmiControlManager.EARC_FEATURE_ENABLED);
+ mTestLooper.dispatchAll();
+ Mockito.clearInvocations(mHdmiControlServiceSpy);
+ mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue(
+ HdmiControlManager.SETTING_NAME_EARC_ENABLED,
+ HdmiControlManager.EARC_FEATURE_DISABLED);
+ mTestLooper.dispatchAll();
+ verify(mHdmiControlServiceSpy, times(1)).setEarcEnabledInHal(false, false);
+ verify(mHdmiControlServiceSpy, times(0)).setEarcEnabledInHal(eq(true), anyBoolean());
+ }
+
+ @Test
+ public void enableCec_DoNotInformHalAboutEarc() {
+ mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue(
+ HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_ENABLED,
+ HdmiControlManager.HDMI_CEC_CONTROL_DISABLED);
+ mTestLooper.dispatchAll();
+ Mockito.clearInvocations(mHdmiControlServiceSpy);
+ mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue(
+ HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_ENABLED,
+ HdmiControlManager.HDMI_CEC_CONTROL_ENABLED);
+ mTestLooper.dispatchAll();
+ verify(mHdmiControlServiceSpy, times(0)).setEarcEnabledInHal(anyBoolean(), anyBoolean());
+ }
+
+ @Test
+ public void enableEarc_informHalAboutEarc() {
+ mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue(
+ HdmiControlManager.SETTING_NAME_EARC_ENABLED,
+ HdmiControlManager.EARC_FEATURE_DISABLED);
+ mTestLooper.dispatchAll();
+ Mockito.clearInvocations(mHdmiControlServiceSpy);
+ mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue(
+ HdmiControlManager.SETTING_NAME_EARC_ENABLED,
+ HdmiControlManager.EARC_FEATURE_ENABLED);
+ mTestLooper.dispatchAll();
+ verify(mHdmiControlServiceSpy, times(1)).setEarcEnabledInHal(true, true);
+ verify(mHdmiControlServiceSpy, times(0)).setEarcEnabledInHal(eq(false), anyBoolean());
+ }
+
+ @Test
+ public void bootWithEarcEnabled_informHalAboutEarc() {
+ mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue(
+ HdmiControlManager.SETTING_NAME_EARC_ENABLED,
+ HdmiControlManager.EARC_FEATURE_ENABLED);
+ mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue(
+ HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_ENABLED,
+ HdmiControlManager.HDMI_CEC_CONTROL_DISABLED);
+ mTestLooper.dispatchAll();
+ Mockito.clearInvocations(mHdmiControlServiceSpy);
+ mHdmiControlServiceSpy.initService();
+ mTestLooper.dispatchAll();
+ verify(mHdmiControlServiceSpy, times(1)).setEarcEnabledInHal(true, false);
+ verify(mHdmiControlServiceSpy, times(0)).setEarcEnabledInHal(eq(false), anyBoolean());
+ }
+
+ @Test
+ public void bootWithEarcDisabled_informHalAboutEarc() {
+ mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue(
+ HdmiControlManager.SETTING_NAME_EARC_ENABLED,
+ HdmiControlManager.EARC_FEATURE_DISABLED);
+ mTestLooper.dispatchAll();
+ Mockito.clearInvocations(mHdmiControlServiceSpy);
+ mHdmiControlServiceSpy.initService();
+ mTestLooper.dispatchAll();
+ verify(mHdmiControlServiceSpy, times(1)).setEarcEnabledInHal(false, false);
+ verify(mHdmiControlServiceSpy, times(0)).setEarcEnabledInHal(eq(true), anyBoolean());
+ }
+
+ @Test
+ public void wakeUpWithEarcEnabled_informHalAboutEarc() {
+ mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue(
+ HdmiControlManager.SETTING_NAME_EARC_ENABLED,
+ HdmiControlManager.EARC_FEATURE_ENABLED);
+ mTestLooper.dispatchAll();
+ Mockito.clearInvocations(mHdmiControlServiceSpy);
+ mHdmiControlServiceSpy.onWakeUp(WAKE_UP_SCREEN_ON);
+ mTestLooper.dispatchAll();
+ verify(mHdmiControlServiceSpy, times(1)).setEarcEnabledInHal(true, false);
+ verify(mHdmiControlServiceSpy, times(0)).setEarcEnabledInHal(eq(false), anyBoolean());
+ }
+
+ @Test
+ public void wakeUpWithEarcDisabled_informHalAboutEarc() {
+ mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue(
+ HdmiControlManager.SETTING_NAME_EARC_ENABLED,
+ HdmiControlManager.EARC_FEATURE_DISABLED);
+ mTestLooper.dispatchAll();
+ Mockito.clearInvocations(mHdmiControlServiceSpy);
+ mHdmiControlServiceSpy.onWakeUp(WAKE_UP_SCREEN_ON);
+ mTestLooper.dispatchAll();
+ verify(mHdmiControlServiceSpy, times(1)).setEarcEnabledInHal(false, false);
+ verify(mHdmiControlServiceSpy, times(0)).setEarcEnabledInHal(eq(true), anyBoolean());
+ }
+
+ @Test
+ public void earcIdle_blocksArcConnection() {
+ mHdmiControlServiceSpy.clearEarcLocalDevice();
+ HdmiEarcLocalDeviceTx localDeviceTx = new HdmiEarcLocalDeviceTx(mHdmiControlServiceSpy);
+ localDeviceTx.handleEarcStateChange(Constants.HDMI_EARC_STATUS_IDLE);
+ mHdmiControlServiceSpy.addEarcLocalDevice(localDeviceTx);
+ assertThat(mHdmiControlServiceSpy.earcBlocksArcConnection()).isTrue();
+ }
+
+ @Test
+ public void earcPending_blocksArcConnection() {
+ mHdmiControlServiceSpy.clearEarcLocalDevice();
+ HdmiEarcLocalDeviceTx localDeviceTx = new HdmiEarcLocalDeviceTx(mHdmiControlServiceSpy);
+ localDeviceTx.handleEarcStateChange(Constants.HDMI_EARC_STATUS_EARC_PENDING);
+ mHdmiControlServiceSpy.addEarcLocalDevice(localDeviceTx);
+ assertThat(mHdmiControlServiceSpy.earcBlocksArcConnection()).isTrue();
+ }
+
+ @Test
+ public void earcEnabled_blocksArcConnection() {
+ mHdmiControlServiceSpy.clearEarcLocalDevice();
+ HdmiEarcLocalDeviceTx localDeviceTx = new HdmiEarcLocalDeviceTx(mHdmiControlServiceSpy);
+ localDeviceTx.handleEarcStateChange(Constants.HDMI_EARC_STATUS_EARC_CONNECTED);
+ mHdmiControlServiceSpy.addEarcLocalDevice(localDeviceTx);
+ assertThat(mHdmiControlServiceSpy.earcBlocksArcConnection()).isTrue();
+ }
+
+ @Test
+ public void arcPending_doesNotBlockArcConnection() {
+ mHdmiControlServiceSpy.clearEarcLocalDevice();
+ HdmiEarcLocalDeviceTx localDeviceTx = new HdmiEarcLocalDeviceTx(mHdmiControlServiceSpy);
+ localDeviceTx.handleEarcStateChange(Constants.HDMI_EARC_STATUS_ARC_PENDING);
+ mHdmiControlServiceSpy.addEarcLocalDevice(localDeviceTx);
+ assertThat(mHdmiControlServiceSpy.earcBlocksArcConnection()).isFalse();
+ }
+
+ @Test
+ public void earcStatusBecomesIdle_terminateArc() {
+ mHdmiControlServiceSpy.clearEarcLocalDevice();
+ HdmiEarcLocalDeviceTx localDeviceTx = new HdmiEarcLocalDeviceTx(mHdmiControlServiceSpy);
+ mHdmiControlServiceSpy.addEarcLocalDevice(localDeviceTx);
+ localDeviceTx.handleEarcStateChange(Constants.HDMI_EARC_STATUS_IDLE);
+ verify(mHdmiControlServiceSpy, times(1)).startArcAction(eq(false), any());
+ }
+
+ @Test
+ public void earcStatusBecomesEnabled_doNothing() {
+ mHdmiControlServiceSpy.clearEarcLocalDevice();
+ HdmiEarcLocalDeviceTx localDeviceTx = new HdmiEarcLocalDeviceTx(mHdmiControlServiceSpy);
+ mHdmiControlServiceSpy.addEarcLocalDevice(localDeviceTx);
+ localDeviceTx.handleEarcStateChange(Constants.HDMI_EARC_STATUS_EARC_CONNECTED);
+ verify(mHdmiControlServiceSpy, times(0)).startArcAction(anyBoolean(), any());
+ }
+
+ @Test
+ public void earcStatusBecomesPending_doNothing() {
+ mHdmiControlServiceSpy.clearEarcLocalDevice();
+ HdmiEarcLocalDeviceTx localDeviceTx = new HdmiEarcLocalDeviceTx(mHdmiControlServiceSpy);
+ mHdmiControlServiceSpy.addEarcLocalDevice(localDeviceTx);
+ localDeviceTx.handleEarcStateChange(Constants.HDMI_EARC_STATUS_EARC_PENDING);
+ verify(mHdmiControlServiceSpy, times(0)).startArcAction(anyBoolean(), any());
+ }
+
+ @Test
+ public void earcStatusBecomesNotEnabled_initiateArc() {
+ mHdmiControlServiceSpy.clearEarcLocalDevice();
+ HdmiEarcLocalDeviceTx localDeviceTx = new HdmiEarcLocalDeviceTx(mHdmiControlServiceSpy);
+ mHdmiControlServiceSpy.addEarcLocalDevice(localDeviceTx);
+ localDeviceTx.handleEarcStateChange(Constants.HDMI_EARC_STATUS_ARC_PENDING);
+ verify(mHdmiControlServiceSpy, times(1)).startArcAction(eq(true), any());
+ }
+
+ @Test
+ public void earcStateWasArcPending_becomesEarcPending_terminateArc() {
+ mHdmiControlServiceSpy.clearEarcLocalDevice();
+ HdmiEarcLocalDeviceTx localDeviceTx = new HdmiEarcLocalDeviceTx(mHdmiControlServiceSpy);
+ mHdmiControlServiceSpy.addEarcLocalDevice(localDeviceTx);
+ localDeviceTx.handleEarcStateChange(Constants.HDMI_EARC_STATUS_ARC_PENDING);
+ mTestLooper.dispatchAll();
+ localDeviceTx.handleEarcStateChange(Constants.HDMI_EARC_STATUS_EARC_PENDING);
+ verify(mHdmiControlServiceSpy, times(1)).startArcAction(eq(false), any());
+ }
+
+ @Test
+ public void earcStateWasArcPending_becomesEarcEnabled_terminateArc() {
+ mHdmiControlServiceSpy.clearEarcLocalDevice();
+ HdmiEarcLocalDeviceTx localDeviceTx = new HdmiEarcLocalDeviceTx(mHdmiControlServiceSpy);
+ mHdmiControlServiceSpy.addEarcLocalDevice(localDeviceTx);
+ localDeviceTx.handleEarcStateChange(Constants.HDMI_EARC_STATUS_ARC_PENDING);
+ mTestLooper.dispatchAll();
+ localDeviceTx.handleEarcStateChange(Constants.HDMI_EARC_STATUS_EARC_CONNECTED);
+ verify(mHdmiControlServiceSpy, times(1)).startArcAction(eq(false), any());
+ }
+
protected static class MockPlaybackDevice extends HdmiCecLocalDevicePlayback {
private boolean mCanGoToStandby;
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiEarcLocalDeviceTxTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiEarcLocalDeviceTxTest.java
new file mode 100644
index 0000000..bf44e09
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiEarcLocalDeviceTxTest.java
@@ -0,0 +1,315 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.hdmi;
+
+import static android.media.AudioProfile.AUDIO_ENCAPSULATION_TYPE_NONE;
+
+import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
+import static com.android.server.hdmi.Constants.HDMI_EARC_STATUS_ARC_PENDING;
+import static com.android.server.hdmi.Constants.HDMI_EARC_STATUS_EARC_CONNECTED;
+import static com.android.server.hdmi.Constants.HDMI_EARC_STATUS_EARC_PENDING;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.hardware.hdmi.HdmiDeviceInfo;
+import android.media.AudioDescriptor;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioManager;
+import android.os.Looper;
+import android.os.test.TestLooper;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+@SmallTest
+@Presubmit
+@RunWith(JUnit4.class)
+/** Tests for {@link HdmiEarcLocalDeviceTx} class. */
+public class HdmiEarcLocalDeviceTxTest {
+
+ private HdmiControlService mHdmiControlService;
+ private HdmiCecController mHdmiCecController;
+ private HdmiEarcLocalDevice mHdmiEarcLocalDeviceTx;
+ private FakeNativeWrapper mNativeWrapper;
+ private FakePowerManagerWrapper mPowerManager;
+ private byte[] mEarcCapabilities = new byte[]{
+ 0x01, 0x01, 0x1a, 0x35, 0x0f, 0x7f, 0x07, 0x15, 0x07, 0x50, 0x3d, 0x1f, (byte) 0xc0,
+ 0x57, 0x06, 0x03, 0x67, 0x7e, 0x03, 0x5f, 0x7e, 0x03, 0x5f, 0x7e, 0x01, (byte) 0x83,
+ 0x5f, 0x00, 0x00, 0x00, 0x00, 0x00};
+ private Looper mMyLooper;
+ private TestLooper mTestLooper = new TestLooper();
+
+ @Mock
+ private AudioManager mAudioManager;
+
+ @Captor
+ ArgumentCaptor<AudioDeviceAttributes> mAudioAttributesCaptor;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ Context context = InstrumentationRegistry.getTargetContext();
+ mMyLooper = mTestLooper.getLooper();
+
+ mHdmiControlService =
+ new HdmiControlService(InstrumentationRegistry.getTargetContext(),
+ Collections.singletonList(HdmiDeviceInfo.DEVICE_TV),
+ new FakeAudioDeviceVolumeManagerWrapper()) {
+ @Override
+ boolean isCecControlEnabled() {
+ return true;
+ }
+
+ @Override
+ boolean isTvDevice() {
+ return true;
+ }
+
+ @Override
+ protected void writeStringSystemProperty(String key, String value) {
+ // do nothing
+ }
+
+ @Override
+ boolean isPowerStandby() {
+ return false;
+ }
+
+ @Override
+ AudioManager getAudioManager() {
+ return mAudioManager;
+ }
+ };
+
+ mHdmiControlService.setIoLooper(mMyLooper);
+ mHdmiControlService.setHdmiCecConfig(new FakeHdmiCecConfig(context));
+ mNativeWrapper = new FakeNativeWrapper();
+ mHdmiCecController = HdmiCecController.createWithNativeWrapper(
+ mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter());
+ mHdmiControlService.setCecController(mHdmiCecController);
+ mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService));
+ mHdmiControlService.initService();
+ mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
+ mPowerManager = new FakePowerManagerWrapper(context);
+ mHdmiControlService.setPowerManager(mPowerManager);
+ mTestLooper.dispatchAll();
+ mHdmiControlService.initializeEarcLocalDevice(HdmiControlService.INITIATED_BY_BOOT_UP);
+ mHdmiEarcLocalDeviceTx = mHdmiControlService.getEarcLocalDevice();
+ }
+
+ @Test
+ public void earcGetsConnected_capsReportedInTime_sad() {
+ mHdmiEarcLocalDeviceTx.handleEarcStateChange(HDMI_EARC_STATUS_EARC_CONNECTED);
+ mTestLooper.moveTimeForward(HdmiEarcLocalDeviceTx.REPORT_CAPS_MAX_DELAY_MS - 200);
+ mTestLooper.dispatchAll();
+ mHdmiEarcLocalDeviceTx.handleEarcCapabilitiesReported(new byte[]{
+ 0x01, 0x01, 0x1a, 0x35, 0x0f, 0x7f, 0x07, 0x15, 0x07, 0x50, 0x3d, 0x1f, (byte) 0xc0,
+ 0x57, 0x06, 0x03, 0x67, 0x7e, 0x03, 0x5f, 0x7e, 0x03, 0x5f, 0x7e, 0x01, 0x00, 0x5f,
+ 0x00, 0x00, 0x00, 0x00, 0x00
+ });
+ mTestLooper.dispatchAll();
+ verify(mAudioManager, times(1)).setWiredDeviceConnectionState(
+ mAudioAttributesCaptor.capture(), eq(1));
+ AudioDeviceAttributes attributes = mAudioAttributesCaptor.getValue();
+ List<AudioDescriptor> descriptors = attributes.getAudioDescriptors();
+ List<AudioDescriptor> expectedDescriptors = new ArrayList<AudioDescriptor>(Arrays.asList(
+ new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE,
+ new byte[] {15, 127, 7}),
+ new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE,
+ new byte[] {21, 7, 80}),
+ new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE,
+ new byte[] {61, 31, -64}),
+ new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE,
+ new byte[] {87, 6, 3}),
+ new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE,
+ new byte[] {103, 126, 3}),
+ new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE,
+ new byte[] {95, 126, 3}),
+ new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE,
+ new byte[] {95, 126, 1})));
+ assertThat(descriptors).isEqualTo(expectedDescriptors);
+ }
+
+ @Test
+ public void earcGetsConnected_capsReportedInTime_sad_sadb() {
+ mHdmiEarcLocalDeviceTx.handleEarcStateChange(HDMI_EARC_STATUS_EARC_CONNECTED);
+ mTestLooper.moveTimeForward(HdmiEarcLocalDeviceTx.REPORT_CAPS_MAX_DELAY_MS - 200);
+ mTestLooper.dispatchAll();
+ mHdmiEarcLocalDeviceTx.handleEarcCapabilitiesReported(new byte[]{
+ 0x01, 0x01, 0x1a, 0x35, 0x0f, 0x7f, 0x07, 0x15, 0x07, 0x50, 0x3d, 0x1f, (byte) 0xc0,
+ 0x57, 0x06, 0x03, 0x67, 0x7e, 0x03, 0x5f, 0x7e, 0x03, 0x5f, 0x7e, 0x01, (byte) 0x83,
+ 0x5f, 0x00, 0x00, 0x00, 0x00, 0x00});
+ mTestLooper.dispatchAll();
+ verify(mAudioManager, times(1)).setWiredDeviceConnectionState(
+ mAudioAttributesCaptor.capture(), eq(1));
+ AudioDeviceAttributes attributes = mAudioAttributesCaptor.getValue();
+ List<AudioDescriptor> descriptors = attributes.getAudioDescriptors();
+ List<AudioDescriptor> expectedDescriptors = new ArrayList<AudioDescriptor>(Arrays.asList(
+ new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE,
+ new byte[] {15, 127, 7}),
+ new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE,
+ new byte[] {21, 7, 80}),
+ new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE,
+ new byte[] {61, 31, -64}),
+ new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE,
+ new byte[] {87, 6, 3}),
+ new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE,
+ new byte[] {103, 126, 3}),
+ new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE,
+ new byte[] {95, 126, 3}),
+ new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE,
+ new byte[] {95, 126, 1}),
+ new AudioDescriptor(AudioDescriptor.STANDARD_SADB, AUDIO_ENCAPSULATION_TYPE_NONE,
+ new byte[] {-125, 95, 0, 0})));
+ assertThat(descriptors).isEqualTo(expectedDescriptors);
+ }
+
+ @Test
+ public void earcGetsConnected_capsReportedInTime_sad_sadb_vsadb() {
+ mHdmiEarcLocalDeviceTx.handleEarcStateChange(HDMI_EARC_STATUS_EARC_CONNECTED);
+ mTestLooper.moveTimeForward(HdmiEarcLocalDeviceTx.REPORT_CAPS_MAX_DELAY_MS - 200);
+ mTestLooper.dispatchAll();
+ mHdmiEarcLocalDeviceTx.handleEarcCapabilitiesReported(new byte[]{
+ 0x01, 0x01, 0x21, 0x35, 0x5F, 0x7E, 0x03, 0x5F, 0x7E, 0x01, 0x67, 0x7E, 0x03, 0x57,
+ 0x06, 0x03, 0x3D, 0x1E, (byte) 0xC0, 0x15, 0x07, 0x50, 0x0F, 0x7F, 0x07,
+ (byte) 0x83, 0x5F, 0x00, 0x00, (byte) 0xE6, 0x11, 0x46, (byte) 0xD0, 0x00, 0x70,
+ 0x00, 0x03, 0x01, (byte) 0x80, 0x00, (byte) 0x9D, (byte) 0xAD, (byte) 0x9E, 0x7B,
+ 0x08, (byte) 0xC1, (byte) 0xA8, 0x23, (byte) 0x9B, 0x49, 0x5C, (byte) 0xF5, 0x6B,
+ (byte) 0xAC, 0x22, (byte) 0xC2, (byte) 0x80, 0x48, 0x67, 0x7F, 0x59, 0x1C, 0x20,
+ 0x71, 0x35, 0x25, (byte) 0x9F, 0x43, 0x70, 0x1E, 0x32, 0x15, 0x60, (byte) 0xED,
+ (byte) 0xC8, 0x77, (byte) 0xA3, 0x24, 0x2E, (byte) 0xDA, (byte) 0x94, 0x6D, 0x35,
+ 0x34, 0x0F, 0x30, 0x62, 0x1A, 0x3B, (byte) 0xC9, 0x5A, (byte) 0xE6, (byte) 0xD8,
+ 0x22, 0x11, 0x56, (byte) 0xA6, (byte) 0x99, (byte) 0xCF, (byte) 0xE3, 0x1B,
+ (byte) 0x88, (byte) 0xA0, 0x2A, 0x5B, 0x6C, 0x5E, 0x53, 0x01, 0x47, 0x69, 0x51,
+ 0x61, (byte) 0xC7, (byte) 0xCB, 0x1B, 0x28, 0x14, 0x23, 0x10, (byte) 0xB1, 0x34,
+ 0x5E, 0x57, (byte) 0x97, (byte) 0xB3, 0x78, 0x03, 0x79, (byte) 0x8A, (byte) 0xFE,
+ 0x1E, (byte) 0xC8, (byte) 0xAB, 0x14, 0x74, 0x73, (byte) 0xFA, (byte) 0xBB,
+ (byte) 0xF7, 0x4E, 0x00, (byte) 0xFC, 0x5C, (byte) 0xDC, (byte) 0x8B, (byte) 0xC9,
+ 0x1E, 0x16, 0x35, (byte) 0xB1, (byte) 0x98, (byte) 0xEB, 0x2B, (byte) 0xE6,
+ (byte) 0xFC, (byte) 0xCC, 0x3C, 0x30, 0x19, 0x40, (byte) 0xC0, 0x50, (byte) 0xF2,
+ 0x58, 0x30, 0x4B, 0x0C, 0x7A, (byte) 0xE0, (byte) 0xFF, 0x7A, 0x64, 0x78,
+ (byte) 0xF8, 0x56, (byte) 0xF8, 0x6E, 0x72, 0x42, 0x49, 0x4E, (byte) 0xA6,
+ (byte) 0x95, (byte) 0xF5, 0x4C, 0x4F, (byte) 0xFF, 0x7F, 0x21, (byte) 0xA2,
+ (byte) 0x98, 0x33, (byte) 0x90, (byte) 0xFD, 0x17, 0x08, 0x13, (byte) 0xB2, 0x00,
+ (byte) 0xA9, (byte) 0xB5, (byte) 0xBD, (byte) 0xB5, (byte) 0xC1, (byte) 0xC7, 0x45,
+ (byte) 0xD9, (byte) 0xDC, (byte) 0x8B, 0x58, (byte) 0xB3, 0x5D, 0x5E, 0x72,
+ (byte) 0xE6, (byte) 0x8D, (byte) 0xDD, 0x0B, 0x21, (byte) 0xF3, (byte) 0x9A,
+ (byte) 0x8E, 0x1B, 0x79, 0x59, (byte) 0xE1, 0x3F, (byte) 0xAC, 0x24, (byte) 0xA0,
+ (byte) 0xC8, 0x56, (byte) 0xFD, (byte) 0x85, (byte) 0x8F, 0x6A, (byte) 0x80, 0x41,
+ (byte) 0xA8, 0x5D, 0x2C, (byte) 0xC2, 0x69, (byte) 0xA1, 0x0D, (byte) 0x82, 0x04,
+ 0x5D, (byte) 0xCA, (byte) 0xB4, (byte) 0x9F, 0x3A, 0x2D, (byte) 0xBF, 0x24});
+ mTestLooper.dispatchAll();
+ verify(mAudioManager, times(1)).setWiredDeviceConnectionState(
+ mAudioAttributesCaptor.capture(), eq(1));
+ AudioDeviceAttributes attributes = mAudioAttributesCaptor.getValue();
+ List<AudioDescriptor> descriptors = attributes.getAudioDescriptors();
+ List<AudioDescriptor> expectedDescriptors = new ArrayList<AudioDescriptor>(Arrays.asList(
+ new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE,
+ new byte[] {95, 126, 3}),
+ new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE,
+ new byte[] {95, 126, 1}),
+ new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE,
+ new byte[] {103, 126, 3}),
+ new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE,
+ new byte[] {87, 6, 3}),
+ new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE,
+ new byte[] {61, 30, -64}),
+ new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE,
+ new byte[] {21, 7, 80}),
+ new AudioDescriptor(AudioDescriptor.STANDARD_EDID, AUDIO_ENCAPSULATION_TYPE_NONE,
+ new byte[] {15, 127, 7}),
+ new AudioDescriptor(AudioDescriptor.STANDARD_SADB, AUDIO_ENCAPSULATION_TYPE_NONE,
+ new byte[] {-125, 95, 0, 0}),
+ new AudioDescriptor(AudioDescriptor.STANDARD_VSADB, AUDIO_ENCAPSULATION_TYPE_NONE,
+ new byte[] {-26, 17, 70, -48, 0, 112, 0})));
+ assertThat(descriptors).isEqualTo(expectedDescriptors);
+ }
+
+ @Test
+ public void earcGetsConnected_capsReportedTooLate() {
+ mHdmiEarcLocalDeviceTx.handleEarcStateChange(HDMI_EARC_STATUS_EARC_CONNECTED);
+ mTestLooper.moveTimeForward(HdmiEarcLocalDeviceTx.REPORT_CAPS_MAX_DELAY_MS + 1);
+ mTestLooper.dispatchAll();
+ verify(mAudioManager, times(1)).setWiredDeviceConnectionState(
+ mAudioAttributesCaptor.capture(), eq(1));
+ AudioDeviceAttributes attributes = mAudioAttributesCaptor.getValue();
+ List<AudioDescriptor> descriptors = attributes.getAudioDescriptors();
+ assertThat(descriptors).hasSize(0);
+ Mockito.clearInvocations(mAudioManager);
+
+ mHdmiEarcLocalDeviceTx.handleEarcCapabilitiesReported(mEarcCapabilities);
+ mTestLooper.dispatchAll();
+ verify(mAudioManager, times(0)).setWiredDeviceConnectionState(any(), anyInt());
+ }
+
+ @Test
+ public void earcGetsConnected_earcGetsDisconnectedBeforeCapsReported() {
+ mHdmiEarcLocalDeviceTx.handleEarcStateChange(HDMI_EARC_STATUS_EARC_CONNECTED);
+ mTestLooper.dispatchAll();
+ mHdmiEarcLocalDeviceTx.handleEarcStateChange(HDMI_EARC_STATUS_ARC_PENDING);
+ mTestLooper.dispatchAll();
+ verify(mAudioManager, times(0)).setWiredDeviceConnectionState(any(), eq(1));
+ verify(mAudioManager, times(1)).setWiredDeviceConnectionState(
+ mAudioAttributesCaptor.capture(), eq(0));
+ AudioDeviceAttributes attributes = mAudioAttributesCaptor.getValue();
+ List<AudioDescriptor> descriptors = attributes.getAudioDescriptors();
+ assertThat(descriptors).hasSize(0);
+ Mockito.clearInvocations(mAudioManager);
+
+ mHdmiEarcLocalDeviceTx.handleEarcCapabilitiesReported(mEarcCapabilities);
+ mTestLooper.dispatchAll();
+ verify(mAudioManager, times(0)).setWiredDeviceConnectionState(any(), anyInt());
+ }
+
+ @Test
+ public void earcGetsConnected_earcBecomesPendingBeforeCapsReported() {
+ mHdmiEarcLocalDeviceTx.handleEarcStateChange(HDMI_EARC_STATUS_EARC_CONNECTED);
+ mTestLooper.dispatchAll();
+ mHdmiEarcLocalDeviceTx.handleEarcStateChange(HDMI_EARC_STATUS_EARC_PENDING);
+ mTestLooper.dispatchAll();
+ verify(mAudioManager, times(0)).setWiredDeviceConnectionState(any(), anyInt());
+ Mockito.clearInvocations(mAudioManager);
+
+ mHdmiEarcLocalDeviceTx.handleEarcCapabilitiesReported(mEarcCapabilities);
+ mTestLooper.dispatchAll();
+ verify(mAudioManager, times(0)).setWiredDeviceConnectionState(any(), anyInt());
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/job/JobSetTest.java b/services/tests/servicestests/src/com/android/server/job/JobSetTest.java
index 62cc111..baa5421 100644
--- a/services/tests/servicestests/src/com/android/server/job/JobSetTest.java
+++ b/services/tests/servicestests/src/com/android/server/job/JobSetTest.java
@@ -79,7 +79,7 @@
.setRequiresCharging(true)
.build();
return JobStatus.createFromJobInfo(jobInfo, callingUid, mContext.getPackageName(),
- mContext.getUserId(), "Test");
+ mContext.getUserId(), "Namespace", "Test");
}
@Test
diff --git a/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java b/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
index 0589b3a..1c44da1 100644
--- a/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
+++ b/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
@@ -6,6 +6,8 @@
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -15,6 +17,7 @@
import android.app.job.JobInfo;
import android.app.job.JobInfo.Builder;
+import android.app.job.JobWorkItem;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.PackageManagerInternal;
@@ -31,6 +34,7 @@
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.internal.util.ArrayUtils;
import com.android.server.LocalServices;
import com.android.server.job.JobStore.JobSet;
import com.android.server.job.controllers.JobStatus;
@@ -43,6 +47,10 @@
import java.io.File;
import java.time.Clock;
import java.time.ZoneOffset;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
/**
* Test reading and writing correctly from file.
@@ -135,8 +143,8 @@
.build();
final int uid1 = SOME_UID;
final int uid2 = uid1 + 1;
- final JobStatus JobStatus1 = JobStatus.createFromJobInfo(task1, uid1, null, -1, null);
- final JobStatus JobStatus2 = JobStatus.createFromJobInfo(task2, uid2, null, -1, null);
+ final JobStatus JobStatus1 = JobStatus.createFromJobInfo(task1, uid1, null, -1, null, null);
+ final JobStatus JobStatus2 = JobStatus.createFromJobInfo(task2, uid2, null, -1, null, null);
runWritingJobsToDisk(JobStatus1, JobStatus2);
// Remove 1 job
@@ -188,8 +196,8 @@
.build();
final int uid1 = SOME_UID;
final int uid2 = uid1 + 1;
- final JobStatus JobStatus1 = JobStatus.createFromJobInfo(task1, uid1, null, -1, null);
- final JobStatus JobStatus2 = JobStatus.createFromJobInfo(task2, uid2, null, -1, null);
+ final JobStatus JobStatus1 = JobStatus.createFromJobInfo(task1, uid1, null, -1, null, null);
+ final JobStatus JobStatus2 = JobStatus.createFromJobInfo(task2, uid2, null, -1, null, null);
runWritingJobsToDisk(JobStatus1, JobStatus2);
// Remove all jobs
@@ -265,7 +273,7 @@
.setMinimumLatency(runFromMillis)
.setPersisted(true)
.build();
- final JobStatus ts = JobStatus.createFromJobInfo(task, SOME_UID, null, -1, null);
+ final JobStatus ts = JobStatus.createFromJobInfo(task, SOME_UID, null, -1, null, null);
ts.addInternalFlags(JobStatus.INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION);
mTaskStoreUnderTest.add(ts);
waitForPendingIo();
@@ -308,8 +316,10 @@
.build();
final int uid1 = SOME_UID;
final int uid2 = uid1 + 1;
- final JobStatus taskStatus1 = JobStatus.createFromJobInfo(task1, uid1, null, -1, null);
- final JobStatus taskStatus2 = JobStatus.createFromJobInfo(task2, uid2, null, -1, null);
+ final JobStatus taskStatus1 =
+ JobStatus.createFromJobInfo(task1, uid1, null, -1, null, null);
+ final JobStatus taskStatus2 =
+ JobStatus.createFromJobInfo(task2, uid2, null, -1, null, null);
runWritingJobsToDisk(taskStatus1, taskStatus2);
}
@@ -364,7 +374,7 @@
extras.putInt("into", 3);
b.setExtras(extras);
final JobInfo task = b.build();
- JobStatus taskStatus = JobStatus.createFromJobInfo(task, SOME_UID, null, -1, null);
+ JobStatus taskStatus = JobStatus.createFromJobInfo(task, SOME_UID, null, -1, null, null);
mTaskStoreUnderTest.add(taskStatus);
waitForPendingIo();
@@ -384,7 +394,7 @@
.setRequiresCharging(true)
.setPersisted(true);
JobStatus taskStatus = JobStatus.createFromJobInfo(b.build(), SOME_UID,
- "com.google.android.gms", 0, null);
+ "com.android.test.app", 0, null, null);
mTaskStoreUnderTest.add(taskStatus);
waitForPendingIo();
@@ -406,7 +416,8 @@
.setPeriodic(5*60*60*1000, 1*60*60*1000)
.setRequiresCharging(true)
.setPersisted(true);
- JobStatus taskStatus = JobStatus.createFromJobInfo(b.build(), SOME_UID, null, -1, null);
+ JobStatus taskStatus = JobStatus.createFromJobInfo(b.build(),
+ SOME_UID, null, -1, null, null);
mTaskStoreUnderTest.add(taskStatus);
waitForPendingIo();
@@ -435,7 +446,7 @@
invalidLateRuntimeElapsedMillis - TWO_HOURS; // Early is (late - period).
final Pair<Long, Long> persistedExecutionTimesUTC = new Pair<>(rtcNow, rtcNow + ONE_HOUR);
final JobStatus js = new JobStatus(b.build(), SOME_UID, "somePackage",
- 0 /* sourceUserId */, 0, "someTag",
+ 0 /* sourceUserId */, 0, "someNamespace", "someTag",
invalidEarlyRuntimeElapsedMillis, invalidLateRuntimeElapsedMillis,
0 /* lastSuccessfulRunTime */, 0 /* lastFailedRunTime */,
persistedExecutionTimesUTC, 0 /* innerFlag */, 0 /* dynamicConstraints */);
@@ -464,7 +475,7 @@
.setOverrideDeadline(5000)
.setBias(42)
.setPersisted(true);
- final JobStatus js = JobStatus.createFromJobInfo(b.build(), SOME_UID, null, -1, null);
+ final JobStatus js = JobStatus.createFromJobInfo(b.build(), SOME_UID, null, -1, null, null);
mTaskStoreUnderTest.add(js);
waitForPendingIo();
@@ -475,13 +486,30 @@
}
@Test
+ public void testNamespacePersisted() throws Exception {
+ final String namespace = "my.test.namespace";
+ JobInfo.Builder b = new Builder(93, mComponent)
+ .setRequiresBatteryNotLow(true)
+ .setPersisted(true);
+ final JobStatus js =
+ JobStatus.createFromJobInfo(b.build(), SOME_UID, null, -1, namespace, null);
+ mTaskStoreUnderTest.add(js);
+ waitForPendingIo();
+
+ final JobSet jobStatusSet = new JobSet();
+ mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
+ JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
+ assertEquals("Namespace not correctly persisted.", namespace, loaded.getNamespace());
+ }
+
+ @Test
public void testPriorityPersisted() throws Exception {
final JobInfo job = new Builder(92, mComponent)
.setOverrideDeadline(5000)
.setPriority(JobInfo.PRIORITY_MIN)
.setPersisted(true)
.build();
- final JobStatus js = JobStatus.createFromJobInfo(job, SOME_UID, null, -1, null);
+ final JobStatus js = JobStatus.createFromJobInfo(job, SOME_UID, null, -1, null, null);
mTaskStoreUnderTest.add(js);
waitForPendingIo();
@@ -500,12 +528,14 @@
JobInfo.Builder b = new Builder(42, mComponent)
.setOverrideDeadline(10000)
.setPersisted(false);
- JobStatus jsNonPersisted = JobStatus.createFromJobInfo(b.build(), SOME_UID, null, -1, null);
+ JobStatus jsNonPersisted = JobStatus.createFromJobInfo(b.build(),
+ SOME_UID, null, -1, null, null);
mTaskStoreUnderTest.add(jsNonPersisted);
b = new Builder(43, mComponent)
.setOverrideDeadline(10000)
.setPersisted(true);
- JobStatus jsPersisted = JobStatus.createFromJobInfo(b.build(), SOME_UID, null, -1, null);
+ JobStatus jsPersisted = JobStatus.createFromJobInfo(b.build(),
+ SOME_UID, null, -1, null, null);
mTaskStoreUnderTest.add(jsPersisted);
waitForPendingIo();
@@ -593,7 +623,8 @@
JobInfo.Builder b = new Builder(8, mComponent)
.setRequiresDeviceIdle(true)
.setPersisted(true);
- JobStatus taskStatus = JobStatus.createFromJobInfo(b.build(), SOME_UID, null, -1, null);
+ JobStatus taskStatus = JobStatus.createFromJobInfo(b.build(),
+ SOME_UID, null, -1, null, null);
mTaskStoreUnderTest.add(taskStatus);
waitForPendingIo();
@@ -612,7 +643,8 @@
JobInfo.Builder b = new Builder(8, mComponent)
.setRequiresCharging(true)
.setPersisted(true);
- JobStatus taskStatus = JobStatus.createFromJobInfo(b.build(), SOME_UID, null, -1, null);
+ JobStatus taskStatus = JobStatus.createFromJobInfo(b.build(),
+ SOME_UID, null, -1, null, null);
mTaskStoreUnderTest.add(taskStatus);
waitForPendingIo();
@@ -631,7 +663,8 @@
JobInfo.Builder b = new Builder(8, mComponent)
.setRequiresStorageNotLow(true)
.setPersisted(true);
- JobStatus taskStatus = JobStatus.createFromJobInfo(b.build(), SOME_UID, null, -1, null);
+ JobStatus taskStatus = JobStatus.createFromJobInfo(b.build(),
+ SOME_UID, null, -1, null, null);
mTaskStoreUnderTest.add(taskStatus);
waitForPendingIo();
@@ -650,7 +683,8 @@
JobInfo.Builder b = new Builder(8, mComponent)
.setRequiresBatteryNotLow(true)
.setPersisted(true);
- JobStatus taskStatus = JobStatus.createFromJobInfo(b.build(), SOME_UID, null, -1, null);
+ JobStatus taskStatus = JobStatus.createFromJobInfo(b.build(),
+ SOME_UID, null, -1, null, null);
mTaskStoreUnderTest.add(taskStatus);
waitForPendingIo();
@@ -664,20 +698,46 @@
taskStatus.getJob().isRequireBatteryNotLow());
}
+ @Test
+ public void testJobWorkItems() throws Exception {
+ JobWorkItem item1 = new JobWorkItem.Builder().build();
+ item1.bumpDeliveryCount();
+ PersistableBundle bundle = new PersistableBundle();
+ bundle.putBoolean("test", true);
+ JobWorkItem item2 = new JobWorkItem.Builder().setExtras(bundle).build();
+ item2.bumpDeliveryCount();
+ JobWorkItem item3 = new JobWorkItem.Builder().setEstimatedNetworkBytes(1, 2).build();
+ JobWorkItem item4 = new JobWorkItem.Builder().setMinimumNetworkChunkBytes(3).build();
+ JobWorkItem item5 = new JobWorkItem.Builder().build();
+
+ JobInfo jobInfo = new JobInfo.Builder(0, mComponent)
+ .setPersisted(true)
+ .build();
+ JobStatus jobStatus =
+ JobStatus.createFromJobInfo(jobInfo, SOME_UID, null, -1, null, null);
+ jobStatus.executingWork = new ArrayList<>(List.of(item1, item2));
+ jobStatus.pendingWork = new ArrayList<>(List.of(item3, item4, item5));
+ assertPersistedEquals(jobStatus);
+ }
+
/**
* Helper function to kick a {@link JobInfo} through a persistence cycle and
* assert that it's unchanged.
*/
private void assertPersistedEquals(JobInfo firstInfo) throws Exception {
+ assertPersistedEquals(
+ JobStatus.createFromJobInfo(firstInfo, SOME_UID, null, -1, null, null));
+ }
+
+ private void assertPersistedEquals(JobStatus original) throws Exception {
mTaskStoreUnderTest.clear();
- JobStatus first = JobStatus.createFromJobInfo(firstInfo, SOME_UID, null, -1, null);
- mTaskStoreUnderTest.add(first);
+ mTaskStoreUnderTest.add(original);
waitForPendingIo();
final JobSet jobStatusSet = new JobSet();
mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
final JobStatus second = jobStatusSet.getAllJobs().iterator().next();
- assertJobsEqual(first, second);
+ assertJobsEqual(original, second);
}
/**
@@ -693,6 +753,8 @@
assertEquals("Calling UID not equal", expected.getUid(), actual.getUid());
assertEquals("Calling user not equal", expected.getUserId(), actual.getUserId());
+ assertEquals(expected.getNamespace(), actual.getNamespace());
+
assertEquals("Internal flags not equal",
expected.getInternalFlags(), actual.getInternalFlags());
@@ -701,6 +763,59 @@
expected.getEarliestRunTime(), actual.getEarliestRunTime());
compareTimestampsSubjectToIoLatency("Late run-times not the same after read.",
expected.getLatestRunTimeElapsed(), actual.getLatestRunTimeElapsed());
+
+ assertEquals(expected.hasWorkLocked(), actual.hasWorkLocked());
+ if (expected.hasWorkLocked()) {
+ List<JobWorkItem> allWork = new ArrayList<>();
+ if (expected.executingWork != null) {
+ allWork.addAll(expected.executingWork);
+ }
+ if (expected.pendingWork != null) {
+ allWork.addAll(expected.pendingWork);
+ }
+ // All work for freshly loaded Job will be pending.
+ assertNotNull(actual.pendingWork);
+ assertTrue(ArrayUtils.isEmpty(actual.executingWork));
+ assertEquals(allWork.size(), actual.pendingWork.size());
+ for (int i = 0; i < allWork.size(); ++i) {
+ JobWorkItem expectedItem = allWork.get(i);
+ JobWorkItem actualItem = actual.pendingWork.get(i);
+ assertJobWorkItemsEqual(expectedItem, actualItem);
+ }
+ }
+ }
+
+ private void assertJobWorkItemsEqual(JobWorkItem expected, JobWorkItem actual) {
+ if (expected == null) {
+ assertNull(actual);
+ return;
+ }
+ assertNotNull(actual);
+ assertEquals(expected.getDeliveryCount(), actual.getDeliveryCount());
+ assertEquals(expected.getEstimatedNetworkDownloadBytes(),
+ actual.getEstimatedNetworkDownloadBytes());
+ assertEquals(expected.getEstimatedNetworkUploadBytes(),
+ actual.getEstimatedNetworkUploadBytes());
+ assertEquals(expected.getMinimumNetworkChunkBytes(), actual.getMinimumNetworkChunkBytes());
+ if (expected.getIntent() == null) {
+ assertNull(actual.getIntent());
+ } else {
+ // filterEquals() just so happens to check almost everything that is persisted to disk.
+ assertTrue(expected.getIntent().filterEquals(actual.getIntent()));
+ assertEquals(expected.getIntent().getFlags(), actual.getIntent().getFlags());
+ }
+ assertEquals(expected.getGrants(), actual.getGrants());
+ PersistableBundle expectedExtras = expected.getExtras();
+ PersistableBundle actualExtras = actual.getExtras();
+ if (expectedExtras == null) {
+ assertNull(actualExtras);
+ } else {
+ assertEquals(expectedExtras.size(), actualExtras.size());
+ Set<String> keys = expectedExtras.keySet();
+ for (String key : keys) {
+ assertTrue(Objects.equals(expectedExtras.get(key), actualExtras.get(key)));
+ }
+ }
}
/**
diff --git a/services/tests/servicestests/src/com/android/server/job/PendingJobQueueTest.java b/services/tests/servicestests/src/com/android/server/job/PendingJobQueueTest.java
index b7faf22..3268df2 100644
--- a/services/tests/servicestests/src/com/android/server/job/PendingJobQueueTest.java
+++ b/services/tests/servicestests/src/com/android/server/job/PendingJobQueueTest.java
@@ -28,7 +28,7 @@
import android.platform.test.annotations.LargeTest;
import android.util.ArraySet;
import android.util.Log;
-import android.util.SparseArray;
+import android.util.SparseArrayMap;
import android.util.SparseBooleanArray;
import android.util.SparseLongArray;
@@ -54,8 +54,13 @@
private JobStatus createJobStatus(String testTag, JobInfo.Builder jobInfoBuilder,
int callingUid) {
+ return createJobStatus(testTag, jobInfoBuilder, callingUid, "PJQTest");
+ }
+
+ private JobStatus createJobStatus(String testTag, JobInfo.Builder jobInfoBuilder,
+ int callingUid, String namespace) {
return JobStatus.createFromJobInfo(
- jobInfoBuilder.build(), callingUid, "com.android.test", 0, testTag);
+ jobInfoBuilder.build(), callingUid, "com.android.test", 0, namespace, testTag);
}
@Test
@@ -373,12 +378,12 @@
jobQueue.add(rC10);
jobQueue.add(eC11);
- checkPendingJobInvariants(jobQueue);
JobStatus job;
final JobStatus[] expectedPureOrder = new JobStatus[]{
eC3, rD4, eE5, eB6, rB2, eA7, rA1, rH8, eF9, rF8, eC11, rC10, rG12, rG13, eE14};
int idx = 0;
jobQueue.setOptimizeIteration(false);
+ checkPendingJobInvariants(jobQueue);
jobQueue.resetIterator();
while ((job = jobQueue.next()) != null) {
assertEquals("List wasn't correctly sorted @ index " + idx,
@@ -390,6 +395,93 @@
eC3, eC11, rD4, eE5, eE14, eB6, rB2, eA7, rA1, rH8, eF9, rF8, rC10, rG12, rG13};
idx = 0;
jobQueue.setOptimizeIteration(true);
+ checkPendingJobInvariants(jobQueue);
+ jobQueue.resetIterator();
+ while ((job = jobQueue.next()) != null) {
+ assertEquals("Optimized list wasn't correctly sorted @ index " + idx,
+ expectedOptimizedOrder[idx].getJobId(), job.getJobId());
+ idx++;
+ }
+ }
+
+ @Test
+ public void testPendingJobSorting_namespacing() {
+ PendingJobQueue jobQueue = new PendingJobQueue();
+
+ // First letter in job variable name indicate regular (r) or expedited (e).
+ // Capital letters in job variable name indicate the app/UID.
+ // Third letter (x, y, z) indicates the namespace.
+ // Numbers in job variable name indicate the enqueue time.
+ // Expected sort order:
+ // eCx3 > rDx4 > eBy6 > rBy2 > eAy7 > rAx1 > eCy8 > rEz9 > rEz5
+ // Intentions:
+ // * A jobs test expedited is before regular, regardless of namespace
+ // * B jobs test expedited is before regular, in the same namespace
+ // * C jobs test sorting by priority with different namespaces
+ // * E jobs test sorting by priority in the same namespace
+ final String namespaceX = null;
+ final String namespaceY = "y";
+ final String namespaceZ = "z";
+ JobStatus rAx1 = createJobStatus("testPendingJobSorting",
+ createJobInfo(1), 1, namespaceX);
+ JobStatus rBy2 = createJobStatus("testPendingJobSorting",
+ createJobInfo(2), 2, namespaceY);
+ JobStatus eCx3 = createJobStatus("testPendingJobSorting",
+ createJobInfo(3).setExpedited(true).setPriority(JobInfo.PRIORITY_HIGH),
+ 3, namespaceX);
+ JobStatus rDx4 = createJobStatus("testPendingJobSorting",
+ createJobInfo(4), 4, namespaceX);
+ JobStatus rEz5 = createJobStatus("testPendingJobSorting",
+ createJobInfo(5).setPriority(JobInfo.PRIORITY_LOW), 5, namespaceZ);
+ JobStatus eBy6 = createJobStatus("testPendingJobSorting",
+ createJobInfo(6).setExpedited(true), 2, namespaceY);
+ JobStatus eAy7 = createJobStatus("testPendingJobSorting",
+ createJobInfo(7).setExpedited(true), 1, namespaceY);
+ JobStatus eCy8 = createJobStatus("testPendingJobSorting",
+ createJobInfo(8).setExpedited(true).setPriority(JobInfo.PRIORITY_MAX),
+ 3, namespaceY);
+ JobStatus rEz9 = createJobStatus("testPendingJobSorting",
+ createJobInfo(9).setPriority(JobInfo.PRIORITY_HIGH), 5, namespaceZ);
+
+ rAx1.enqueueTime = 10;
+ rBy2.enqueueTime = 20;
+ eCx3.enqueueTime = 30;
+ rDx4.enqueueTime = 40;
+ rEz5.enqueueTime = 50;
+ eBy6.enqueueTime = 60;
+ eAy7.enqueueTime = 70;
+ eCy8.enqueueTime = 80;
+ rEz9.enqueueTime = 90;
+
+ // Add in random order so sorting is apparent.
+ jobQueue.add(rEz9);
+ jobQueue.add(eCy8);
+ jobQueue.add(rDx4);
+ jobQueue.add(rEz5);
+ jobQueue.add(rBy2);
+ jobQueue.add(rAx1);
+ jobQueue.add(eCx3);
+ jobQueue.add(eBy6);
+ jobQueue.add(eAy7);
+
+ JobStatus job;
+ final JobStatus[] expectedPureOrder = new JobStatus[]{
+ eCx3, rDx4, eBy6, rBy2, eAy7, rAx1, eCy8, rEz9, rEz5};
+ int idx = 0;
+ jobQueue.setOptimizeIteration(false);
+ checkPendingJobInvariants(jobQueue);
+ jobQueue.resetIterator();
+ while ((job = jobQueue.next()) != null) {
+ assertEquals("List wasn't correctly sorted @ index " + idx,
+ expectedPureOrder[idx].getJobId(), job.getJobId());
+ idx++;
+ }
+
+ final JobStatus[] expectedOptimizedOrder = new JobStatus[]{
+ eCx3, eCy8, rDx4, eBy6, rBy2, eAy7, rAx1, rEz9, rEz5};
+ idx = 0;
+ jobQueue.setOptimizeIteration(true);
+ checkPendingJobInvariants(jobQueue);
jobQueue.resetIterator();
while ((job = jobQueue.next()) != null) {
assertEquals("Optimized list wasn't correctly sorted @ index " + idx,
@@ -414,6 +506,22 @@
}
@Test
+ public void testPendingJobSorting_Random_namespacing() {
+ PendingJobQueue jobQueue = new PendingJobQueue();
+ Random random = new Random(1); // Always use the same series of pseudo random values.
+
+ for (int i = 0; i < 5000; ++i) {
+ JobStatus job = createJobStatus("testPendingJobSorting_Random",
+ createJobInfo(i).setExpedited(random.nextBoolean()), random.nextInt(250),
+ "namespace" + random.nextInt(5));
+ job.enqueueTime = random.nextInt(1_000_000);
+ jobQueue.add(job);
+ }
+
+ checkPendingJobInvariants(jobQueue);
+ }
+
+ @Test
public void testPendingJobSortingTransitivity() {
PendingJobQueue jobQueue = new PendingJobQueue();
// Always use the same series of pseudo random values.
@@ -546,10 +654,11 @@
private void checkPendingJobInvariants(PendingJobQueue jobQueue) {
final SparseBooleanArray regJobSeen = new SparseBooleanArray();
- // Latest priority enqueue times seen for each priority for each app.
- final SparseArray<SparseLongArray> latestPriorityRegEnqueueTimesPerUid =
- new SparseArray<>();
- final SparseArray<SparseLongArray> latestPriorityEjEnqueueTimesPerUid = new SparseArray<>();
+ // Latest priority enqueue times seen for each priority+namespace for each app.
+ final SparseArrayMap<String, SparseLongArray> latestPriorityRegEnqueueTimesPerUid =
+ new SparseArrayMap();
+ final SparseArrayMap<String, SparseLongArray> latestPriorityEjEnqueueTimesPerUid =
+ new SparseArrayMap<>();
final int noEntry = -1;
int prevOverrideState = noEntry;
@@ -579,11 +688,12 @@
}
final int priority = job.getEffectivePriority();
- final SparseArray<SparseLongArray> latestPriorityEnqueueTimesPerUid =
+ final SparseArrayMap<String, SparseLongArray> latestPriorityEnqueueTimesPerUid =
job.isRequestedExpeditedJob()
? latestPriorityEjEnqueueTimesPerUid
: latestPriorityRegEnqueueTimesPerUid;
- SparseLongArray latestPriorityEnqueueTimes = latestPriorityEnqueueTimesPerUid.get(uid);
+ SparseLongArray latestPriorityEnqueueTimes =
+ latestPriorityEnqueueTimesPerUid.get(uid, job.getNamespace());
if (latestPriorityEnqueueTimes != null) {
// Invariant 2
for (int p = priority - 1; p >= JobInfo.PRIORITY_MIN; --p) {
@@ -603,7 +713,8 @@
}
} else {
latestPriorityEnqueueTimes = new SparseLongArray();
- latestPriorityEnqueueTimesPerUid.put(uid, latestPriorityEnqueueTimes);
+ latestPriorityEnqueueTimesPerUid.add(
+ uid, job.getNamespace(), latestPriorityEnqueueTimes);
}
latestPriorityEnqueueTimes.put(priority, job.enqueueTime);
@@ -618,7 +729,7 @@
}
private static String testJobToString(JobStatus job) {
- return "testJob " + job.getSourceUid() + "/" + job.getJobId()
+ return "testJob " + job.getSourceUid() + "/" + job.getNamespace() + "/" + job.getJobId()
+ "/o" + job.overrideState
+ "/p" + job.getEffectivePriority()
+ "/b" + job.lastEvaluatedBias
diff --git a/services/tests/servicestests/src/com/android/server/locales/FakePackageConfigurationUpdater.java b/services/tests/servicestests/src/com/android/server/locales/FakePackageConfigurationUpdater.java
index 6cdae53..56df9d9 100644
--- a/services/tests/servicestests/src/com/android/server/locales/FakePackageConfigurationUpdater.java
+++ b/services/tests/servicestests/src/com/android/server/locales/FakePackageConfigurationUpdater.java
@@ -16,6 +16,8 @@
package com.android.server.locales;
+import static android.content.res.Configuration.GRAMMATICAL_GENDER_NOT_SPECIFIED;
+
import android.annotation.Nullable;
import android.os.LocaleList;
@@ -29,6 +31,8 @@
FakePackageConfigurationUpdater() {}
+ private int mGender = GRAMMATICAL_GENDER_NOT_SPECIFIED;
+
LocaleList mLocales = null;
@Override
@@ -43,6 +47,12 @@
}
@Override
+ public PackageConfigurationUpdater setGrammaticalGender(int gender) {
+ mGender = gender;
+ return this;
+ }
+
+ @Override
public boolean commit() {
return mLocales != null;
}
@@ -56,4 +66,10 @@
return mLocales;
}
+ /**
+ * Returns the gender that were stored during the test run.
+ */
+ int getGender() {
+ return mGender;
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java
index 79ed7d1..065aec5 100644
--- a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java
@@ -16,6 +16,8 @@
package com.android.server.locales;
+import static android.content.res.Configuration.GRAMMATICAL_GENDER_NOT_SPECIFIED;
+
import static com.google.common.truth.Truth.assertThat;
import static junit.framework.Assert.assertEquals;
@@ -234,7 +236,8 @@
throws Exception {
doReturn(DEFAULT_UID).when(mMockPackageManager)
.getPackageUidAsUser(anyString(), any(), anyInt());
- doReturn(new PackageConfig(/* nightMode = */ 0, DEFAULT_LOCALES))
+ doReturn(new PackageConfig(/* nightMode = */ 0, DEFAULT_LOCALES,
+ GRAMMATICAL_GENDER_NOT_SPECIFIED))
.when(mMockActivityTaskManager).getApplicationConfig(anyString(), anyInt());
String imPkgName = getCurrentInputMethodPackageName();
doReturn(Binder.getCallingUid()).when(mMockPackageManager)
@@ -274,7 +277,8 @@
doReturn(DEFAULT_UID).when(mMockPackageManager)
.getPackageUidAsUser(anyString(), any(), anyInt());
setUpPassingPermissionCheckFor(Manifest.permission.READ_APP_SPECIFIC_LOCALES);
- doReturn(new PackageConfig(/* nightMode = */ 0, /* locales = */ null))
+ doReturn(new PackageConfig(/* nightMode = */ 0, /* locales = */ null,
+ GRAMMATICAL_GENDER_NOT_SPECIFIED))
.when(mMockActivityTaskManager).getApplicationConfig(any(), anyInt());
LocaleList locales = mLocaleManagerService.getApplicationLocales(
@@ -288,7 +292,8 @@
throws Exception {
doReturn(Binder.getCallingUid()).when(mMockPackageManager)
.getPackageUidAsUser(anyString(), any(), anyInt());
- doReturn(new PackageConfig(/* nightMode = */ 0, DEFAULT_LOCALES))
+ doReturn(new PackageConfig(/* nightMode = */ 0, DEFAULT_LOCALES,
+ GRAMMATICAL_GENDER_NOT_SPECIFIED))
.when(mMockActivityTaskManager).getApplicationConfig(anyString(), anyInt());
LocaleList locales =
@@ -303,7 +308,8 @@
doReturn(DEFAULT_UID).when(mMockPackageManager)
.getPackageUidAsUser(anyString(), any(), anyInt());
setUpPassingPermissionCheckFor(Manifest.permission.READ_APP_SPECIFIC_LOCALES);
- doReturn(new PackageConfig(/* nightMode = */ 0, DEFAULT_LOCALES))
+ doReturn(new PackageConfig(/* nightMode = */ 0, DEFAULT_LOCALES,
+ GRAMMATICAL_GENDER_NOT_SPECIFIED))
.when(mMockActivityTaskManager).getApplicationConfig(anyString(), anyInt());
LocaleList locales =
@@ -319,7 +325,8 @@
.getPackageUidAsUser(eq(DEFAULT_PACKAGE_NAME), any(), anyInt());
doReturn(Binder.getCallingUid()).when(mMockPackageManager)
.getPackageUidAsUser(eq(DEFAULT_INSTALLER_PACKAGE_NAME), any(), anyInt());
- doReturn(new PackageConfig(/* nightMode = */ 0, DEFAULT_LOCALES))
+ doReturn(new PackageConfig(/* nightMode = */ 0, DEFAULT_LOCALES,
+ GRAMMATICAL_GENDER_NOT_SPECIFIED))
.when(mMockActivityTaskManager).getApplicationConfig(anyString(), anyInt());
LocaleList locales =
@@ -334,7 +341,8 @@
throws Exception {
doReturn(DEFAULT_UID).when(mMockPackageManager)
.getPackageUidAsUser(anyString(), any(), anyInt());
- doReturn(new PackageConfig(/* nightMode = */ 0, DEFAULT_LOCALES))
+ doReturn(new PackageConfig(/* nightMode = */ 0, DEFAULT_LOCALES,
+ GRAMMATICAL_GENDER_NOT_SPECIFIED))
.when(mMockActivityTaskManager).getApplicationConfig(anyString(), anyInt());
String imPkgName = getCurrentInputMethodPackageName();
doReturn(Binder.getCallingUid()).when(mMockPackageManager)
diff --git a/services/tests/servicestests/src/com/android/server/locales/SystemAppUpdateTrackerTest.java b/services/tests/servicestests/src/com/android/server/locales/SystemAppUpdateTrackerTest.java
index cbf555e..494796e 100644
--- a/services/tests/servicestests/src/com/android/server/locales/SystemAppUpdateTrackerTest.java
+++ b/services/tests/servicestests/src/com/android/server/locales/SystemAppUpdateTrackerTest.java
@@ -16,6 +16,8 @@
package com.android.server.locales;
+import static android.content.res.Configuration.GRAMMATICAL_GENDER_NOT_SPECIFIED;
+
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertTrue;
@@ -168,7 +170,8 @@
/* isUpdatedSystemApp = */ true))
.when(mMockPackageManager).getApplicationInfo(eq(DEFAULT_PACKAGE_NAME_1), any());
doReturn(new ActivityTaskManagerInternal.PackageConfig(/* nightMode = */ 0,
- DEFAULT_LOCALES)).when(mMockActivityTaskManager)
+ DEFAULT_LOCALES, GRAMMATICAL_GENDER_NOT_SPECIFIED))
+ .when(mMockActivityTaskManager)
.getApplicationConfig(anyString(), anyInt());
mPackageMonitor.onPackageUpdateFinished(DEFAULT_PACKAGE_NAME_1,
@@ -186,7 +189,8 @@
/* isUpdatedSystemApp = */ true))
.when(mMockPackageManager).getApplicationInfo(eq(DEFAULT_PACKAGE_NAME_1), any());
doReturn(new ActivityTaskManagerInternal.PackageConfig(/* nightMode = */ 0,
- DEFAULT_LOCALES)).when(mMockActivityTaskManager)
+ DEFAULT_LOCALES, GRAMMATICAL_GENDER_NOT_SPECIFIED))
+ .when(mMockActivityTaskManager)
.getApplicationConfig(anyString(), anyInt());
// first update
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java
index d3b647d..f0f0632 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java
@@ -154,7 +154,7 @@
storageManager, spManager, gsiService, recoverableKeyStoreManager,
userManagerInternal, deviceStateCache));
mGateKeeperService = gatekeeper;
- mAuthSecretServiceAidl = authSecretService;
+ mAuthSecretService = authSecretService;
}
@Override
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTests.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTests.java
index 95d0e15..03d5b17 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTests.java
@@ -389,11 +389,11 @@
@Test
public void testPersistentData_serializeUnserialize() {
- byte[] serialized = PersistentData.toBytes(PersistentData.TYPE_SP, SOME_USER_ID,
+ byte[] serialized = PersistentData.toBytes(PersistentData.TYPE_SP_GATEKEEPER, SOME_USER_ID,
DevicePolicyManager.PASSWORD_QUALITY_COMPLEX, PAYLOAD);
PersistentData deserialized = PersistentData.fromBytes(serialized);
- assertEquals(PersistentData.TYPE_SP, deserialized.type);
+ assertEquals(PersistentData.TYPE_SP_GATEKEEPER, deserialized.type);
assertEquals(DevicePolicyManager.PASSWORD_QUALITY_COMPLEX, deserialized.qualityForUi);
assertArrayEquals(PAYLOAD, deserialized.payload);
}
@@ -424,13 +424,13 @@
// the wire format in the future.
byte[] serializedVersion1 = new byte[] {
1, /* PersistentData.VERSION_1 */
- 1, /* PersistentData.TYPE_SP */
+ 1, /* PersistentData.TYPE_SP_GATEKEEPER */
0x00, 0x00, 0x04, 0x0A, /* SOME_USER_ID */
0x00, 0x03, 0x00, 0x00, /* PASSWORD_NUMERIC_COMPLEX */
1, 2, -1, -2, 33, /* PAYLOAD */
};
PersistentData deserialized = PersistentData.fromBytes(serializedVersion1);
- assertEquals(PersistentData.TYPE_SP, deserialized.type);
+ assertEquals(PersistentData.TYPE_SP_GATEKEEPER, deserialized.type);
assertEquals(SOME_USER_ID, deserialized.userId);
assertEquals(DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX,
deserialized.qualityForUi);
@@ -438,7 +438,7 @@
// Make sure the constants we use on the wire do not change.
assertEquals(0, PersistentData.TYPE_NONE);
- assertEquals(1, PersistentData.TYPE_SP);
+ assertEquals(1, PersistentData.TYPE_SP_GATEKEEPER);
assertEquals(2, PersistentData.TYPE_SP_WEAVER);
}
diff --git a/services/tests/servicestests/src/com/android/server/people/data/ConversationInfoTest.java b/services/tests/servicestests/src/com/android/server/people/data/ConversationInfoTest.java
index c90064e..9f914a1 100644
--- a/services/tests/servicestests/src/com/android/server/people/data/ConversationInfoTest.java
+++ b/services/tests/servicestests/src/com/android/server/people/data/ConversationInfoTest.java
@@ -30,6 +30,8 @@
import android.content.LocusId;
import android.content.pm.ShortcutInfo;
import android.net.Uri;
+import android.util.proto.ProtoInputStream;
+import android.util.proto.ProtoOutputStream;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -270,4 +272,58 @@
assertTrue(conversationInfoFromBackup.isContactStarred());
// ConversationStatus is a transient object and not persisted
}
+
+ @Test
+ public void testBuildFromProtoPayload() throws Exception {
+ ConversationStatus cs = new ConversationStatus.Builder("id", ACTIVITY_ANNIVERSARY).build();
+ ConversationStatus cs2 = new ConversationStatus.Builder("id2", ACTIVITY_GAME).build();
+
+ ConversationInfo conversationInfo = new ConversationInfo.Builder()
+ .setShortcutId(SHORTCUT_ID)
+ .setLocusId(LOCUS_ID)
+ .setContactUri(CONTACT_URI)
+ .setContactPhoneNumber(PHONE_NUMBER)
+ .setNotificationChannelId(NOTIFICATION_CHANNEL_ID)
+ .setParentNotificationChannelId(PARENT_NOTIFICATION_CHANNEL_ID)
+ .setLastEventTimestamp(100L)
+ .setCreationTimestamp(200L)
+ .setShortcutFlags(ShortcutInfo.FLAG_LONG_LIVED
+ | ShortcutInfo.FLAG_CACHED_NOTIFICATIONS)
+ .setImportant(true)
+ .setNotificationSilenced(true)
+ .setBubbled(true)
+ .setDemoted(true)
+ .setPersonImportant(true)
+ .setPersonBot(true)
+ .setContactStarred(true)
+ .addOrUpdateStatus(cs)
+ .addOrUpdateStatus(cs2)
+ .build();
+
+ final ProtoOutputStream protoOutputStream = new ProtoOutputStream();
+ conversationInfo.writeToProto(protoOutputStream);
+ ConversationInfo conversationInfoFromBackup =
+ ConversationInfo.readFromProto(new ProtoInputStream(protoOutputStream.getBytes()));
+
+ assertEquals(SHORTCUT_ID, conversationInfoFromBackup.getShortcutId());
+ assertEquals(LOCUS_ID, conversationInfoFromBackup.getLocusId());
+ assertEquals(CONTACT_URI, conversationInfoFromBackup.getContactUri());
+ assertEquals(PHONE_NUMBER, conversationInfoFromBackup.getContactPhoneNumber());
+ assertEquals(
+ NOTIFICATION_CHANNEL_ID, conversationInfoFromBackup.getNotificationChannelId());
+ assertEquals(PARENT_NOTIFICATION_CHANNEL_ID,
+ conversationInfoFromBackup.getParentNotificationChannelId());
+ assertEquals(100L, conversationInfoFromBackup.getLastEventTimestamp());
+ assertEquals(200L, conversationInfoFromBackup.getCreationTimestamp());
+ assertTrue(conversationInfoFromBackup.isShortcutLongLived());
+ assertTrue(conversationInfoFromBackup.isShortcutCachedForNotification());
+ assertTrue(conversationInfoFromBackup.isImportant());
+ assertTrue(conversationInfoFromBackup.isNotificationSilenced());
+ assertTrue(conversationInfoFromBackup.isBubbled());
+ assertTrue(conversationInfoFromBackup.isDemoted());
+ assertTrue(conversationInfoFromBackup.isPersonImportant());
+ assertTrue(conversationInfoFromBackup.isPersonBot());
+ assertTrue(conversationInfoFromBackup.isContactStarred());
+ // ConversationStatus is a transient object and not persisted
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java b/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java
index 050fbea..304344e 100644
--- a/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java
@@ -1678,7 +1678,7 @@
mParentNotificationChannel.getImportance(),
null, null,
mParentNotificationChannel, null, null, true, 0, false, -1, false, null, null,
- false, false, false, null, 0, false);
+ false, false, false, null, 0, false, 0);
return true;
}).when(mRankingMap).getRanking(eq(GENERIC_KEY),
any(NotificationListenerService.Ranking.class));
@@ -1704,7 +1704,7 @@
mNotificationChannel.getImportance(),
null, null,
mNotificationChannel, null, null, true, 0, false, -1, false, null, null, false,
- false, false, null, 0, false);
+ false, false, null, 0, false, 0);
return true;
}).when(mRankingMap).getRanking(eq(CUSTOM_KEY),
any(NotificationListenerService.Ranking.class));
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
index 810b294..5059ef3 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
@@ -619,6 +619,30 @@
@MediumTest
@Test
+ public void testRevokeUserAdmin() throws Exception {
+ UserInfo userInfo = createUser("Admin", /*flags=*/ UserInfo.FLAG_ADMIN);
+ assertThat(userInfo.isAdmin()).isTrue();
+
+ mUserManager.revokeUserAdmin(userInfo.id);
+
+ userInfo = mUserManager.getUserInfo(userInfo.id);
+ assertThat(userInfo.isAdmin()).isFalse();
+ }
+
+ @MediumTest
+ @Test
+ public void testRevokeUserAdminFromNonAdmin() throws Exception {
+ UserInfo userInfo = createUser("NonAdmin", /*flags=*/ 0);
+ assertThat(userInfo.isAdmin()).isFalse();
+
+ mUserManager.revokeUserAdmin(userInfo.id);
+
+ userInfo = mUserManager.getUserInfo(userInfo.id);
+ assertThat(userInfo.isAdmin()).isFalse();
+ }
+
+ @MediumTest
+ @Test
public void testGetProfileParent() throws Exception {
assumeManagedUsersSupported();
final int primaryUserId = mUserManager.getPrimaryUser().id;
diff --git a/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java b/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java
index 308a4b6..dcf1b35 100644
--- a/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java
+++ b/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java
@@ -295,7 +295,7 @@
}
@Override
- boolean hasExactAlarmPermission(String packageName, int uid) {
+ boolean shouldGetExactAlarmBucketElevation(String packageName, int uid) {
return mClockApps.contains(Pair.create(packageName, uid));
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java
index 12cd834..8a99c2c 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java
@@ -193,7 +193,8 @@
tweak.isConversation(),
tweak.getConversationShortcutInfo(),
tweak.getRankingAdjustment(),
- tweak.isBubble()
+ tweak.isBubble(),
+ tweak.getProposedImportance()
);
assertNotEquals(nru, nru2);
}
@@ -274,7 +275,8 @@
isConversation(i),
getShortcutInfo(i),
getRankingAdjustment(i),
- isBubble(i)
+ isBubble(i),
+ getProposedImportance(i)
);
rankings[i] = ranking;
}
@@ -402,6 +404,10 @@
return index % 3 - 1;
}
+ private int getProposedImportance(int index) {
+ return index % 5 - 1;
+ }
+
private boolean isBubble(int index) {
return index % 4 == 0;
}
@@ -443,6 +449,7 @@
assertEquals(comment, a.getConversationShortcutInfo().getId(),
b.getConversationShortcutInfo().getId());
assertActionsEqual(a.getSmartActions(), b.getSmartActions());
+ assertEquals(a.getProposedImportance(), b.getProposedImportance());
}
private void detailedAssertEquals(RankingMap a, RankingMap b) {
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordExtractorDataTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordExtractorDataTest.java
new file mode 100644
index 0000000..87e86cb
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordExtractorDataTest.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.notification;
+
+import static android.app.NotificationManager.IMPORTANCE_HIGH;
+import static android.app.NotificationManager.IMPORTANCE_LOW;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.assertTrue;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.graphics.drawable.Icon;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.service.notification.Adjustment;
+import android.service.notification.SnoozeCriterion;
+import android.service.notification.StatusBarNotification;
+
+import com.android.server.UiServiceTestCase;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Objects;
+
+public class NotificationRecordExtractorDataTest extends UiServiceTestCase {
+
+ @Test
+ public void testHasDiffs_noDiffs() {
+ NotificationRecord r = generateRecord();
+
+ NotificationRecordExtractorData extractorData = new NotificationRecordExtractorData(
+ 1,
+ r.getPackageVisibilityOverride(),
+ r.canShowBadge(),
+ r.canBubble(),
+ r.getNotification().isBubbleNotification(),
+ r.getChannel(),
+ r.getGroupKey(),
+ r.getPeopleOverride(),
+ r.getSnoozeCriteria(),
+ r.getUserSentiment(),
+ r.getSuppressedVisualEffects(),
+ r.getSystemGeneratedSmartActions(),
+ r.getSmartReplies(),
+ r.getImportance(),
+ r.getRankingScore(),
+ r.isConversation(),
+ r.getProposedImportance());
+
+ assertFalse(extractorData.hasDiffForRankingLocked(r, 1));
+ assertFalse(extractorData.hasDiffForLoggingLocked(r, 1));
+ }
+
+ @Test
+ public void testHasDiffs_proposedImportanceChange() {
+ NotificationRecord r = generateRecord();
+
+ NotificationRecordExtractorData extractorData = new NotificationRecordExtractorData(
+ 1,
+ r.getPackageVisibilityOverride(),
+ r.canShowBadge(),
+ r.canBubble(),
+ r.getNotification().isBubbleNotification(),
+ r.getChannel(),
+ r.getGroupKey(),
+ r.getPeopleOverride(),
+ r.getSnoozeCriteria(),
+ r.getUserSentiment(),
+ r.getSuppressedVisualEffects(),
+ r.getSystemGeneratedSmartActions(),
+ r.getSmartReplies(),
+ r.getImportance(),
+ r.getRankingScore(),
+ r.isConversation(),
+ r.getProposedImportance());
+
+ Bundle signals = new Bundle();
+ signals.putInt(Adjustment.KEY_IMPORTANCE_PROPOSAL, IMPORTANCE_HIGH);
+ Adjustment adjustment = new Adjustment("pkg", r.getKey(), signals, "", 0);
+ r.addAdjustment(adjustment);
+ r.applyAdjustments();
+
+ assertTrue(extractorData.hasDiffForRankingLocked(r, 1));
+ assertTrue(extractorData.hasDiffForLoggingLocked(r, 1));
+ }
+
+ private NotificationRecord generateRecord() {
+ NotificationChannel channel = new NotificationChannel("a", "a", IMPORTANCE_LOW);
+ final Notification.Builder builder = new Notification.Builder(getContext())
+ .setContentTitle("foo")
+ .setSmallIcon(android.R.drawable.sym_def_app_icon);
+ Notification n = builder.build();
+ StatusBarNotification sbn = new StatusBarNotification("", "", 0, "", 0,
+ 0, n, UserHandle.ALL, null, System.currentTimeMillis());
+ return new NotificationRecord(getContext(), sbn, channel);
+ }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java
index 5468220..14b0048 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java
@@ -19,6 +19,7 @@
import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
import static android.app.NotificationManager.IMPORTANCE_HIGH;
import static android.app.NotificationManager.IMPORTANCE_LOW;
+import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED;
import static android.service.notification.Adjustment.KEY_IMPORTANCE;
import static android.service.notification.Adjustment.KEY_NOT_CONVERSATION;
import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING;
@@ -755,6 +756,24 @@
}
@Test
+ public void testProposedImportance() {
+ StatusBarNotification sbn = getNotification(PKG_O, true /* noisy */,
+ true /* defaultSound */, false /* buzzy */, false /* defaultBuzz */,
+ false /* lights */, false /* defaultLights */, groupId /* group */);
+ NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel);
+
+ assertEquals(IMPORTANCE_UNSPECIFIED, record.getProposedImportance());
+
+ Bundle signals = new Bundle();
+ signals.putInt(Adjustment.KEY_IMPORTANCE_PROPOSAL, IMPORTANCE_DEFAULT);
+ record.addAdjustment(new Adjustment(mPkg, record.getKey(), signals, null, sbn.getUserId()));
+
+ record.applyAdjustments();
+
+ assertEquals(IMPORTANCE_DEFAULT, record.getProposedImportance());
+ }
+
+ @Test
public void testAppImportance_returnsCorrectly() {
StatusBarNotification sbn = getNotification(PKG_O, true /* noisy */,
true /* defaultSound */, false /* buzzy */, false /* defaultBuzz */,
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java
index b16ca8b..b4a294d 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java
@@ -19,6 +19,7 @@
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.fail;
+import android.os.Parcel;
import android.service.notification.ZenPolicy;
import android.service.notification.nano.DNDPolicyProto;
import android.test.suitebuilder.annotation.SmallTest;
@@ -32,9 +33,13 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+
@SmallTest
@RunWith(AndroidJUnit4.class)
public class ZenPolicyTest extends UiServiceTestCase {
+ private static final String CLASS = "android.service.notification.ZenPolicy";
@Test
public void testZenPolicyApplyAllowedToDisallowed() {
@@ -524,6 +529,66 @@
assertProtoMatches(policy, policy.toProto());
}
+ @Test
+ public void testTooLongLists_fromParcel() {
+ ArrayList<Integer> longList = new ArrayList<Integer>(50);
+ for (int i = 0; i < 50; i++) {
+ longList.add(ZenPolicy.STATE_UNSET);
+ }
+
+ ZenPolicy.Builder builder = new ZenPolicy.Builder();
+ ZenPolicy policy = builder.build();
+
+ try {
+ Field priorityCategories = Class.forName(CLASS).getDeclaredField(
+ "mPriorityCategories");
+ priorityCategories.setAccessible(true);
+ priorityCategories.set(policy, longList);
+
+ Field visualEffects = Class.forName(CLASS).getDeclaredField("mVisualEffects");
+ visualEffects.setAccessible(true);
+ visualEffects.set(policy, longList);
+ } catch (NoSuchFieldException e) {
+ fail(e.toString());
+ } catch (ClassNotFoundException e) {
+ fail(e.toString());
+ } catch (IllegalAccessException e) {
+ fail(e.toString());
+ }
+
+ Parcel parcel = Parcel.obtain();
+ policy.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+
+ ZenPolicy fromParcel = ZenPolicy.CREATOR.createFromParcel(parcel);
+
+ // Confirm that all the fields are accessible and UNSET
+ assertAllPriorityCategoriesUnsetExcept(fromParcel, -1);
+ assertAllVisualEffectsUnsetExcept(fromParcel, -1);
+
+ // Because we don't access the lists directly, we also need to use reflection to make sure
+ // the lists are the right length.
+ try {
+ Field priorityCategories = Class.forName(CLASS).getDeclaredField(
+ "mPriorityCategories");
+ priorityCategories.setAccessible(true);
+ ArrayList<Integer> pcList = (ArrayList<Integer>) priorityCategories.get(fromParcel);
+ assertEquals(ZenPolicy.NUM_PRIORITY_CATEGORIES, pcList.size());
+
+
+ Field visualEffects = Class.forName(CLASS).getDeclaredField("mVisualEffects");
+ visualEffects.setAccessible(true);
+ ArrayList<Integer> veList = (ArrayList<Integer>) visualEffects.get(fromParcel);
+ assertEquals(ZenPolicy.NUM_VISUAL_EFFECTS, veList.size());
+ } catch (NoSuchFieldException e) {
+ fail(e.toString());
+ } catch (ClassNotFoundException e) {
+ fail(e.toString());
+ } catch (IllegalAccessException e) {
+ fail(e.toString());
+ }
+ }
+
private void assertAllPriorityCategoriesUnsetExcept(ZenPolicy policy, int except) {
if (except != ZenPolicy.PRIORITY_CATEGORY_REMINDERS) {
assertEquals(ZenPolicy.STATE_UNSET, policy.getPriorityCategoryReminders());
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 5eecb0a..8f110a88 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
@@ -1748,7 +1748,7 @@
TaskFragment inTaskFragment) {
starter.startActivityInner(target, source, null /* voiceSession */,
null /* voiceInteractor */, 0 /* startFlags */,
- options, inTask, inTaskFragment, false /* restrictedBgActivity */,
- null /* intentGrants */);
+ options, inTask, inTaskFragment,
+ BackgroundActivityStartController.BAL_ALLOW_DEFAULT, null /* intentGrants */);
}
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
index 1b77c95..9a786d4 100644
--- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
@@ -37,6 +37,8 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
import android.os.RemoteException;
import android.platform.test.annotations.Presubmit;
import android.view.WindowManager;
@@ -223,10 +225,15 @@
CountDownLatch systemLatch = new CountDownLatch(1);
CountDownLatch appLatch = new CountDownLatch(1);
+ final ApplicationInfo info = mock(ApplicationInfo.class);
+ final Context context = mock(Context.class);
+ Mockito.doReturn(true).when(info).isOnBackInvokedCallbackEnabled();
+ Mockito.doReturn(info).when(context).getApplicationInfo();
+
Task task = createTopTaskWithActivity();
WindowState appWindow = task.getTopVisibleAppMainWindow();
WindowOnBackInvokedDispatcher dispatcher =
- new WindowOnBackInvokedDispatcher(true /* applicationCallbackEnabled */);
+ new WindowOnBackInvokedDispatcher(context);
doAnswer(invocation -> {
appWindow.setOnBackInvokedCallbackInfo(invocation.getArgument(1));
return null;
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 92dd047..4ad8516 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java
@@ -16,7 +16,6 @@
package com.android.server.wm;
-
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR;
import static android.view.Display.INVALID_DISPLAY;
@@ -281,6 +280,64 @@
}
@Test
+ public void testStartRecording_notifiesCallback() {
+ // WHEN a recording is ongoing.
+ mContentRecorder.setContentRecordingSession(mTaskSession);
+ mContentRecorder.updateRecording();
+ assertThat(mContentRecorder.isCurrentlyRecording()).isTrue();
+
+ // THEN the visibility change callback is notified.
+ verify(mMediaProjectionManagerWrapper)
+ .notifyActiveProjectionCapturedContentVisibilityChanged(true);
+ }
+
+ @Test
+ public void testOnVisibleRequestedChanged_notifiesCallback() {
+ // WHEN a recording is ongoing.
+ mContentRecorder.setContentRecordingSession(mTaskSession);
+ mContentRecorder.updateRecording();
+ assertThat(mContentRecorder.isCurrentlyRecording()).isTrue();
+
+ // WHEN the child requests a visibility change.
+ boolean isVisibleRequested = true;
+ mContentRecorder.onVisibleRequestedChanged(isVisibleRequested);
+
+ // THEN the visibility change callback is notified.
+ verify(mMediaProjectionManagerWrapper, atLeastOnce())
+ .notifyActiveProjectionCapturedContentVisibilityChanged(isVisibleRequested);
+
+ // WHEN the child requests a visibility change.
+ isVisibleRequested = false;
+ mContentRecorder.onVisibleRequestedChanged(isVisibleRequested);
+
+ // THEN the visibility change callback is notified.
+ verify(mMediaProjectionManagerWrapper)
+ .notifyActiveProjectionCapturedContentVisibilityChanged(isVisibleRequested);
+ }
+
+ @Test
+ public void testOnVisibleRequestedChanged_noRecording_doesNotNotifyCallback() {
+ // WHEN a recording is not ongoing.
+ assertThat(mContentRecorder.isCurrentlyRecording()).isFalse();
+
+ // WHEN the child requests a visibility change.
+ boolean isVisibleRequested = true;
+ mContentRecorder.onVisibleRequestedChanged(isVisibleRequested);
+
+ // THEN the visibility change callback is not notified.
+ verify(mMediaProjectionManagerWrapper, never())
+ .notifyActiveProjectionCapturedContentVisibilityChanged(isVisibleRequested);
+
+ // WHEN the child requests a visibility change.
+ isVisibleRequested = false;
+ mContentRecorder.onVisibleRequestedChanged(isVisibleRequested);
+
+ // THEN the visibility change callback is not notified.
+ verify(mMediaProjectionManagerWrapper, never())
+ .notifyActiveProjectionCapturedContentVisibilityChanged(isVisibleRequested);
+ }
+
+ @Test
public void testPauseRecording_pausesRecording() {
mContentRecorder.setContentRecordingSession(mDisplaySession);
mContentRecorder.updateRecording();
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
new file mode 100644
index 0000000..8bb79e3
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
@@ -0,0 +1,429 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE;
+import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LOCKED;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+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.Surface.ROTATION_0;
+import static android.view.Surface.ROTATION_90;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.app.servertransaction.ClientTransaction;
+import android.app.servertransaction.RefreshCallbackItem;
+import android.app.servertransaction.ResumeActivityItem;
+import android.content.ComponentName;
+import android.content.pm.ActivityInfo.ScreenOrientation;
+import android.content.res.Configuration;
+import android.content.res.Configuration.Orientation;
+import android.hardware.camera2.CameraManager;
+import android.os.Handler;
+import android.platform.test.annotations.Presubmit;
+import android.view.Display;
+import android.view.Surface.Rotation;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Tests for {@link DisplayRotationCompatPolicy}.
+ *
+ * Build/Install/Run:
+ * atest WmTests:DisplayRotationCompatPolicyTests
+ */
+@SmallTest
+@Presubmit
+@RunWith(WindowTestRunner.class)
+public final class DisplayRotationCompatPolicyTests extends WindowTestsBase {
+
+ private static final String TEST_PACKAGE_1 = "com.test.package.one";
+ private static final String TEST_PACKAGE_2 = "com.test.package.two";
+ private static final String CAMERA_ID_1 = "camera-1";
+ private static final String CAMERA_ID_2 = "camera-2";
+
+ private CameraManager mMockCameraManager;
+ private Handler mMockHandler;
+ private LetterboxConfiguration mLetterboxConfiguration;
+
+ private DisplayRotationCompatPolicy mDisplayRotationCompatPolicy;
+ private CameraManager.AvailabilityCallback mCameraAvailabilityCallback;
+
+ private ActivityRecord mActivity;
+ private Task mTask;
+
+ @Before
+ public void setUp() throws Exception {
+ mLetterboxConfiguration = mDisplayContent.mWmService.mLetterboxConfiguration;
+ spyOn(mLetterboxConfiguration);
+ when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
+ /* checkDeviceConfig */ anyBoolean()))
+ .thenReturn(true);
+ when(mLetterboxConfiguration.isCameraCompatRefreshEnabled())
+ .thenReturn(true);
+ when(mLetterboxConfiguration.isCameraCompatRefreshCycleThroughStopEnabled())
+ .thenReturn(true);
+
+ mMockCameraManager = mock(CameraManager.class);
+ doAnswer(invocation -> {
+ mCameraAvailabilityCallback = invocation.getArgument(1);
+ return null;
+ }).when(mMockCameraManager).registerAvailabilityCallback(
+ any(Executor.class), any(CameraManager.AvailabilityCallback.class));
+
+ spyOn(mContext);
+ when(mContext.getSystemService(CameraManager.class)).thenReturn(mMockCameraManager);
+
+ spyOn(mDisplayContent);
+
+ mDisplayContent.setIgnoreOrientationRequest(true);
+
+ mMockHandler = mock(Handler.class);
+
+ when(mMockHandler.postDelayed(any(Runnable.class), anyLong())).thenAnswer(
+ invocation -> {
+ ((Runnable) invocation.getArgument(0)).run();
+ return null;
+ });
+ mDisplayRotationCompatPolicy = new DisplayRotationCompatPolicy(
+ mDisplayContent, mMockHandler);
+ }
+
+ @Test
+ public void testTreatmentNotEnabled_noForceRotationOrRefresh() throws Exception {
+ when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
+ /* checkDeviceConfig */ anyBoolean()))
+ .thenReturn(false);
+
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ assertEquals(mDisplayRotationCompatPolicy.getOrientation(),
+ SCREEN_ORIENTATION_UNSPECIFIED);
+
+ assertNoForceRotationOrRefresh();
+ }
+
+ @Test
+ public void testTreatmentDisabledViaDeviceConfig_noForceRotationOrRefresh() throws Exception {
+ when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
+ /* checkDeviceConfig */ true))
+ .thenReturn(false);
+
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ assertNoForceRotationOrRefresh();
+ }
+
+ @Test
+ public void testMultiWindowMode_returnUnspecified_noForceRotationOrRefresh() throws Exception {
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ final TestSplitOrganizer organizer = new TestSplitOrganizer(mAtm, mDisplayContent);
+ mActivity.getTask().reparent(organizer.mPrimary, WindowContainer.POSITION_TOP,
+ false /* moveParents */, "test" /* reason */);
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ assertTrue(mActivity.inMultiWindowMode());
+ assertNoForceRotationOrRefresh();
+ }
+
+ @Test
+ public void testOrientationUnspecified_noForceRotationOrRefresh() throws Exception {
+ configureActivity(SCREEN_ORIENTATION_UNSPECIFIED);
+
+ assertNoForceRotationOrRefresh();
+ }
+
+ @Test
+ public void testOrientationLocked_noForceRotationOrRefresh() throws Exception {
+ configureActivity(SCREEN_ORIENTATION_LOCKED);
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ assertNoForceRotationOrRefresh();
+ }
+
+ @Test
+ public void testOrientationNoSensor_noForceRotationOrRefresh() throws Exception {
+ configureActivity(SCREEN_ORIENTATION_NOSENSOR);
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ assertNoForceRotationOrRefresh();
+ }
+
+ @Test
+ public void testIgnoreOrientationRequestIsFalse_noForceRotationOrRefresh() throws Exception {
+ mDisplayContent.setIgnoreOrientationRequest(false);
+
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ assertNoForceRotationOrRefresh();
+ }
+
+ @Test
+ public void testDisplayNotInternal_noForceRotationOrRefresh() throws Exception {
+ Display display = mDisplayContent.getDisplay();
+ spyOn(display);
+
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ when(display.getType()).thenReturn(Display.TYPE_EXTERNAL);
+ assertNoForceRotationOrRefresh();
+
+ when(display.getType()).thenReturn(Display.TYPE_WIFI);
+ assertNoForceRotationOrRefresh();
+
+ when(display.getType()).thenReturn(Display.TYPE_OVERLAY);
+ assertNoForceRotationOrRefresh();
+
+ when(display.getType()).thenReturn(Display.TYPE_VIRTUAL);
+ assertNoForceRotationOrRefresh();
+ }
+
+ @Test
+ public void testNoCameraConnection_noForceRotationOrRefresh() throws Exception {
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+
+ assertNoForceRotationOrRefresh();
+ }
+
+ @Test
+ public void testCameraReconnected_forceRotationAndRefresh() throws Exception {
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ mCameraAvailabilityCallback.onCameraClosed(CAMERA_ID_1);
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ true);
+
+ assertEquals(mDisplayRotationCompatPolicy.getOrientation(),
+ SCREEN_ORIENTATION_PORTRAIT);
+ assertActivityRefreshRequested(/* refreshRequested */ true);
+ }
+
+ @Test
+ public void testReconnectedToDifferentCamera_forceRotationAndRefresh() throws Exception {
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ mCameraAvailabilityCallback.onCameraClosed(CAMERA_ID_1);
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_2, TEST_PACKAGE_1);
+ callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ true);
+
+ assertEquals(mDisplayRotationCompatPolicy.getOrientation(),
+ SCREEN_ORIENTATION_PORTRAIT);
+ assertActivityRefreshRequested(/* refreshRequested */ true);
+ }
+
+ @Test
+ public void testGetOrientation_cameraConnectionClosed_returnUnspecified() {
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ assertEquals(mDisplayRotationCompatPolicy.getOrientation(),
+ SCREEN_ORIENTATION_PORTRAIT);
+
+ mCameraAvailabilityCallback.onCameraClosed(CAMERA_ID_1);
+
+ assertEquals(mDisplayRotationCompatPolicy.getOrientation(),
+ SCREEN_ORIENTATION_UNSPECIFIED);
+ }
+
+ @Test
+ public void testCameraOpenedForDifferentPackage_noForceRotationOrRefresh() throws Exception {
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_2);
+
+ assertNoForceRotationOrRefresh();
+ }
+
+ @Test
+ public void testGetOrientation_portraitActivity_portraitNaturalOrientation_returnPortrait() {
+ testGetOrientationForActivityAndNaturalOrientations(
+ /* activityOrientation */ SCREEN_ORIENTATION_PORTRAIT,
+ /* naturalOrientation */ ORIENTATION_PORTRAIT,
+ /* expectedOrientation */ SCREEN_ORIENTATION_PORTRAIT);
+ }
+
+ @Test
+ public void testGetOrientation_portraitActivity_landscapeNaturalOrientation_returnLandscape() {
+ testGetOrientationForActivityAndNaturalOrientations(
+ /* activityOrientation */ SCREEN_ORIENTATION_PORTRAIT,
+ /* naturalOrientation */ ORIENTATION_LANDSCAPE,
+ /* expectedOrientation */ SCREEN_ORIENTATION_LANDSCAPE);
+ }
+
+ @Test
+ public void testGetOrientation_landscapeActivity_portraitNaturalOrientation_returnLandscape() {
+ testGetOrientationForActivityAndNaturalOrientations(
+ /* activityOrientation */ SCREEN_ORIENTATION_LANDSCAPE,
+ /* naturalOrientation */ ORIENTATION_PORTRAIT,
+ /* expectedOrientation */ SCREEN_ORIENTATION_LANDSCAPE);
+ }
+
+ @Test
+ public void testGetOrientation_landscapeActivity_landscapeNaturalOrientation_returnPortrait() {
+ testGetOrientationForActivityAndNaturalOrientations(
+ /* activityOrientation */ SCREEN_ORIENTATION_LANDSCAPE,
+ /* naturalOrientation */ ORIENTATION_LANDSCAPE,
+ /* expectedOrientation */ SCREEN_ORIENTATION_PORTRAIT);
+ }
+
+ private void testGetOrientationForActivityAndNaturalOrientations(
+ @ScreenOrientation int activityOrientation,
+ @Orientation int naturalOrientation,
+ @ScreenOrientation int expectedOrientation) {
+ configureActivityAndDisplay(activityOrientation, naturalOrientation);
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ assertEquals(mDisplayRotationCompatPolicy.getOrientation(),
+ expectedOrientation);
+ }
+
+ @Test
+ public void testOnActivityConfigurationChanging_refreshDisabled_noRefresh() throws Exception {
+ when(mLetterboxConfiguration.isCameraCompatRefreshEnabled()).thenReturn(false);
+
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ true);
+
+ assertActivityRefreshRequested(/* refreshRequested */ false);
+ }
+
+ @Test
+ public void testOnActivityConfigurationChanging_displayRotationNotChanging_noRefresh()
+ throws Exception {
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ false);
+
+ assertActivityRefreshRequested(/* refreshRequested */ false);
+ }
+
+ @Test
+ public void testOnActivityConfigurationChanging_cycleThroughStopDisabled() throws Exception {
+ when(mLetterboxConfiguration.isCameraCompatRefreshCycleThroughStopEnabled())
+ .thenReturn(false);
+
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ true);
+
+ assertActivityRefreshRequested(/* refreshRequested */ true, /* cycleThroughStop */ false);
+ }
+
+ private void configureActivity(@ScreenOrientation int activityOrientation) {
+ configureActivityAndDisplay(activityOrientation, ORIENTATION_PORTRAIT);
+ }
+
+ private void configureActivityAndDisplay(@ScreenOrientation int activityOrientation,
+ @Orientation int naturalOrientation) {
+
+ mTask = new TaskBuilder(mSupervisor)
+ .setDisplay(mDisplayContent)
+ .build();
+
+ mActivity = new ActivityBuilder(mAtm)
+ .setComponent(new ComponentName(TEST_PACKAGE_1, ".TestActivity"))
+ .setScreenOrientation(activityOrientation)
+ .setTask(mTask)
+ .build();
+
+ spyOn(mActivity.mAtmService.getLifecycleManager());
+ spyOn(mActivity.mLetterboxUiController);
+
+ doReturn(mActivity).when(mDisplayContent).topRunningActivity(anyBoolean());
+ doReturn(naturalOrientation).when(mDisplayContent).getNaturalOrientation();
+ }
+
+ private void assertActivityRefreshRequested(boolean refreshRequested) throws Exception {
+ assertActivityRefreshRequested(refreshRequested, /* cycleThroughStop*/ true);
+ }
+
+ private void assertActivityRefreshRequested(boolean refreshRequested,
+ boolean cycleThroughStop) throws Exception {
+ verify(mActivity.mLetterboxUiController, times(refreshRequested ? 1 : 0))
+ .setIsRefreshAfterRotationRequested(true);
+
+ final ClientTransaction transaction = ClientTransaction.obtain(
+ mActivity.app.getThread(), mActivity.token);
+ transaction.addCallback(RefreshCallbackItem.obtain(cycleThroughStop ? ON_STOP : ON_PAUSE));
+ transaction.setLifecycleStateRequest(ResumeActivityItem.obtain(
+ /* isForward */ false, /* shouldSendCompatFakeFocus */ false));
+
+ verify(mActivity.mAtmService.getLifecycleManager(), times(refreshRequested ? 1 : 0))
+ .scheduleTransaction(eq(transaction));
+ }
+
+ private void assertNoForceRotationOrRefresh() throws Exception {
+ callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ true);
+
+ assertEquals(mDisplayRotationCompatPolicy.getOrientation(),
+ SCREEN_ORIENTATION_UNSPECIFIED);
+ assertActivityRefreshRequested(/* refreshRequested */ false);
+ }
+
+ private void callOnActivityConfigurationChanging(
+ ActivityRecord activity, boolean isDisplayRotationChanging) {
+ mDisplayRotationCompatPolicy.onActivityConfigurationChanging(activity,
+ /* newConfig */ createConfigurationWithDisplayRotation(ROTATION_0),
+ /* newConfig */ createConfigurationWithDisplayRotation(
+ isDisplayRotationChanging ? ROTATION_90 : ROTATION_0));
+ }
+
+ private static Configuration createConfigurationWithDisplayRotation(@Rotation int rotation) {
+ final Configuration config = new Configuration();
+ config.windowConfiguration.setDisplayRotation(rotation);
+ return config;
+ }
+}
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 3c5bc17..b2abd44 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowPolicyControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowPolicyControllerTests.java
@@ -188,7 +188,7 @@
/* options */null,
/* inTask */null,
/* inTaskFragment */ null,
- /* restrictedBgActivity */false,
+ /* balCode */ BackgroundActivityStartController.BAL_ALLOW_DEFAULT,
/* intentGrants */null);
assertEquals(result, START_ABORTED);
@@ -212,7 +212,7 @@
/* options= */null,
/* inTask= */null,
/* inTaskFragment= */ null,
- /* restrictedBgActivity= */false,
+ /* balCode= */ BackgroundActivityStartController.BAL_ALLOW_DEFAULT,
/* intentGrants= */null);
assertEquals(result, START_ABORTED);
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java
index 1be9de7..51a7e74 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java
@@ -67,6 +67,11 @@
R.integer.config_letterboxDefaultPositionForHorizontalReachability),
() -> mContext.getResources().getInteger(
R.integer.config_letterboxDefaultPositionForVerticalReachability),
+ () -> mContext.getResources().getInteger(
+ R.integer.config_letterboxDefaultPositionForBookModeReachability),
+ () -> mContext.getResources().getInteger(
+ R.integer.config_letterboxDefaultPositionForTabletopModeReachability
+ ),
mConfigFolder, mPersisterQueue, mQueueState);
mQueueListener = queueEmpty -> mQueueState.onItemAdded();
mPersisterQueue.addListener(mQueueListener);
@@ -84,14 +89,15 @@
@Test
public void test_whenStoreIsCreated_valuesAreDefaults() {
final int positionForHorizontalReachability =
- mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
+ mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(
+ false);
final int defaultPositionForHorizontalReachability =
mContext.getResources().getInteger(
R.integer.config_letterboxDefaultPositionForHorizontalReachability);
Assert.assertEquals(defaultPositionForHorizontalReachability,
positionForHorizontalReachability);
final int positionForVerticalReachability =
- mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
+ mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(false);
final int defaultPositionForVerticalReachability =
mContext.getResources().getInteger(
R.integer.config_letterboxDefaultPositionForVerticalReachability);
@@ -101,15 +107,16 @@
@Test
public void test_whenUpdatedWithNewValues_valuesAreWritten() {
- mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability(
+ mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability(false,
LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT);
- mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability(
+ mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability(false,
LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP);
waitForCompletion(mPersisterQueue);
final int newPositionForHorizontalReachability =
- mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
+ mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(
+ false);
final int newPositionForVerticalReachability =
- mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
+ mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(false);
Assert.assertEquals(LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
newPositionForHorizontalReachability);
Assert.assertEquals(LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
@@ -120,24 +127,24 @@
public void test_whenUpdatedWithNewValues_valuesAreReadAfterRestart() {
final PersisterQueue firstPersisterQueue = new PersisterQueue();
final LetterboxConfigurationPersister firstPersister = new LetterboxConfigurationPersister(
- mContext, () -> -1, () -> -1, mContext.getFilesDir(), firstPersisterQueue,
- mQueueState);
+ mContext, () -> -1, () -> -1, () -> -1, () -> -1, mContext.getFilesDir(),
+ firstPersisterQueue, mQueueState);
firstPersister.start();
- firstPersister.setLetterboxPositionForHorizontalReachability(
+ firstPersister.setLetterboxPositionForHorizontalReachability(false,
LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT);
- firstPersister.setLetterboxPositionForVerticalReachability(
+ firstPersister.setLetterboxPositionForVerticalReachability(false,
LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP);
waitForCompletion(firstPersisterQueue);
stopPersisterSafe(firstPersisterQueue);
final PersisterQueue secondPersisterQueue = new PersisterQueue();
final LetterboxConfigurationPersister secondPersister = new LetterboxConfigurationPersister(
- mContext, () -> -1, () -> -1, mContext.getFilesDir(), secondPersisterQueue,
- mQueueState);
+ mContext, () -> -1, () -> -1, () -> -1, () -> -1, mContext.getFilesDir(),
+ secondPersisterQueue, mQueueState);
secondPersister.start();
final int newPositionForHorizontalReachability =
- secondPersister.getLetterboxPositionForHorizontalReachability();
+ secondPersister.getLetterboxPositionForHorizontalReachability(false);
final int newPositionForVerticalReachability =
- secondPersister.getLetterboxPositionForVerticalReachability();
+ secondPersister.getLetterboxPositionForVerticalReachability(false);
Assert.assertEquals(LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
newPositionForHorizontalReachability);
Assert.assertEquals(LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
@@ -149,15 +156,16 @@
@Test
public void test_whenUpdatedWithNewValuesAndDeleted_valuesAreDefaults() {
- mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability(
+ mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability(false,
LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT);
- mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability(
+ mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability(false,
LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP);
waitForCompletion(mPersisterQueue);
final int newPositionForHorizontalReachability =
- mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
+ mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(
+ false);
final int newPositionForVerticalReachability =
- mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
+ mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(false);
Assert.assertEquals(LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
newPositionForHorizontalReachability);
Assert.assertEquals(LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
@@ -165,14 +173,15 @@
deleteConfiguration(mLetterboxConfigurationPersister, mPersisterQueue);
waitForCompletion(mPersisterQueue);
final int positionForHorizontalReachability =
- mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
+ mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(
+ false);
final int defaultPositionForHorizontalReachability =
mContext.getResources().getInteger(
R.integer.config_letterboxDefaultPositionForHorizontalReachability);
Assert.assertEquals(defaultPositionForHorizontalReachability,
positionForHorizontalReachability);
final int positionForVerticalReachability =
- mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
+ mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(false);
final int defaultPositionForVerticalReachability =
mContext.getResources().getInteger(
R.integer.config_letterboxDefaultPositionForVerticalReachability);
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java
index c927f9e..e196704 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java
@@ -26,6 +26,7 @@
import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -39,7 +40,8 @@
import org.junit.Before;
import org.junit.Test;
-import java.util.function.Consumer;
+import java.util.Arrays;
+import java.util.function.BiConsumer;
@SmallTest
@Presubmit
@@ -58,20 +60,28 @@
@Test
public void test_whenReadingValues_storeIsInvoked() {
- mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability();
- verify(mLetterboxConfigurationPersister).getLetterboxPositionForHorizontalReachability();
- mLetterboxConfiguration.getLetterboxPositionForVerticalReachability();
- verify(mLetterboxConfigurationPersister).getLetterboxPositionForVerticalReachability();
+ for (boolean halfFoldPose : Arrays.asList(false, true)) {
+ mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability(halfFoldPose);
+ verify(mLetterboxConfigurationPersister).getLetterboxPositionForHorizontalReachability(
+ halfFoldPose);
+ mLetterboxConfiguration.getLetterboxPositionForVerticalReachability(halfFoldPose);
+ verify(mLetterboxConfigurationPersister).getLetterboxPositionForVerticalReachability(
+ halfFoldPose);
+ }
}
@Test
public void test_whenSettingValues_updateConfigurationIsInvoked() {
- mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextRightStop();
- verify(mLetterboxConfigurationPersister).setLetterboxPositionForHorizontalReachability(
- anyInt());
- mLetterboxConfiguration.movePositionForVerticalReachabilityToNextBottomStop();
- verify(mLetterboxConfigurationPersister).setLetterboxPositionForVerticalReachability(
- anyInt());
+ for (boolean halfFoldPose : Arrays.asList(false, true)) {
+ mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextRightStop(
+ halfFoldPose);
+ verify(mLetterboxConfigurationPersister).setLetterboxPositionForHorizontalReachability(
+ eq(halfFoldPose), anyInt());
+ mLetterboxConfiguration.movePositionForVerticalReachabilityToNextBottomStop(
+ halfFoldPose);
+ verify(mLetterboxConfigurationPersister).setLetterboxPositionForVerticalReachability(
+ eq(halfFoldPose), anyInt());
+ }
}
@Test
@@ -81,33 +91,65 @@
/* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER,
/* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
/* expectedTime */ 1,
+ /* halfFoldPose */ false,
LetterboxConfiguration::movePositionForHorizontalReachabilityToNextLeftStop);
assertForHorizontalMove(
/* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER,
/* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
/* expectedTime */ 1,
+ /* halfFoldPose */ false,
LetterboxConfiguration::movePositionForHorizontalReachabilityToNextRightStop);
// Starting from left
assertForHorizontalMove(
/* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
/* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
/* expectedTime */ 2,
+ /* halfFoldPose */ false,
LetterboxConfiguration::movePositionForHorizontalReachabilityToNextLeftStop);
assertForHorizontalMove(
/* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
/* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER,
/* expectedTime */ 1,
+ /* halfFoldPose */ false,
LetterboxConfiguration::movePositionForHorizontalReachabilityToNextRightStop);
// Starting from right
assertForHorizontalMove(
/* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
/* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
/* expectedTime */ 2,
+ /* halfFoldPose */ false,
LetterboxConfiguration::movePositionForHorizontalReachabilityToNextRightStop);
assertForHorizontalMove(
/* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
/* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER,
/* expectedTime */ 2,
+ /* halfFoldPose */ false,
+ LetterboxConfiguration::movePositionForHorizontalReachabilityToNextLeftStop);
+ // Starting from left - book mode
+ assertForHorizontalMove(
+ /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
+ /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
+ /* expectedTime */ 1,
+ /* halfFoldPose */ true,
+ LetterboxConfiguration::movePositionForHorizontalReachabilityToNextLeftStop);
+ assertForHorizontalMove(
+ /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
+ /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
+ /* expectedTime */ 1,
+ /* halfFoldPose */ true,
+ LetterboxConfiguration::movePositionForHorizontalReachabilityToNextRightStop);
+ // Starting from right - book mode
+ assertForHorizontalMove(
+ /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
+ /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
+ /* expectedTime */ 2,
+ /* halfFoldPose */ true,
+ LetterboxConfiguration::movePositionForHorizontalReachabilityToNextRightStop);
+ assertForHorizontalMove(
+ /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
+ /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
+ /* expectedTime */ 2,
+ /* halfFoldPose */ true,
LetterboxConfiguration::movePositionForHorizontalReachabilityToNextLeftStop);
}
@@ -118,55 +160,87 @@
/* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER,
/* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
/* expectedTime */ 1,
+ /* halfFoldPose */ false,
LetterboxConfiguration::movePositionForVerticalReachabilityToNextBottomStop);
assertForVerticalMove(
/* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER,
/* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
/* expectedTime */ 1,
+ /* halfFoldPose */ false,
LetterboxConfiguration::movePositionForVerticalReachabilityToNextTopStop);
// Starting from top
assertForVerticalMove(
/* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
/* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER,
/* expectedTime */ 1,
+ /* halfFoldPose */ false,
LetterboxConfiguration::movePositionForVerticalReachabilityToNextBottomStop);
assertForVerticalMove(
/* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
/* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
/* expectedTime */ 2,
+ /* halfFoldPose */ false,
LetterboxConfiguration::movePositionForVerticalReachabilityToNextTopStop);
// Starting from bottom
assertForVerticalMove(
/* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
/* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER,
/* expectedTime */ 2,
+ /* halfFoldPose */ false,
LetterboxConfiguration::movePositionForVerticalReachabilityToNextTopStop);
assertForVerticalMove(
/* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
/* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
/* expectedTime */ 2,
+ /* halfFoldPose */ false,
+ LetterboxConfiguration::movePositionForVerticalReachabilityToNextBottomStop);
+ // Starting from top - tabletop mode
+ assertForVerticalMove(
+ /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
+ /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
+ /* expectedTime */ 1,
+ /* halfFoldPose */ true,
+ LetterboxConfiguration::movePositionForVerticalReachabilityToNextBottomStop);
+ assertForVerticalMove(
+ /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
+ /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
+ /* expectedTime */ 1,
+ /* halfFoldPose */ true,
+ LetterboxConfiguration::movePositionForVerticalReachabilityToNextTopStop);
+ // Starting from bottom - tabletop mode
+ assertForVerticalMove(
+ /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
+ /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
+ /* expectedTime */ 2,
+ /* halfFoldPose */ true,
+ LetterboxConfiguration::movePositionForVerticalReachabilityToNextTopStop);
+ assertForVerticalMove(
+ /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
+ /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
+ /* expectedTime */ 2,
+ /* halfFoldPose */ true,
LetterboxConfiguration::movePositionForVerticalReachabilityToNextBottomStop);
}
private void assertForHorizontalMove(int from, int expected, int expectedTime,
- Consumer<LetterboxConfiguration> move) {
+ boolean halfFoldPose, BiConsumer<LetterboxConfiguration, Boolean> move) {
// We are in the current position
- when(mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability())
+ when(mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability(halfFoldPose))
.thenReturn(from);
- move.accept(mLetterboxConfiguration);
+ move.accept(mLetterboxConfiguration, halfFoldPose);
verify(mLetterboxConfigurationPersister,
- times(expectedTime)).setLetterboxPositionForHorizontalReachability(
+ times(expectedTime)).setLetterboxPositionForHorizontalReachability(halfFoldPose,
expected);
}
private void assertForVerticalMove(int from, int expected, int expectedTime,
- Consumer<LetterboxConfiguration> move) {
+ boolean halfFoldPose, BiConsumer<LetterboxConfiguration, Boolean> move) {
// We are in the current position
- when(mLetterboxConfiguration.getLetterboxPositionForVerticalReachability())
+ when(mLetterboxConfiguration.getLetterboxPositionForVerticalReachability(halfFoldPose))
.thenReturn(from);
- move.accept(mLetterboxConfiguration);
+ move.accept(mLetterboxConfiguration, halfFoldPose);
verify(mLetterboxConfigurationPersister,
- times(expectedTime)).setLetterboxPositionForVerticalReachability(
+ times(expectedTime)).setLetterboxPositionForVerticalReachability(halfFoldPose,
expected);
}
}
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 1484cb7..850bed6 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -16,6 +16,7 @@
package com.android.server.wm;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE;
import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE;
@@ -98,6 +99,7 @@
import com.android.internal.policy.SystemBarUtils;
import com.android.internal.statusbar.LetterboxDetails;
import com.android.server.statusbar.StatusBarManagerInternal;
+import com.android.server.wm.DeviceStateController.FoldState;
import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
@@ -2821,6 +2823,70 @@
}
@Test
+ public void testUpdateResolvedBoundsVerticalPosition_tabletop() {
+
+ // Set up a display in portrait with a fixed-orientation LANDSCAPE app
+ setUpDisplaySizeWithApp(1400, 2800);
+ mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+ mActivity.mWmService.mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(
+ 1.0f /*letterboxVerticalPositionMultiplier*/);
+ prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE);
+
+ Rect letterboxNoFold = new Rect(0, 2100, 1400, 2800);
+ assertEquals(letterboxNoFold, mActivity.getBounds());
+
+ // Make the activity full-screen
+ mTask.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+
+ setFoldablePosture(true /* isHalfFolded */, true /* isTabletop */);
+
+ Rect letterboxHalfFold = new Rect(0, 0, 1400, 700);
+ assertEquals(letterboxHalfFold, mActivity.getBounds());
+
+ setFoldablePosture(false /* isHalfFolded */, false /* isTabletop */);
+
+ assertEquals(letterboxNoFold, mActivity.getBounds());
+
+ }
+
+ @Test
+ public void testUpdateResolvedBoundsHorizontalPosition_book() {
+
+ // Set up a display in landscape with a fixed-orientation PORTRAIT app
+ setUpDisplaySizeWithApp(2800, 1400);
+ mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+ mActivity.mWmService.mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(
+ 1.0f /*letterboxVerticalPositionMultiplier*/);
+ prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT);
+
+ Rect letterboxNoFold = new Rect(2100, 0, 2800, 1400);
+ assertEquals(letterboxNoFold, mActivity.getBounds());
+
+ // Make the activity full-screen
+ mTask.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+
+ setFoldablePosture(true /* isHalfFolded */, false /* isTabletop */);
+
+ Rect letterboxHalfFold = new Rect(0, 0, 700, 1400);
+ assertEquals(letterboxHalfFold, mActivity.getBounds());
+
+ setFoldablePosture(false /* isHalfFolded */, false /* isTabletop */);
+
+ assertEquals(letterboxNoFold, mActivity.getBounds());
+
+ }
+
+ private void setFoldablePosture(boolean isHalfFolded, boolean isTabletop) {
+ final DisplayRotation r = mActivity.mDisplayContent.getDisplayRotation();
+ doReturn(isHalfFolded).when(r).isDisplaySeparatingHinge();
+ doReturn(false).when(r).isDeviceInPosture(any(FoldState.class), anyBoolean());
+ if (isHalfFolded) {
+ doReturn(true).when(r).isDeviceInPosture(FoldState.HALF_FOLDED, isTabletop);
+ }
+ mActivity.recomputeConfiguration();
+ }
+
+ @Test
public void testUpdateResolvedBoundsPosition_alignToTop() {
final int notchHeight = 100;
final DisplayContent display = new TestDisplayContent.Builder(mAtm, 1000, 2800)
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 2665659..d2cca9f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupTest.java
@@ -40,6 +40,7 @@
@SmallTest
@Presubmit
public class SurfaceSyncGroupTest {
+ private static final String TAG = "SurfaceSyncGroupTest";
private final Executor mExecutor = Runnable::run;
@@ -51,7 +52,7 @@
@Test
public void testSyncOne() throws InterruptedException {
final CountDownLatch finishedLatch = new CountDownLatch(1);
- SurfaceSyncGroup syncGroup = new SurfaceSyncGroup();
+ SurfaceSyncGroup syncGroup = new SurfaceSyncGroup(TAG);
syncGroup.addSyncCompleteCallback(mExecutor, finishedLatch::countDown);
SyncTarget syncTarget = new SyncTarget();
syncGroup.addToSync(syncTarget, false /* parentSyncGroupMerge */);
@@ -66,7 +67,7 @@
@Test
public void testSyncMultiple() throws InterruptedException {
final CountDownLatch finishedLatch = new CountDownLatch(1);
- SurfaceSyncGroup syncGroup = new SurfaceSyncGroup();
+ SurfaceSyncGroup syncGroup = new SurfaceSyncGroup(TAG);
syncGroup.addSyncCompleteCallback(mExecutor, finishedLatch::countDown);
SyncTarget syncTarget1 = new SyncTarget();
SyncTarget syncTarget2 = new SyncTarget();
@@ -91,7 +92,7 @@
@Test
public void testAddSyncWhenSyncComplete() {
- SurfaceSyncGroup syncGroup = new SurfaceSyncGroup();
+ SurfaceSyncGroup syncGroup = new SurfaceSyncGroup(TAG);
SyncTarget syncTarget1 = new SyncTarget();
SyncTarget syncTarget2 = new SyncTarget();
@@ -107,8 +108,8 @@
public void testMultipleSyncGroups() throws InterruptedException {
final CountDownLatch finishedLatch1 = new CountDownLatch(1);
final CountDownLatch finishedLatch2 = new CountDownLatch(1);
- SurfaceSyncGroup syncGroup1 = new SurfaceSyncGroup();
- SurfaceSyncGroup syncGroup2 = new SurfaceSyncGroup();
+ SurfaceSyncGroup syncGroup1 = new SurfaceSyncGroup(TAG);
+ SurfaceSyncGroup syncGroup2 = new SurfaceSyncGroup(TAG);
syncGroup1.addSyncCompleteCallback(mExecutor, finishedLatch1::countDown);
syncGroup2.addSyncCompleteCallback(mExecutor, finishedLatch2::countDown);
@@ -137,8 +138,8 @@
public void testAddSyncGroup() throws InterruptedException {
final CountDownLatch finishedLatch1 = new CountDownLatch(1);
final CountDownLatch finishedLatch2 = new CountDownLatch(1);
- SurfaceSyncGroup syncGroup1 = new SurfaceSyncGroup();
- SurfaceSyncGroup syncGroup2 = new SurfaceSyncGroup();
+ SurfaceSyncGroup syncGroup1 = new SurfaceSyncGroup(TAG);
+ SurfaceSyncGroup syncGroup2 = new SurfaceSyncGroup(TAG);
syncGroup1.addSyncCompleteCallback(mExecutor, finishedLatch1::countDown);
syncGroup2.addSyncCompleteCallback(mExecutor, finishedLatch2::countDown);
@@ -173,8 +174,8 @@
public void testAddSyncAlreadyComplete() throws InterruptedException {
final CountDownLatch finishedLatch1 = new CountDownLatch(1);
final CountDownLatch finishedLatch2 = new CountDownLatch(1);
- SurfaceSyncGroup syncGroup1 = new SurfaceSyncGroup();
- SurfaceSyncGroup syncGroup2 = new SurfaceSyncGroup();
+ SurfaceSyncGroup syncGroup1 = new SurfaceSyncGroup(TAG);
+ SurfaceSyncGroup syncGroup2 = new SurfaceSyncGroup(TAG);
syncGroup1.addSyncCompleteCallback(mExecutor, finishedLatch1::countDown);
syncGroup2.addSyncCompleteCallback(mExecutor, finishedLatch2::countDown);
@@ -205,8 +206,8 @@
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();
+ SurfaceSyncGroup syncGroup1 = new SurfaceSyncGroup(TAG);
+ SurfaceSyncGroup syncGroup2 = new SurfaceSyncGroup(TAG);
syncGroup1.addSyncCompleteCallback(mExecutor, finishedLatch1::countDown);
syncGroup2.addSyncCompleteCallback(mExecutor, finishedLatch2::countDown);
@@ -250,8 +251,8 @@
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();
+ SurfaceSyncGroup syncGroup1 = new SurfaceSyncGroup(TAG);
+ SurfaceSyncGroup syncGroup2 = new SurfaceSyncGroup(TAG);
syncGroup1.addSyncCompleteCallback(mExecutor, finishedLatch1::countDown);
syncGroup2.addSyncCompleteCallback(mExecutor, finishedLatch2::countDown);
@@ -295,7 +296,7 @@
SurfaceSyncGroup.setTransactionFactory(() -> parentTransaction);
final CountDownLatch finishedLatch = new CountDownLatch(1);
- SurfaceSyncGroup syncGroup = new SurfaceSyncGroup();
+ SurfaceSyncGroup syncGroup = new SurfaceSyncGroup(TAG);
syncGroup.addSyncCompleteCallback(mExecutor, finishedLatch::countDown);
SurfaceControl.Transaction targetTransaction = spy(new StubTransaction());
@@ -320,7 +321,7 @@
SurfaceSyncGroup.setTransactionFactory(() -> parentTransaction);
final CountDownLatch finishedLatch = new CountDownLatch(1);
- SurfaceSyncGroup syncGroup = new SurfaceSyncGroup();
+ SurfaceSyncGroup syncGroup = new SurfaceSyncGroup(TAG);
syncGroup.addSyncCompleteCallback(mExecutor, finishedLatch::countDown);
SurfaceControl.Transaction targetTransaction = spy(new StubTransaction());
@@ -339,7 +340,7 @@
@Test
public void testAddToSameParentNoCrash() {
final CountDownLatch finishedLatch = new CountDownLatch(1);
- SurfaceSyncGroup syncGroup = new SurfaceSyncGroup();
+ SurfaceSyncGroup syncGroup = new SurfaceSyncGroup(TAG);
syncGroup.addSyncCompleteCallback(mExecutor, finishedLatch::countDown);
SyncTarget syncTarget = new SyncTarget();
syncGroup.addToSync(syncTarget, false /* parentSyncGroupMerge */);
@@ -358,6 +359,10 @@
}
private static class SyncTarget extends SurfaceSyncGroup {
+ SyncTarget() {
+ super("FakeSyncTarget");
+ }
+
void onBufferReady() {
SurfaceControl.Transaction t = new StubTransaction();
onTransactionReady(t);
diff --git a/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupValidatorTestCase.java b/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupValidatorTestCase.java
index 2df3085..1fa0c23 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupValidatorTestCase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupValidatorTestCase.java
@@ -132,7 +132,7 @@
mLastExpanded = !mLastExpanded;
mRenderingThread.pauseRendering();
- mSyncGroup = new SurfaceSyncGroup();
+ mSyncGroup = new SurfaceSyncGroup(TAG);
mSyncGroup.addToSync(mParent.getRootSurfaceControl());
ViewGroup.LayoutParams svParams = mSurfaceView.getLayoutParams();
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
index d583e89..1a1ca54 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
@@ -56,6 +56,8 @@
import static com.android.server.wm.WindowContainer.POSITION_BOTTOM;
import static com.android.server.wm.WindowContainer.POSITION_TOP;
+import static com.google.common.truth.Truth.assertThat;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
@@ -660,6 +662,111 @@
}
@Test
+ public void testSetVisibleRequested() {
+ final TestWindowContainer root = spy(new TestWindowContainerBuilder(mWm).setLayer(
+ 0).build());
+ assertThat(root.isVisibleRequested()).isFalse();
+ final TestWindowContainerListener listener = new TestWindowContainerListener();
+ root.registerWindowContainerListener(listener);
+
+ assertThat(root.setVisibleRequested(/* isVisible= */ false)).isFalse();
+ assertThat(root.isVisibleRequested()).isFalse();
+
+ assertThat(root.setVisibleRequested(/* isVisible= */ true)).isTrue();
+ assertThat(root.isVisibleRequested()).isTrue();
+ assertThat(listener.mIsVisibleRequested).isTrue();
+ }
+
+ @Test
+ public void testSetVisibleRequested_childRequestsVisible() {
+ final TestWindowContainer root = spy(new TestWindowContainerBuilder(mWm).setLayer(
+ 0).build());
+ final TestWindowContainer child1 = root.addChildWindow();
+ assertThat(child1.isVisibleRequested()).isFalse();
+ final TestWindowContainerListener listener = new TestWindowContainerListener();
+ root.registerWindowContainerListener(listener);
+
+ // Hidden root and child request hidden.
+ assertThat(root.setVisibleRequested(/* isVisible= */ false)).isFalse();
+ assertThat(listener.mIsVisibleRequested).isFalse();
+ assertThat(child1.isVisibleRequested()).isFalse();
+
+ // Child requests to be visible, so child and root request visible.
+ assertThat(child1.setVisibleRequested(/* isVisible= */ true)).isTrue();
+ assertThat(root.isVisibleRequested()).isTrue();
+ assertThat(listener.mIsVisibleRequested).isTrue();
+ assertThat(child1.isVisibleRequested()).isTrue();
+ // Visible request didn't change.
+ assertThat(child1.setVisibleRequested(/* isVisible= */ true)).isFalse();
+ verify(root, times(2)).onChildVisibleRequestedChanged(child1);
+ }
+
+ @Test
+ public void testSetVisibleRequested_childRequestsHidden() {
+ final TestWindowContainer root = spy(new TestWindowContainerBuilder(mWm).setLayer(
+ 0).build());
+ final TestWindowContainer child1 = root.addChildWindow();
+ assertThat(child1.isVisibleRequested()).isFalse();
+ final TestWindowContainerListener listener = new TestWindowContainerListener();
+ root.registerWindowContainerListener(listener);
+
+ // Root and child requests visible.
+ assertThat(root.setVisibleRequested(/* isVisible= */ true)).isTrue();
+ assertThat(listener.mIsVisibleRequested).isTrue();
+ assertThat(child1.setVisibleRequested(/* isVisible= */ true)).isTrue();
+ assertThat(child1.isVisibleRequested()).isTrue();
+
+ // Child requests hidden, so child and root request hidden.
+ assertThat(child1.setVisibleRequested(/* isVisible= */ false)).isTrue();
+ assertThat(root.isVisibleRequested()).isFalse();
+ assertThat(listener.mIsVisibleRequested).isFalse();
+ assertThat(child1.isVisibleRequested()).isFalse();
+ // Visible request didn't change.
+ assertThat(child1.setVisibleRequested(/* isVisible= */ false)).isFalse();
+ verify(root, times(3)).onChildVisibleRequestedChanged(child1);
+ }
+
+ @Test
+ public void testOnChildVisibleRequestedChanged_bothVisible() {
+ final TestWindowContainer root = spy(new TestWindowContainerBuilder(mWm).setLayer(
+ 0).build());
+ final TestWindowContainer child1 = root.addChildWindow();
+
+ // Child and root request visible.
+ assertThat(root.setVisibleRequested(/* isVisible= */ true)).isTrue();
+ assertThat(child1.setVisibleRequested(/* isVisible= */ true)).isTrue();
+
+ // Visible request already updated on root when child requested.
+ assertThat(root.onChildVisibleRequestedChanged(child1)).isFalse();
+ }
+
+ @Test
+ public void testOnChildVisibleRequestedChanged_childVisible() {
+ final TestWindowContainer root = spy(new TestWindowContainerBuilder(mWm).setLayer(
+ 0).build());
+ final TestWindowContainer child1 = root.addChildWindow();
+
+ assertThat(root.setVisibleRequested(/* isVisible= */ false)).isFalse();
+ assertThat(child1.setVisibleRequested(/* isVisible= */ true)).isTrue();
+
+ // Visible request already updated on root when child requested.
+ assertThat(root.onChildVisibleRequestedChanged(child1)).isFalse();
+ }
+
+ @Test
+ public void testOnChildVisibleRequestedChanged_childHidden() {
+ final TestWindowContainer root = spy(new TestWindowContainerBuilder(mWm).setLayer(
+ 0).build());
+ final TestWindowContainer child1 = root.addChildWindow();
+
+ assertThat(root.setVisibleRequested(/* isVisible= */ false)).isFalse();
+ assertThat(child1.setVisibleRequested(/* isVisible= */ false)).isFalse();
+
+ // Visible request did not change.
+ assertThat(root.onChildVisibleRequestedChanged(child1)).isFalse();
+ }
+
+ @Test
public void testSetOrientation() {
final TestWindowContainer root = spy(new TestWindowContainerBuilder(mWm).build());
final TestWindowContainer child = spy(root.addChildWindow());
@@ -1656,6 +1763,7 @@
private static class TestWindowContainerListener implements WindowContainerListener {
private Configuration mConfiguration = new Configuration();
private DisplayContent mDisplayContent;
+ private boolean mIsVisibleRequested;
@Override
public void onRequestedOverrideConfigurationChanged(Configuration overrideConfiguration) {
@@ -1666,5 +1774,10 @@
public void onDisplayChanged(DisplayContent dc) {
mDisplayContent = dc;
}
+
+ @Override
+ public void onVisibleRequestedChanged(boolean isVisibleRequested) {
+ mIsVisibleRequested = isVisibleRequested;
+ }
}
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
index 8bd4148..d6cfd00 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
@@ -18,6 +18,7 @@
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
+import static android.content.res.Configuration.GRAMMATICAL_GENDER_NOT_SPECIFIED;
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
@@ -414,9 +415,10 @@
public void testTopActivityUiModeChangeScheduleConfigChange() {
final ActivityRecord activity = createActivityRecord(mWpc);
activity.setVisibleRequested(true);
- doReturn(true).when(activity).applyAppSpecificConfig(anyInt(), any());
+ doReturn(true).when(activity).applyAppSpecificConfig(anyInt(), any(), anyInt());
mWpc.updateAppSpecificSettingsForAllActivitiesInPackage(DEFAULT_COMPONENT_PACKAGE_NAME,
- Configuration.UI_MODE_NIGHT_YES, LocaleList.forLanguageTags("en-XA"));
+ Configuration.UI_MODE_NIGHT_YES, LocaleList.forLanguageTags("en-XA"),
+ GRAMMATICAL_GENDER_NOT_SPECIFIED);
verify(activity).ensureActivityConfiguration(anyInt(), anyBoolean());
}
@@ -425,8 +427,9 @@
final ActivityRecord activity = createActivityRecord(mWpc);
activity.setVisibleRequested(true);
mWpc.updateAppSpecificSettingsForAllActivitiesInPackage("com.different.package",
- Configuration.UI_MODE_NIGHT_YES, LocaleList.forLanguageTags("en-XA"));
- verify(activity, never()).applyAppSpecificConfig(anyInt(), any());
+ Configuration.UI_MODE_NIGHT_YES, LocaleList.forLanguageTags("en-XA"),
+ GRAMMATICAL_GENDER_NOT_SPECIFIED);
+ verify(activity, never()).applyAppSpecificConfig(anyInt(), any(), anyInt());
verify(activity, never()).ensureActivityConfiguration(anyInt(), anyBoolean());
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/utils/StateMachineTest.java b/services/tests/wmtests/src/com/android/server/wm/utils/StateMachineTest.java
index e82a7c2..fb0ce56 100644
--- a/services/tests/wmtests/src/com/android/server/wm/utils/StateMachineTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/utils/StateMachineTest.java
@@ -215,6 +215,20 @@
}
@Test
+ public void testStateMachineTriggerStateActionDelegateRoot() {
+ final StringBuffer log = new StringBuffer();
+
+ StateMachine stateMachine = new StateMachine(0x2);
+ stateMachine.addStateHandler(0x0, new LoggingHandler(0x0, log));
+ stateMachine.addStateHandler(0x2,
+ new LoggingHandler(0x2, log, false /* handleSelf */));
+
+ // state 0x2 delegate the message handling to its parent state
+ stateMachine.handle(0, null);
+ assertEquals("h0;", log.toString());
+ }
+
+ @Test
public void testStateMachineNestedTransition() {
final StringBuffer log = new StringBuffer();
diff --git a/services/usb/java/com/android/server/usb/UsbPortManager.java b/services/usb/java/com/android/server/usb/UsbPortManager.java
index 1399458..f920f0f 100644
--- a/services/usb/java/com/android/server/usb/UsbPortManager.java
+++ b/services/usb/java/com/android/server/usb/UsbPortManager.java
@@ -1082,6 +1082,7 @@
private void handlePortComplianceWarningLocked(PortInfo portInfo, IndentingPrintWriter pw) {
logAndPrint(Log.INFO, pw, "USB port compliance warning changed: " + portInfo);
+ logToStatsdComplianceWarnings(portInfo);
sendComplianceWarningBroadcastLocked(portInfo);
}
@@ -1108,6 +1109,33 @@
}
}
+ // Constants have to be converted to stats-log constants
+ private static int[] toStatsLogConstant(@NonNull int[] complianceWarnings) {
+ IntArray complianceWarningsProto = new IntArray();
+ for (int warning : complianceWarnings) {
+ switch (warning) {
+ case UsbPortStatus.COMPLIANCE_WARNING_OTHER:
+ complianceWarningsProto.add(FrameworkStatsLog
+ .USB_COMPLIANCE_WARNINGS_REPORTED__COMPLIANCE_WARNINGS__COMPLIANCE_WARNING_OTHER);
+ continue;
+ case UsbPortStatus.COMPLIANCE_WARNING_DEBUG_ACCESSORY:
+ complianceWarningsProto.add(FrameworkStatsLog
+ .USB_COMPLIANCE_WARNINGS_REPORTED__COMPLIANCE_WARNINGS__COMPLIANCE_WARNING_DEBUG_ACCESSORY);
+ continue;
+ case UsbPortStatus.COMPLIANCE_WARNING_BC_1_2:
+ complianceWarningsProto.add(FrameworkStatsLog
+ .USB_COMPLIANCE_WARNINGS_REPORTED__COMPLIANCE_WARNINGS__COMPLIANCE_WARNING_BC_1_2);
+ continue;
+ case UsbPortStatus.COMPLIANCE_WARNING_MISSING_RP:
+ complianceWarningsProto.add(FrameworkStatsLog
+ .USB_COMPLIANCE_WARNINGS_REPORTED__COMPLIANCE_WARNINGS__COMPLIANCE_WARNING_MISSING_RP);
+ continue;
+ }
+ }
+ return complianceWarningsProto.toArray();
+ }
+
+
private void sendPortChangedBroadcastLocked(PortInfo portInfo) {
final Intent intent = new Intent(UsbManager.ACTION_USB_PORT_CHANGED);
intent.addFlags(
@@ -1219,6 +1247,20 @@
}
}
+ // Need to create new version to prevent double counting existing stats due
+ // to new broadcast
+ private void logToStatsdComplianceWarnings(PortInfo portInfo) {
+ if (portInfo.mUsbPortStatus == null) {
+ FrameworkStatsLog.write(FrameworkStatsLog.USB_COMPLIANCE_WARNINGS_REPORTED,
+ portInfo.mUsbPort.getId(), new int[0]);
+ return;
+ }
+
+ FrameworkStatsLog.write(FrameworkStatsLog.USB_COMPLIANCE_WARNINGS_REPORTED,
+ portInfo.mUsbPort.getId(),
+ toStatsLogConstant(portInfo.mUsbPortStatus.getComplianceWarnings()));
+ }
+
public static void logAndPrint(int priority, IndentingPrintWriter pw, String msg) {
Slog.println(priority, TAG, msg);
if (pw != null) {
diff --git a/services/usb/java/com/android/server/usb/hal/port/UsbPortAidl.java b/services/usb/java/com/android/server/usb/hal/port/UsbPortAidl.java
index ca11629..ff4268f 100644
--- a/services/usb/java/com/android/server/usb/hal/port/UsbPortAidl.java
+++ b/services/usb/java/com/android/server/usb/hal/port/UsbPortAidl.java
@@ -80,38 +80,46 @@
/**
* USB data status is not known.
*/
- public static final int USB_DATA_STATUS_UNKNOWN = 0;
+ public static final int AIDL_USB_DATA_STATUS_UNKNOWN = 0;
/**
* USB data is enabled.
*/
- public static final int USB_DATA_STATUS_ENABLED = 1;
+ public static final int AIDL_USB_DATA_STATUS_ENABLED = 1;
/**
* USB data is disabled as the port is too hot.
*/
- public static final int USB_DATA_STATUS_DISABLED_OVERHEAT = 2;
+ public static final int AIDL_USB_DATA_STATUS_DISABLED_OVERHEAT = 2;
/**
* USB data is disabled due to contaminated port.
*/
- public static final int USB_DATA_STATUS_DISABLED_CONTAMINANT = 3;
+ public static final int AIDL_USB_DATA_STATUS_DISABLED_CONTAMINANT = 3;
/**
- * USB data is disabled due to docking event.
+ * USB data(both host mode and device mode) is disabled due to docking event.
*/
- public static final int USB_DATA_STATUS_DISABLED_DOCK = 4;
+ public static final int AIDL_USB_DATA_STATUS_DISABLED_DOCK = 4;
/**
* USB data is disabled by
* {@link UsbPort#enableUsbData UsbPort.enableUsbData}.
*/
- public static final int USB_DATA_STATUS_DISABLED_FORCE = 5;
+ public static final int AIDL_USB_DATA_STATUS_DISABLED_FORCE = 5;
/**
* USB data is disabled for debug.
*/
- public static final int USB_DATA_STATUS_DISABLED_DEBUG = 6;
+ public static final int AIDL_USB_DATA_STATUS_DISABLED_DEBUG = 6;
+ /**
+ * USB host mode disabled due to docking event.
+ */
+ public static final int AIDL_USB_DATA_STATUS_DISABLED_DOCK_HOST_MODE = 7;
+ /**
+ * USB device mode disabled due to docking event.
+ */
+ public static final int AIDL_USB_DATA_STATUS_DISABLED_DOCK_DEVICE_MODE = 8;
public @UsbHalVersion int getUsbHalVersion() throws RemoteException {
synchronized (mLock) {
@@ -529,24 +537,43 @@
int usbDataStatus = UsbPortStatus.DATA_STATUS_UNKNOWN;
for (int i = 0; i < usbDataStatusHal.length; i++) {
switch (usbDataStatusHal[i]) {
- case USB_DATA_STATUS_ENABLED:
+ case AIDL_USB_DATA_STATUS_ENABLED:
usbDataStatus |= UsbPortStatus.DATA_STATUS_ENABLED;
break;
- case USB_DATA_STATUS_DISABLED_OVERHEAT:
+ case AIDL_USB_DATA_STATUS_DISABLED_OVERHEAT:
usbDataStatus |= UsbPortStatus.DATA_STATUS_DISABLED_OVERHEAT;
break;
- case USB_DATA_STATUS_DISABLED_CONTAMINANT:
+ case AIDL_USB_DATA_STATUS_DISABLED_CONTAMINANT:
usbDataStatus |= UsbPortStatus.DATA_STATUS_DISABLED_CONTAMINANT;
break;
- case USB_DATA_STATUS_DISABLED_DOCK:
+ /* Indicates both host and gadget mode being disabled. */
+ case AIDL_USB_DATA_STATUS_DISABLED_DOCK:
usbDataStatus |= UsbPortStatus.DATA_STATUS_DISABLED_DOCK;
+ usbDataStatus |= UsbPortStatus.DATA_STATUS_DISABLED_DOCK_HOST_MODE;
+ usbDataStatus |= UsbPortStatus.DATA_STATUS_DISABLED_DOCK_DEVICE_MODE;
break;
- case USB_DATA_STATUS_DISABLED_FORCE:
+ case AIDL_USB_DATA_STATUS_DISABLED_FORCE:
usbDataStatus |= UsbPortStatus.DATA_STATUS_DISABLED_FORCE;
break;
- case USB_DATA_STATUS_DISABLED_DEBUG:
+ case AIDL_USB_DATA_STATUS_DISABLED_DEBUG:
usbDataStatus |= UsbPortStatus.DATA_STATUS_DISABLED_DEBUG;
break;
+ /*
+ * Set DATA_STATUS_DISABLED_DOCK when DATA_STATUS_DISABLED_DOCK_HOST_MODE
+ * is set.
+ */
+ case AIDL_USB_DATA_STATUS_DISABLED_DOCK_HOST_MODE:
+ usbDataStatus |= UsbPortStatus.DATA_STATUS_DISABLED_DOCK_HOST_MODE;
+ usbDataStatus |= UsbPortStatus.DATA_STATUS_DISABLED_DOCK;
+ break;
+ /*
+ * Set DATA_STATUS_DISABLED_DOCK when DATA_STATUS_DISABLED_DEVICE_DOCK
+ * is set.
+ */
+ case AIDL_USB_DATA_STATUS_DISABLED_DOCK_DEVICE_MODE:
+ usbDataStatus |= UsbPortStatus.DATA_STATUS_DISABLED_DOCK_DEVICE_MODE;
+ usbDataStatus |= UsbPortStatus.DATA_STATUS_DISABLED_DOCK;
+ break;
default:
usbDataStatus |= UsbPortStatus.DATA_STATUS_UNKNOWN;
}
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
index 6a7a2f9..276bccd 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
@@ -25,6 +25,7 @@
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__DETECTOR_TYPE__NORMAL_DETECTOR;
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__DETECTOR_TYPE__TRUSTED_DETECTOR_DSP;
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__KEYPHRASE_TRIGGER;
+import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__SERVICE_CRASH;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -541,6 +542,11 @@
HotwordDetectorSession.HOTWORD_DETECTION_SERVICE_DIED);
});
}
+ // Can improve to log exit reason if needed
+ HotwordMetricsLogger.writeKeyphraseTriggerEvent(
+ mDetectorType,
+ HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__SERVICE_CRASH,
+ mVoiceInteractionServiceUid);
}
@Override
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
index f041adc..9643282 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
@@ -22,6 +22,8 @@
import static android.app.ActivityManager.START_VOICE_NOT_ACTIVE_SESSION;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT;
+import static com.android.server.policy.PhoneWindowManager.SYSTEM_DIALOG_REASON_ASSIST;
+
import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -62,6 +64,7 @@
import android.service.voice.VoiceInteractionService;
import android.service.voice.VoiceInteractionServiceInfo;
import android.system.OsConstants;
+import android.text.TextUtils;
import android.util.PrintWriterPrinter;
import android.util.Slog;
import android.view.IWindowManager;
@@ -118,7 +121,9 @@
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) {
String reason = intent.getStringExtra("reason");
- if (!CLOSE_REASON_VOICE_INTERACTION.equals(reason) && !"dream".equals(reason)) {
+ if (!CLOSE_REASON_VOICE_INTERACTION.equals(reason)
+ && !TextUtils.equals("dream", reason)
+ && !SYSTEM_DIALOG_REASON_ASSIST.equals(reason)) {
synchronized (mServiceStub) {
if (mActiveSession != null && mActiveSession.mSession != null) {
try {
diff --git a/telecomm/java/android/telecom/CallAttributes.aidl b/telecomm/java/android/telecom/CallAttributes.aidl
new file mode 100644
index 0000000..19bada7
--- /dev/null
+++ b/telecomm/java/android/telecom/CallAttributes.aidl
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ * 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.telecom;
+
+/**
+ * {@hide}
+ */
+parcelable CallAttributes;
\ No newline at end of file
diff --git a/telecomm/java/android/telecom/CallAttributes.java b/telecomm/java/android/telecom/CallAttributes.java
new file mode 100644
index 0000000..6d87981
--- /dev/null
+++ b/telecomm/java/android/telecom/CallAttributes.java
@@ -0,0 +1,344 @@
+/*
+ * 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.telecom;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import java.util.Objects;
+
+/**
+ * CallAttributes represents a set of properties that define a new Call. Apps should build an
+ * instance of this class and use {@link TelecomManager#addCall} to start a new call with Telecom.
+ *
+ * <p>
+ * Apps should first register a {@link PhoneAccount} via {@link TelecomManager#registerPhoneAccount}
+ * and use the same {@link PhoneAccountHandle} registered with Telecom when creating an
+ * instance of CallAttributes.
+ */
+public final class CallAttributes implements Parcelable {
+
+ /** PhoneAccountHandle associated with the App managing calls **/
+ private final PhoneAccountHandle mPhoneAccountHandle;
+
+ /** Display name of the person on the other end of the call **/
+ private final CharSequence mDisplayName;
+
+ /** Address of the call. Note, this can be extended to a meeting link **/
+ private final Uri mAddress;
+
+ /** The direction (Outgoing/Incoming) of the new Call **/
+ @Direction private final int mDirection;
+
+ /** Information related to data being transmitted (voice, video, etc. ) **/
+ @CallType private final int mCallType;
+
+ /** Allows a package to opt into capabilities on the telecom side, on a per-call basis **/
+ @CallCapability private final int mCallCapabilities;
+
+ /** @hide **/
+ public static final String CALL_CAPABILITIES_KEY = "TelecomCapabilities";
+
+ private CallAttributes(@NonNull PhoneAccountHandle phoneAccountHandle,
+ @NonNull CharSequence displayName,
+ @NonNull Uri address,
+ int direction,
+ int callType,
+ int callCapabilities) {
+ mPhoneAccountHandle = phoneAccountHandle;
+ mDisplayName = displayName;
+ mAddress = address;
+ mDirection = direction;
+ mCallType = callType;
+ mCallCapabilities = callCapabilities;
+ }
+
+ /** @hide */
+ @IntDef(value = {DIRECTION_INCOMING, DIRECTION_OUTGOING})
+ public @interface Direction {
+ }
+ /**
+ * Indicates that the call is an incoming call.
+ */
+ public static final int DIRECTION_INCOMING = 1;
+ /**
+ * Indicates that the call is an outgoing call.
+ */
+ public static final int DIRECTION_OUTGOING = 2;
+
+ /** @hide */
+ @IntDef(value = {AUDIO_CALL, VIDEO_CALL})
+ public @interface CallType {
+ }
+ /**
+ * Used when answering or dialing a call to indicate that the call does not have a video
+ * component
+ */
+ public static final int AUDIO_CALL = 1;
+ /**
+ * Indicates video transmission is supported
+ */
+ public static final int VIDEO_CALL = 2;
+
+ /** @hide */
+ @IntDef(value = {SUPPORTS_SET_INACTIVE, SUPPORTS_STREAM, SUPPORTS_TRANSFER}, flag = true)
+ public @interface CallCapability {
+ }
+ /**
+ * The call being created can be set to inactive (traditionally referred to as hold). This
+ * means that once a new call goes active, if the active call needs to be held in order to
+ * place or receive an incoming call, the active call will be placed on hold. otherwise, the
+ * active call may be disconnected.
+ */
+ public static final int SUPPORTS_SET_INACTIVE = 1 << 1;
+ /**
+ * The call can be streamed from a root device to another device to continue the call without
+ * completely transferring it.
+ */
+ public static final int SUPPORTS_STREAM = 1 << 2;
+ /**
+ * The call can be completely transferred from one endpoint to another
+ */
+ public static final int SUPPORTS_TRANSFER = 1 << 3;
+
+ /**
+ * Build an instance of {@link CallAttributes}. In order to build a valid instance, a
+ * {@link PhoneAccountHandle}, call {@link Direction}, display name, and {@link Uri} address
+ * are required.
+ *
+ * <p>
+ * Note: Pass in the same {@link PhoneAccountHandle} that was used to register a
+ * {@link PhoneAccount} with Telecom. see {@link TelecomManager#registerPhoneAccount}
+ */
+ public static final class Builder {
+ // required and final fields
+ private final PhoneAccountHandle mPhoneAccountHandle;
+ @Direction private final int mDirection;
+ private final CharSequence mDisplayName;
+ private final Uri mAddress;
+ // optional fields
+ @CallType private int mCallType = CallAttributes.AUDIO_CALL;
+ @CallCapability private int mCallCapabilities = SUPPORTS_SET_INACTIVE;
+
+ /**
+ * Constructor for the CallAttributes.Builder class
+ *
+ * @param phoneAccountHandle that belongs to package registered with Telecom
+ * @param callDirection of the new call that will be added to Telecom
+ * @param displayName of the caller for incoming calls or initiating user for outgoing calls
+ * @param address of the caller for incoming calls or destination for outgoing calls
+ */
+ public Builder(@NonNull PhoneAccountHandle phoneAccountHandle,
+ @Direction int callDirection, @NonNull CharSequence displayName,
+ @NonNull Uri address) {
+ if (!isInRange(DIRECTION_INCOMING, DIRECTION_OUTGOING, callDirection)) {
+ throw new IllegalArgumentException(TextUtils.formatSimple("CallDirection=[%d] is"
+ + " invalid. CallDirections value should be within [%d, %d]",
+ callDirection, DIRECTION_INCOMING, DIRECTION_OUTGOING));
+ }
+ Objects.requireNonNull(phoneAccountHandle);
+ Objects.requireNonNull(displayName);
+ Objects.requireNonNull(address);
+ mPhoneAccountHandle = phoneAccountHandle;
+ mDirection = callDirection;
+ mDisplayName = displayName;
+ mAddress = address;
+ }
+
+ /**
+ * @param callType see {@link CallType} for valid arguments
+ * @return Builder
+ */
+ @NonNull
+ public Builder setCallType(@CallType int callType) {
+ if (!isInRange(AUDIO_CALL, VIDEO_CALL, callType)) {
+ throw new IllegalArgumentException(TextUtils.formatSimple("CallType=[%d] is"
+ + " invalid. CallTypes value should be within [%d, %d]",
+ callType, AUDIO_CALL, VIDEO_CALL));
+ }
+ mCallType = callType;
+ return this;
+ }
+
+ /**
+ * @param callCapabilities see {@link CallCapability} for valid arguments
+ * @return Builder
+ */
+ @NonNull
+ public Builder setCallCapabilities(@CallCapability int callCapabilities) {
+ mCallCapabilities = callCapabilities;
+ return this;
+ }
+
+ /**
+ * Build an instance of {@link CallAttributes} based on the last values passed to the
+ * setters or default values.
+ *
+ * @return an instance of {@link CallAttributes}
+ */
+ @NonNull
+ public CallAttributes build() {
+ return new CallAttributes(mPhoneAccountHandle, mDisplayName, mAddress, mDirection,
+ mCallType, mCallCapabilities);
+ }
+
+ /** @hide */
+ private boolean isInRange(int floor, int ceiling, int value) {
+ return value >= floor && value <= ceiling;
+ }
+ }
+
+ /**
+ * The {@link PhoneAccountHandle} that should be registered to Telecom to allow calls. The
+ * {@link PhoneAccountHandle} should be registered before creating a CallAttributes instance.
+ *
+ * @return the {@link PhoneAccountHandle} for this package that allows this call to be created
+ */
+ @NonNull public PhoneAccountHandle getPhoneAccountHandle() {
+ return mPhoneAccountHandle;
+ }
+
+ /**
+ * @return display name of the incoming caller or the person being called for an outgoing call
+ */
+ @NonNull public CharSequence getDisplayName() {
+ return mDisplayName;
+ }
+
+ /**
+ * @return address of the incoming caller
+ * or the address of the person being called for an outgoing call
+ */
+ @NonNull public Uri getAddress() {
+ return mAddress;
+ }
+
+ /**
+ * @return the direction of the new call.
+ */
+ public @Direction int getDirection() {
+ return mDirection;
+ }
+
+ /**
+ * @return Information related to data being transmitted (voice, video, etc. )
+ */
+ public @CallType int getCallType() {
+ return mCallType;
+ }
+
+ /**
+ * @return The allowed capabilities of the new call
+ */
+ public @CallCapability int getCallCapabilities() {
+ return mCallCapabilities;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@Nullable Parcel dest, int flags) {
+ dest.writeParcelable(mPhoneAccountHandle, flags);
+ dest.writeCharSequence(mDisplayName);
+ dest.writeParcelable(mAddress, flags);
+ dest.writeInt(mDirection);
+ dest.writeInt(mCallType);
+ dest.writeInt(mCallCapabilities);
+ }
+
+ /**
+ * Responsible for creating CallAttribute objects for deserialized Parcels.
+ */
+ public static final @android.annotation.NonNull
+ Parcelable.Creator<CallAttributes> CREATOR =
+ new Parcelable.Creator<>() {
+ @Override
+ public CallAttributes createFromParcel(Parcel source) {
+ return new CallAttributes(source.readParcelable(getClass().getClassLoader(),
+ android.telecom.PhoneAccountHandle.class),
+ source.readCharSequence(),
+ source.readParcelable(getClass().getClassLoader(),
+ android.net.Uri.class),
+ source.readInt(),
+ source.readInt(),
+ source.readInt());
+ }
+
+ @Override
+ public CallAttributes[] newArray(int size) {
+ return new CallAttributes[size];
+ }
+ };
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+
+ sb.append("{ CallAttributes: [phoneAccountHandle: ")
+ .append(mPhoneAccountHandle) /* PhoneAccountHandle#toString handles PII */
+ .append("], [contactName: ")
+ .append(Log.pii(mDisplayName))
+ .append("], [address=")
+ .append(Log.pii(mAddress))
+ .append("], [direction=")
+ .append(mDirection)
+ .append("], [callType=")
+ .append(mCallType)
+ .append("], [mCallCapabilities=")
+ .append(mCallCapabilities)
+ .append("] }");
+
+ return sb.toString();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null || obj.getClass() != this.getClass()) {
+ return false;
+ }
+ CallAttributes that = (CallAttributes) obj;
+ return this.mDirection == that.mDirection
+ && this.mCallType == that.mCallType
+ && this.mCallCapabilities == that.mCallCapabilities
+ && Objects.equals(this.mPhoneAccountHandle, that.mPhoneAccountHandle)
+ && Objects.equals(this.mAddress, that.mAddress)
+ && Objects.equals(this.mDisplayName, that.mDisplayName);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int hashCode() {
+ return Objects.hash(mPhoneAccountHandle, mAddress, mDisplayName,
+ mDirection, mCallType, mCallCapabilities);
+ }
+}
diff --git a/telecomm/java/android/telecom/CallAudioState.java b/telecomm/java/android/telecom/CallAudioState.java
index fccdf76..c7cc1bd 100644
--- a/telecomm/java/android/telecom/CallAudioState.java
+++ b/telecomm/java/android/telecom/CallAudioState.java
@@ -27,7 +27,6 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@@ -58,6 +57,9 @@
/** Direct the audio stream through the device's speakerphone. */
public static final int ROUTE_SPEAKER = 0x00000008;
+ /** Direct the audio stream through another device. */
+ public static final int ROUTE_STREAMING = 0x00000010;
+
/**
* Direct the audio stream through the device's earpiece or wired headset if one is
* connected.
@@ -70,7 +72,7 @@
* @hide
**/
public static final int ROUTE_ALL = ROUTE_EARPIECE | ROUTE_BLUETOOTH | ROUTE_WIRED_HEADSET |
- ROUTE_SPEAKER;
+ ROUTE_SPEAKER | ROUTE_STREAMING;
private final boolean isMuted;
private final int route;
@@ -189,7 +191,11 @@
*/
@CallAudioRoute
public int getSupportedRouteMask() {
- return supportedRouteMask;
+ if (route == ROUTE_STREAMING) {
+ return ROUTE_STREAMING;
+ } else {
+ return supportedRouteMask;
+ }
}
/**
@@ -232,6 +238,9 @@
if ((route & ROUTE_SPEAKER) == ROUTE_SPEAKER) {
listAppend(buffer, "SPEAKER");
}
+ if ((route & ROUTE_STREAMING) == ROUTE_STREAMING) {
+ listAppend(buffer, "STREAMING");
+ }
return buffer.toString();
}
diff --git a/telecomm/java/android/telecom/CallControl.aidl b/telecomm/java/android/telecom/CallControl.aidl
new file mode 100644
index 0000000..0f780e6
--- /dev/null
+++ b/telecomm/java/android/telecom/CallControl.aidl
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ * 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.telecom;
+
+/**
+ * {@hide}
+ */
+parcelable CallControl;
diff --git a/telecomm/java/android/telecom/CallControl.java b/telecomm/java/android/telecom/CallControl.java
new file mode 100644
index 0000000..867bcc7
--- /dev/null
+++ b/telecomm/java/android/telecom/CallControl.java
@@ -0,0 +1,282 @@
+/*
+ * 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.telecom;
+
+import static android.telecom.CallException.TRANSACTION_EXCEPTION_KEY;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.OutcomeReceiver;
+import android.os.ParcelUuid;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+
+import com.android.internal.telecom.ClientTransactionalServiceRepository;
+import com.android.internal.telecom.ICallControl;
+
+import java.util.concurrent.Executor;
+
+/**
+ * CallControl provides client side control of a call. Each Call will get an individual CallControl
+ * instance in which the client can alter the state of the associated call.
+ *
+ * <p>
+ * Each method is Transactional meaning that it can succeed or fail. If a transaction succeeds,
+ * the {@link OutcomeReceiver#onResult} will be called by Telecom. Otherwise, the
+ * {@link OutcomeReceiver#onError} is called and provides a {@link CallException} that details why
+ * the operation failed.
+ */
+public final class CallControl implements AutoCloseable {
+ private static final String TAG = CallControl.class.getSimpleName();
+ private static final String INTERFACE_ERROR_MSG = "Call Control is not available";
+ private final String mCallId;
+ private final ICallControl mServerInterface;
+ private final PhoneAccountHandle mPhoneAccountHandle;
+ private final ClientTransactionalServiceRepository mRepository;
+
+ /** @hide */
+ public CallControl(@NonNull String callId, @Nullable ICallControl serverInterface,
+ @NonNull ClientTransactionalServiceRepository repository,
+ @NonNull PhoneAccountHandle pah) {
+ mCallId = callId;
+ mServerInterface = serverInterface;
+ mRepository = repository;
+ mPhoneAccountHandle = pah;
+ }
+
+ /**
+ * @return the callId Telecom assigned to this CallControl object which should be attached to
+ * an individual call.
+ */
+ @NonNull
+ public ParcelUuid getCallId() {
+ return ParcelUuid.fromString(mCallId);
+ }
+
+ /**
+ * Request Telecom set the call state to active.
+ *
+ * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
+ * will be called on.
+ * @param callback that will be completed on the Telecom side that details success or failure
+ * of the requested operation.
+ *
+ * {@link OutcomeReceiver#onResult} will be called if Telecom has successfully
+ * switched the call state to active
+ *
+ * {@link OutcomeReceiver#onError} will be called if Telecom has failed to set
+ * the call state to active. A {@link CallException} will be passed
+ * that details why the operation failed.
+ */
+ public void setActive(@CallbackExecutor @NonNull Executor executor,
+ @NonNull OutcomeReceiver<Void, CallException> callback) {
+ if (mServerInterface != null) {
+ try {
+ mServerInterface.setActive(mCallId,
+ new CallControlResultReceiver("setActive", executor, callback));
+
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+ } else {
+ throw new IllegalStateException(INTERFACE_ERROR_MSG);
+ }
+ }
+
+ /**
+ * Request Telecom set the call state to inactive. This the same as hold for two call endpoints
+ * but can be extended to setting a meeting to inactive.
+ *
+ * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
+ * will be called on.
+ * @param callback that will be completed on the Telecom side that details success or failure
+ * of the requested operation.
+ *
+ * {@link OutcomeReceiver#onResult} will be called if Telecom has successfully
+ * switched the call state to inactive
+ *
+ * {@link OutcomeReceiver#onError} will be called if Telecom has failed to set
+ * the call state to inactive. A {@link CallException} will be passed
+ * that details why the operation failed.
+ */
+ public void setInactive(@CallbackExecutor @NonNull Executor executor,
+ @NonNull OutcomeReceiver<Void, CallException> callback) {
+ if (mServerInterface != null) {
+ try {
+ mServerInterface.setInactive(mCallId,
+ new CallControlResultReceiver("setInactive", executor, callback));
+
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+ } else {
+ throw new IllegalStateException(INTERFACE_ERROR_MSG);
+ }
+ }
+
+ /**
+ * Request Telecom set the call state to disconnect.
+ *
+ * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
+ * will be called on.
+ * @param callback that will be completed on the Telecom side that details success or failure
+ * of the requested operation.
+ *
+ * {@link OutcomeReceiver#onResult} will be called if Telecom has successfully
+ * disconnected the call.
+ *
+ * {@link OutcomeReceiver#onError} will be called if Telecom has failed to
+ * disconnect the call. A {@link CallException} will be passed
+ * that details why the operation failed.
+ */
+ public void disconnect(@NonNull DisconnectCause disconnectCause,
+ @CallbackExecutor @NonNull Executor executor,
+ @NonNull OutcomeReceiver<Void, CallException> callback) {
+ if (mServerInterface != null) {
+ try {
+ mServerInterface.disconnect(mCallId, disconnectCause,
+ new CallControlResultReceiver("disconnect", executor, callback));
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+ } else {
+ throw new IllegalStateException(INTERFACE_ERROR_MSG);
+ }
+ }
+
+ /**
+ * Request Telecom reject the incoming call.
+ *
+ * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
+ * will be called on.
+ * @param callback that will be completed on the Telecom side that details success or failure
+ * of the requested operation.
+ *
+ * {@link OutcomeReceiver#onResult} will be called if Telecom has successfully
+ * rejected the incoming call.
+ *
+ * {@link OutcomeReceiver#onError} will be called if Telecom has failed to
+ * reject the incoming call. A {@link CallException} will be passed
+ * that details why the operation failed.
+ */
+ public void rejectCall(@CallbackExecutor @NonNull Executor executor,
+ @NonNull OutcomeReceiver<Void, CallException> callback) {
+ if (mServerInterface != null) {
+ try {
+ mServerInterface.rejectCall(mCallId,
+ new CallControlResultReceiver("rejectCall", executor, callback));
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+ } else {
+ throw new IllegalStateException(INTERFACE_ERROR_MSG);
+ }
+ }
+
+ /**
+ * Request start a call streaming session. On receiving valid request, telecom will bind to
+ * the {@link CallStreamingService} implemented by a general call streaming sender. So that the
+ * call streaming sender can perform streaming local device audio to another remote device and
+ * control the call during streaming.
+ *
+ * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
+ * will be called on.
+ * @param callback that will be completed on the Telecom side that details success or failure
+ * of the requested operation.
+ *
+ * {@link OutcomeReceiver#onResult} will be called if Telecom has successfully
+ * rejected the incoming call.
+ *
+ * {@link OutcomeReceiver#onError} will be called if Telecom has failed to
+ * reject the incoming call. A {@link CallException} will be passed that
+ * details why the operation failed.
+ */
+ public void startCallStreaming(@CallbackExecutor @NonNull Executor executor,
+ @NonNull OutcomeReceiver<Void, CallException> callback) {
+ if (mServerInterface != null) {
+ try {
+ mServerInterface.startCallStreaming(mCallId,
+ new CallControlResultReceiver("startCallStreaming", executor, callback));
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+ } else {
+ throw new IllegalStateException(INTERFACE_ERROR_MSG);
+ }
+ }
+
+ /**
+ * This method should be called after
+ * {@link CallControl#disconnect(DisconnectCause, Executor, OutcomeReceiver)} or
+ * {@link CallControl#rejectCall(Executor, OutcomeReceiver)}
+ * to destroy all references of this object and avoid memory leaks.
+ */
+ @Override
+ public void close() {
+ mRepository.removeCallFromServiceWrapper(mPhoneAccountHandle, mCallId);
+ }
+
+ /**
+ * Since {@link OutcomeReceiver}s cannot be passed via AIDL, a ResultReceiver (which can) must
+ * wrap the Clients {@link OutcomeReceiver} passed in and await for the Telecom Server side
+ * response in {@link ResultReceiver#onReceiveResult(int, Bundle)}.
+ * @hide */
+ private class CallControlResultReceiver extends ResultReceiver {
+ private final String mCallingMethod;
+ private final Executor mExecutor;
+ private final OutcomeReceiver<Void, CallException> mClientCallback;
+
+ CallControlResultReceiver(String method, Executor executor,
+ OutcomeReceiver<Void, CallException> clientCallback) {
+ super(null);
+ mCallingMethod = method;
+ mExecutor = executor;
+ mClientCallback = clientCallback;
+ }
+
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ Log.d(CallControl.TAG, "%s: oRR: resultCode=[%s]", mCallingMethod, resultCode);
+ super.onReceiveResult(resultCode, resultData);
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ if (resultCode == TelecomManager.TELECOM_TRANSACTION_SUCCESS) {
+ mExecutor.execute(() -> mClientCallback.onResult(null));
+ } else {
+ mExecutor.execute(() ->
+ mClientCallback.onError(getTransactionException(resultData)));
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ }
+
+ /** @hide */
+ private CallException getTransactionException(Bundle resultData) {
+ String message = "unknown error";
+ if (resultData != null && resultData.containsKey(TRANSACTION_EXCEPTION_KEY)) {
+ return resultData.getParcelable(TRANSACTION_EXCEPTION_KEY,
+ CallException.class);
+ }
+ return new CallException(message, CallException.CODE_ERROR_UNKNOWN);
+ }
+}
diff --git a/telecomm/java/android/telecom/CallEventCallback.java b/telecomm/java/android/telecom/CallEventCallback.java
new file mode 100644
index 0000000..fd7e101
--- /dev/null
+++ b/telecomm/java/android/telecom/CallEventCallback.java
@@ -0,0 +1,121 @@
+/*
+ * 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.telecom;
+
+
+import android.annotation.NonNull;
+
+import java.util.function.Consumer;
+
+/**
+ * CallEventCallback relays updates to a call from the Telecom framework.
+ * This can include operations which the app must implement on a Call due to the presence of other
+ * calls on the device, requests relayed from a Bluetooth device, or from another calling surface.
+ *
+ * <p>
+ * CallEventCallbacks with {@link Consumer}s are transactional, meaning that a client must
+ * complete the {@link Consumer} via {@link Consumer#accept(Object)} in order to complete the
+ * CallEventCallback. If a CallEventCallback can be completed, the
+ * {@link Consumer#accept(Object)} should be called with {@link Boolean#TRUE}. Otherwise,
+ * {@link Consumer#accept(Object)} should be called with {@link Boolean#FALSE} to represent the
+ * CallEventCallback cannot be completed on the client side.
+ *
+ * <p>
+ * Note: Each CallEventCallback has a timeout of 5000 milliseconds. Failing to complete the
+ * {@link Consumer} before the timeout will result in a failed transaction.
+ */
+public interface CallEventCallback {
+ /**
+ * Telecom is informing the client to set the call active
+ *
+ * @param wasCompleted The {@link Consumer} to be completed. If the client can set the call
+ * active on their end, the {@link Consumer#accept(Object)} should be
+ * called with {@link Boolean#TRUE}. Otherwise,
+ * {@link Consumer#accept(Object)} should be called with
+ * {@link Boolean#FALSE}.
+ */
+ void onSetActive(@NonNull Consumer<Boolean> wasCompleted);
+
+ /**
+ * Telecom is informing the client to set the call inactive. This is the same as holding a call
+ * for two endpoints but can be extended to setting a meeting inactive.
+ *
+ * @param wasCompleted The {@link Consumer} to be completed. If the client can set the call
+ * inactive on their end, the {@link Consumer#accept(Object)} should be
+ * called with {@link Boolean#TRUE}. Otherwise,
+ * {@link Consumer#accept(Object)} should be called with
+ * {@link Boolean#FALSE}.
+ */
+ void onSetInactive(@NonNull Consumer<Boolean> wasCompleted);
+
+ /**
+ * Telecom is informing the client to answer an incoming call and set it to active.
+ *
+ * @param videoState see {@link android.telecom.CallAttributes.CallType} for valid states
+ * @param wasCompleted The {@link Consumer} to be completed. If the client can answer the call
+ * on their end, {@link Consumer#accept(Object)} should be called with
+ * {@link Boolean#TRUE}. Otherwise, {@link Consumer#accept(Object)} should
+ * be called with {@link Boolean#FALSE}.
+ */
+ void onAnswer(@android.telecom.CallAttributes.CallType int videoState,
+ @NonNull Consumer<Boolean> wasCompleted);
+
+ /**
+ * Telecom is informing the client to reject the incoming call
+ *
+ * @param wasCompleted The {@link Consumer} to be completed. If the client can reject the
+ * incoming call, {@link Consumer#accept(Object)} should be called with
+ * {@link Boolean#TRUE}. Otherwise, {@link Consumer#accept(Object)}
+ * should be called with {@link Boolean#FALSE}.
+ */
+ void onReject(@NonNull Consumer<Boolean> wasCompleted);
+
+ /**
+ * Telecom is informing the client to disconnect the call
+ *
+ * @param wasCompleted The {@link Consumer} to be completed. If the client can disconnect the
+ * call on their end, {@link Consumer#accept(Object)} should be called with
+ * {@link Boolean#TRUE}. Otherwise, {@link Consumer#accept(Object)}
+ * should be called with {@link Boolean#FALSE}.
+ */
+ void onDisconnect(@NonNull Consumer<Boolean> wasCompleted);
+
+ /**
+ * update the client on the new {@link CallAudioState}
+ *
+ * @param callAudioState that is currently being used
+ */
+ void onCallAudioStateChanged(@NonNull CallAudioState callAudioState);
+
+ /**
+ * Telecom is informing the client to set the call in streaming.
+ *
+ * @param wasCompleted The {@link Consumer} to be completed. If the client can stream the
+ * call on their end, {@link Consumer#accept(Object)} should be called with
+ * {@link Boolean#TRUE}. Otherwise, {@link Consumer#accept(Object)}
+ * should be called with {@link Boolean#FALSE}.
+ */
+ void onCallStreamingStarted(@NonNull Consumer<Boolean> wasCompleted);
+
+ /**
+ * Telecom is informing the client user requested call streaming but the stream can't be
+ * started.
+ *
+ * @param reason Code to indicate the reason of this failure
+ */
+ void onCallStreamingFailed(@CallStreamingService.StreamingFailedReason int reason);
+}
diff --git a/telecomm/java/android/telecom/CallException.aidl b/telecomm/java/android/telecom/CallException.aidl
new file mode 100644
index 0000000..a16af12
--- /dev/null
+++ b/telecomm/java/android/telecom/CallException.aidl
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ * 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.telecom;
+
+/**
+ * {@hide}
+ */
+parcelable CallException;
\ No newline at end of file
diff --git a/telecomm/java/android/telecom/CallException.java b/telecomm/java/android/telecom/CallException.java
new file mode 100644
index 0000000..0b0de6b
--- /dev/null
+++ b/telecomm/java/android/telecom/CallException.java
@@ -0,0 +1,160 @@
+/*
+ * 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.telecom;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * This class defines exceptions that can be thrown when using Telecom APIs with
+ * {@link android.os.OutcomeReceiver}s. Most of these exceptions are thrown when changing a call
+ * state with {@link CallControl}s or {@link CallEventCallback}s.
+ */
+public final class CallException extends RuntimeException implements Parcelable {
+ /** @hide **/
+ public static final String TRANSACTION_EXCEPTION_KEY = "TelecomTransactionalExceptionKey";
+
+ /**
+ * The operation has failed due to an unknown or unspecified error.
+ */
+ public static final int CODE_ERROR_UNKNOWN = 1;
+
+ /**
+ * The operation has failed due to Telecom failing to hold the current active call for the
+ * call attempting to become the new active call. The client should end the current active call
+ * and re-try the failed operation.
+ */
+ public static final int CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL = 2;
+
+ /**
+ * The operation has failed because Telecom has already removed the call from the server side
+ * and destroyed all the objects associated with it. The client should re-add the call.
+ */
+ public static final int CODE_CALL_IS_NOT_BEING_TRACKED = 3;
+
+ /**
+ * The operation has failed because Telecom cannot set the requested call as the current active
+ * call. The client should end the current active call and re-try the operation.
+ */
+ public static final int CODE_CALL_CANNOT_BE_SET_TO_ACTIVE = 4;
+
+ /**
+ * The operation has failed because there is either no PhoneAccount registered with Telecom
+ * for the given operation, or the limit of calls has been reached. The client should end the
+ * current active call and re-try the failed operation.
+ */
+ public static final int CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME = 5;
+
+ /**
+ * The operation has failed because the operation failed to complete before the timeout
+ */
+ public static final int CODE_OPERATION_TIMED_OUT = 6;
+
+ private int mCode = CODE_ERROR_UNKNOWN;
+ private final String mMessage;
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeString8(mMessage);
+ dest.writeInt(mCode);
+ }
+
+ /**
+ * Responsible for creating CallAttribute objects for deserialized Parcels.
+ */
+ public static final @android.annotation.NonNull
+ Parcelable.Creator<CallException> CREATOR = new Parcelable.Creator<>() {
+ @Override
+ public CallException createFromParcel(Parcel source) {
+ return new CallException(source.readString8(), source.readInt());
+ }
+
+ @Override
+ public CallException[] newArray(int size) {
+ return new CallException[size];
+ }
+ };
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = "CODE_ERROR_", value = {
+ CODE_ERROR_UNKNOWN,
+ CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL,
+ CODE_CALL_IS_NOT_BEING_TRACKED,
+ CODE_CALL_CANNOT_BE_SET_TO_ACTIVE,
+ CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME,
+ CODE_OPERATION_TIMED_OUT
+ })
+ public @interface CallErrorCode {
+ }
+
+ /**
+ * Constructor for a new CallException when only message can be specified.
+ * {@code CODE_ERROR_UNKNOWN} will be default code returned when calling {@code getCode}
+ *
+ * @param message related to why the exception was created
+ */
+ public CallException(@Nullable String message) {
+ super(getMessage(message, CODE_ERROR_UNKNOWN));
+ mMessage = message;
+ }
+
+ /**
+ * Constructor for a new CallException that has a defined error code in this class
+ *
+ * @param message related to why the exception was created
+ * @param code defined above that caused this exception to be created
+ */
+ public CallException(@Nullable String message, @CallErrorCode int code) {
+ super(getMessage(message, code));
+ mCode = code;
+ mMessage = message;
+ }
+
+ /**
+ * @return one of the error codes defined in this class that was passed into the constructor
+ */
+ public @CallErrorCode int getCode() {
+ return mCode;
+ }
+
+ private static String getMessage(String message, int code) {
+ StringBuilder builder;
+ if (!TextUtils.isEmpty(message)) {
+ builder = new StringBuilder(message);
+ builder.append(" (code: ");
+ builder.append(code);
+ builder.append(")");
+ return builder.toString();
+ } else {
+ return "code: " + code;
+ }
+ }
+}
diff --git a/telecomm/java/android/telecom/CallStreamingService.java b/telecomm/java/android/telecom/CallStreamingService.java
new file mode 100644
index 0000000..affa6b6
--- /dev/null
+++ b/telecomm/java/android/telecom/CallStreamingService.java
@@ -0,0 +1,184 @@
+/*
+ * 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.telecom;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SdkConstant;
+import android.app.Service;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+
+import androidx.annotation.Nullable;
+
+import com.android.internal.telecom.ICallStreamingService;
+import com.android.internal.telecom.IStreamingCallAdapter;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * This service is implemented by an app that wishes to provide functionality for a general call
+ * streaming sender for voip calls.
+ *
+ * TODO: add doc of how to be the general streaming sender
+ *
+ */
+public abstract class CallStreamingService extends Service {
+ /**
+ * The {@link android.content.Intent} that must be declared as handled by the service.
+ */
+ @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
+ public static final String SERVICE_INTERFACE = "android.telecom.CallStreamingService";
+
+ private static final int MSG_SET_STREAMING_CALL_ADAPTER = 1;
+ private static final int MSG_CALL_STREAMING_STARTED = 2;
+ private static final int MSG_CALL_STREAMING_STOPPED = 3;
+ private static final int MSG_CALL_STREAMING_CHANGED_CHANGED = 4;
+
+ /** Default Handler used to consolidate binder method calls onto a single thread. */
+ private final Handler mHandler = new Handler(Looper.getMainLooper()) {
+ @Override
+ public void handleMessage(Message msg) {
+ if (mStreamingCallAdapter == null && msg.what != MSG_SET_STREAMING_CALL_ADAPTER) {
+ return;
+ }
+
+ switch (msg.what) {
+ case MSG_SET_STREAMING_CALL_ADAPTER:
+ mStreamingCallAdapter = new StreamingCallAdapter(
+ (IStreamingCallAdapter) msg.obj);
+ break;
+ case MSG_CALL_STREAMING_STARTED:
+ mCall = (StreamingCall) msg.obj;
+ mCall.setAdapter(mStreamingCallAdapter);
+ CallStreamingService.this.onCallStreamingStarted(mCall);
+ break;
+ case MSG_CALL_STREAMING_STOPPED:
+ mCall = null;
+ mStreamingCallAdapter = null;
+ CallStreamingService.this.onCallStreamingStopped();
+ break;
+ case MSG_CALL_STREAMING_CHANGED_CHANGED:
+ int state = (int) msg.obj;
+ mCall.setStreamingState(state);
+ CallStreamingService.this.onCallStreamingStateChanged(state);
+ break;
+ default:
+ break;
+ }
+ }
+ };
+
+ @Nullable
+ @Override
+ public IBinder onBind(@NonNull Intent intent) {
+ return new CallStreamingServiceBinder();
+ }
+
+ /** Manages the binder calls so that the implementor does not need to deal with it. */
+ private final class CallStreamingServiceBinder extends ICallStreamingService.Stub {
+ @Override
+ public void setStreamingCallAdapter(IStreamingCallAdapter streamingCallAdapter)
+ throws RemoteException {
+ mHandler.obtainMessage(MSG_SET_STREAMING_CALL_ADAPTER, mStreamingCallAdapter)
+ .sendToTarget();
+ }
+
+ @Override
+ public void onCallStreamingStarted(StreamingCall call) throws RemoteException {
+ mHandler.obtainMessage(MSG_CALL_STREAMING_STARTED, call).sendToTarget();
+ }
+
+ @Override
+ public void onCallStreamingStopped() throws RemoteException {
+ mHandler.obtainMessage(MSG_CALL_STREAMING_STOPPED).sendToTarget();
+ }
+
+ @Override
+ public void onCallStreamingStateChanged(int state) throws RemoteException {
+ mHandler.obtainMessage(MSG_CALL_STREAMING_CHANGED_CHANGED, state).sendToTarget();
+ }
+ }
+
+ /**
+ * Call streaming request reject reason used with
+ * {@link CallEventCallback#onCallStreamingFailed(int)} to indicate that telecom is rejecting a
+ * call streaming request because there's an ongoing streaming call on this device.
+ */
+ public static final int STREAMING_FAILED_ALREADY_STREAMING = 1;
+
+ /**
+ * Call streaming request reject reason used with
+ * {@link CallEventCallback#onCallStreamingFailed(int)} to indicate that telecom is rejecting a
+ * call streaming request because telecom can't find existing general streaming sender on this
+ * device.
+ */
+ public static final int STREAMING_FAILED_NO_SENDER = 2;
+
+ /**
+ * Call streaming request reject reason used with
+ * {@link CallEventCallback#onCallStreamingFailed(int)} to indicate that telecom is rejecting a
+ * call streaming request because telecom can't bind to the general streaming sender app.
+ */
+ public static final int STREAMING_FAILED_SENDER_BINDING_ERROR = 3;
+
+ private StreamingCallAdapter mStreamingCallAdapter;
+ private StreamingCall mCall;
+
+ /**
+ * @hide
+ */
+ @IntDef(prefix = {"STREAMING_FAILED"},
+ value = {
+ STREAMING_FAILED_ALREADY_STREAMING,
+ STREAMING_FAILED_NO_SENDER,
+ STREAMING_FAILED_SENDER_BINDING_ERROR
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface StreamingFailedReason {};
+
+ /**
+ * Called when a {@code StreamingCall} has been added to this call streaming session. The call
+ * streaming sender should start to intercept the device audio using audio records and audio
+ * tracks from Audio frameworks.
+ *
+ * @param call a newly added {@code StreamingCall}.
+ */
+ public void onCallStreamingStarted(@NonNull StreamingCall call) {
+ }
+
+ /**
+ * Called when a current {@code StreamingCall} has been removed from this call streaming
+ * session. The call streaming sender should notify the streaming receiver that the call is
+ * stopped streaming and stop the device audio interception.
+ */
+ public void onCallStreamingStopped() {
+ }
+
+ /**
+ * Called when the streaming state of current {@code StreamingCall} changed. General streaming
+ * sender usually get notified of the holding/unholding from the original owner voip app of the
+ * call.
+ */
+ public void onCallStreamingStateChanged(@StreamingCall.StreamingCallState int state) {
+ }
+}
diff --git a/telecomm/java/android/telecom/PhoneAccount.java b/telecomm/java/android/telecom/PhoneAccount.java
index ec18c6a..b8c056e 100644
--- a/telecomm/java/android/telecom/PhoneAccount.java
+++ b/telecomm/java/android/telecom/PhoneAccount.java
@@ -418,7 +418,34 @@
*/
public static final int CAPABILITY_VOICE_CALLING_AVAILABLE = 0x20000;
- /* NEXT CAPABILITY: 0x40000 */
+
+ /**
+ * Flag indicating that this {@link PhoneAccount} supports the use TelecomManager APIs that
+ * utilize {@link android.os.OutcomeReceiver}s or {@link java.util.function.Consumer}s.
+ * Be aware, if this capability is set, {@link #CAPABILITY_SELF_MANAGED} will be amended by
+ * Telecom when this {@link PhoneAccount} is registered via
+ * {@link TelecomManager#registerPhoneAccount(PhoneAccount)}.
+ *
+ * <p>
+ * {@link android.os.OutcomeReceiver}s and {@link java.util.function.Consumer}s represent
+ * transactional operations because the operation can succeed or fail. An app wishing to use
+ * transactional operations should define behavior for a successful and failed TelecomManager
+ * API call.
+ *
+ * @see #CAPABILITY_SELF_MANAGED
+ * @see #getCapabilities
+ */
+ public static final int CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS = 0x40000;
+
+ /**
+ * Flag indicating that this voip app {@link PhoneAccount} supports the call streaming session
+ * to stream call audio to another remote device via streaming app.
+ *
+ * @see #getCapabilities
+ */
+ public static final int CAPABILITY_SUPPORTS_CALL_STREAMING = 0x80000;
+
+ /* NEXT CAPABILITY: [0x100000, 0x200000, 0x400000] */
/**
* URI scheme for telephone number URIs.
diff --git a/telecomm/java/android/telecom/StreamingCall.aidl b/telecomm/java/android/telecom/StreamingCall.aidl
new file mode 100644
index 0000000..d286658
--- /dev/null
+++ b/telecomm/java/android/telecom/StreamingCall.aidl
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ * 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.telecom;
+
+/**
+ * {@hide}
+ */
+parcelable StreamingCall;
\ No newline at end of file
diff --git a/telecomm/java/android/telecom/StreamingCall.java b/telecomm/java/android/telecom/StreamingCall.java
new file mode 100644
index 0000000..985cccc
--- /dev/null
+++ b/telecomm/java/android/telecom/StreamingCall.java
@@ -0,0 +1,178 @@
+/*
+ * 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.telecom;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.content.ComponentName;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Represents a voip call requested to stream to another device that the general streaming sender
+ * app should present to the receiver.
+ */
+public final class StreamingCall implements Parcelable {
+ /**
+ * The state of a {@code StreamingCall} when newly created. General streaming sender should
+ * continuously stream call audio to the sender device as long as the {@code StreamingCall} is
+ * in this state.
+ */
+ public static final int STATE_STREAMING = 1;
+
+ /**
+ * The state of a {@code StreamingCall} when in a holding state.
+ */
+ public static final int STATE_HOLDING = 2;
+
+ /**
+ * The state of a {@code StreamingCall} when it's either disconnected or pulled back to the
+ * original device.
+ */
+ public static final int STATE_DISCONNECTED = 3;
+
+ private StreamingCall(@NonNull Parcel in) {
+ mComponentName = in.readParcelable(ComponentName.class.getClassLoader());
+ mDisplayName = in.readString16NoHelper();
+ mAddress = in.readParcelable(Uri.class.getClassLoader());
+ mExtras = in.readBundle();
+ mState = in.readInt();
+ }
+
+ @NonNull
+ public static final Creator<StreamingCall> CREATOR = new Creator<>() {
+ @Override
+ public StreamingCall createFromParcel(@NonNull Parcel in) {
+ return new StreamingCall(in);
+ }
+
+ @Override
+ public StreamingCall[] newArray(int size) {
+ return new StreamingCall[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@androidx.annotation.NonNull Parcel dest, int flags) {
+ dest.writeParcelable(mComponentName, flags);
+ dest.writeString16NoHelper(mDisplayName);
+ dest.writeParcelable(mAddress, flags);
+ dest.writeBundle(mExtras);
+ dest.writeInt(mState);
+ }
+
+ /**
+ * @hide
+ */
+ @IntDef(prefix = { "STATE_" },
+ value = {
+ STATE_STREAMING,
+ STATE_HOLDING,
+ STATE_DISCONNECTED
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface StreamingCallState {}
+
+ private final ComponentName mComponentName;
+ private final String mDisplayName;
+ private final Uri mAddress;
+ private final Bundle mExtras;
+ @StreamingCallState
+ private int mState;
+ private StreamingCallAdapter mAdapter = null;
+
+ public StreamingCall(@NonNull ComponentName componentName, @NonNull String displayName,
+ @NonNull Uri address, @NonNull Bundle extras) {
+ mComponentName = componentName;
+ mDisplayName = displayName;
+ mAddress = address;
+ mExtras = extras;
+ mState = STATE_STREAMING;
+ }
+
+ /**
+ * @hide
+ */
+ public void setAdapter(StreamingCallAdapter adapter) {
+ mAdapter = adapter;
+ }
+
+ /**
+ * @return The {@link ComponentName} to identify the original voip app of this
+ * {@code StreamingCall}. General streaming sender app can use this to query necessary
+ * information (app icon etc.) in order to present notification of the streaming call on the
+ * receiver side.
+ */
+ @NonNull
+ public ComponentName getComponentName() {
+ return mComponentName;
+ }
+
+ /**
+ * @return The display name that the general streaming sender app can use this to present the
+ * {@code StreamingCall} to the receiver side.
+ */
+ @NonNull
+ public String getDisplayName() {
+ return mDisplayName;
+ }
+
+ /**
+ * @return The address (e.g., phone number) to which the {@code StreamingCall} is currently
+ * connected.
+ */
+ @NonNull
+ public Uri getAddress() {
+ return mAddress;
+ }
+
+ /**
+ * @return The state of this {@code StreamingCall}.
+ */
+ @StreamingCallState
+ public int getState() {
+ return mState;
+ }
+
+ /**
+ * @return The extra info the general streaming app need to stream the call from voip app or
+ * D2DI sdk.
+ */
+ @NonNull
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ /**
+ * Sets the state of this {@code StreamingCall}. The general streaming sender app can use this
+ * to request holding, unholding and disconnecting this {@code StreamingCall}.
+ * @param state The current streaming state of the call.
+ */
+ public void setStreamingState(@StreamingCallState int state) {
+ mAdapter.setStreamingState(state);
+ }
+}
diff --git a/telecomm/java/android/telecom/StreamingCallAdapter.java b/telecomm/java/android/telecom/StreamingCallAdapter.java
new file mode 100644
index 0000000..bd8727d
--- /dev/null
+++ b/telecomm/java/android/telecom/StreamingCallAdapter.java
@@ -0,0 +1,54 @@
+/*
+ * 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.telecom;
+
+import android.os.RemoteException;
+
+import com.android.internal.telecom.IStreamingCallAdapter;
+
+/**
+ * Receives commands from {@link CallStreamingService} implementations which should be executed by
+ * Telecom. When Telecom binds to a {@link CallStreamingService}, an instance of this class is given
+ * to the general streaming app through which it can manipulate the streaming calls. Whe the general
+ * streaming app is notified of new ongoing streaming calls, it can execute
+ * {@link StreamingCall#setStreamingState(int)} for the ongoing streaming calls the user on the
+ * receiver side would like to hold, unhold and disconnect.
+ *
+ * @hide
+ */
+public final class StreamingCallAdapter {
+ private final IStreamingCallAdapter mAdapter;
+
+ /**
+ * {@hide}
+ */
+ public StreamingCallAdapter(IStreamingCallAdapter adapter) {
+ mAdapter = adapter;
+ }
+
+ /**
+ * Instruct telecom to change the state of the streaming call.
+ *
+ * @param state The streaming state to set
+ */
+ public void setStreamingState(@StreamingCall.StreamingCallState int state) {
+ try {
+ mAdapter.setStreamingState(state);
+ } catch (RemoteException e) {
+ }
+ }
+}
diff --git a/telecomm/java/android/telecom/TelecomManager.java b/telecomm/java/android/telecom/TelecomManager.java
index af37ed5..7c86a75a 100644
--- a/telecomm/java/android/telecom/TelecomManager.java
+++ b/telecomm/java/android/telecom/TelecomManager.java
@@ -18,6 +18,7 @@
import static android.content.Intent.LOCAL_FLAG_FROM_SYSTEM;
import android.Manifest;
+import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -38,6 +39,7 @@
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
+import android.os.OutcomeReceiver;
import android.os.Process;
import android.os.RemoteException;
import android.os.ServiceManager;
@@ -49,6 +51,8 @@
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.telecom.ClientTransactionalServiceRepository;
+import com.android.internal.telecom.ClientTransactionalServiceWrapper;
import com.android.internal.telecom.ITelecomService;
import java.lang.annotation.Retention;
@@ -1056,6 +1060,14 @@
private final ITelecomService mTelecomServiceOverride;
+ /** @hide **/
+ private final ClientTransactionalServiceRepository mTransactionalServiceRepository =
+ new ClientTransactionalServiceRepository();
+ /** @hide **/
+ public static final int TELECOM_TRANSACTION_SUCCESS = 0;
+ /** @hide **/
+ public static final String TRANSACTION_CALL_ID_KEY = "TelecomCallId";
+
/**
* @hide
*/
@@ -2640,6 +2652,92 @@
}
/**
+ * Adds a new call with the specified {@link CallAttributes} to the telecom service. This method
+ * can be used to add both incoming and outgoing calls.
+ *
+ * <p>
+ * The difference between this API call and {@link TelecomManager#placeCall(Uri, Bundle)} or
+ * {@link TelecomManager#addNewIncomingCall(PhoneAccountHandle, Bundle)} is that this API
+ * will asynchronously provide an update on whether the new call was added successfully via
+ * an {@link OutcomeReceiver}. Additionally, callbacks will run on the executor thread that was
+ * passed in.
+ *
+ * <p>
+ * Note: Only packages that register with
+ * {@link PhoneAccount#CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS}
+ * can utilize this API. {@link PhoneAccount}s that set the capabilities
+ * {@link PhoneAccount#CAPABILITY_SIM_SUBSCRIPTION},
+ * {@link PhoneAccount#CAPABILITY_CALL_PROVIDER},
+ * {@link PhoneAccount#CAPABILITY_CONNECTION_MANAGER}
+ * are not supported and will cause an exception to be thrown.
+ *
+ * <p>
+ * Usage example:
+ * <pre>
+ *
+ * // An app should first define their own construct of a Call that overrides all the
+ * // {@link CallEventCallback}s
+ * private class MyVoipCall implements CallEventCallback {
+ * // override all the {@link CallEventCallback}s
+ * }
+ *
+ * PhoneAccountHandle handle = new PhoneAccountHandle(
+ * new ComponentName("com.example.voip.app",
+ * "com.example.voip.app.NewCallActivity"), "123");
+ *
+ * CallAttributes callAttributes = new CallAttributes.Builder(handle,
+ * CallAttributes.DIRECTION_OUTGOING,
+ * "John Smith", Uri.fromParts("tel", "123", null))
+ * .build();
+ *
+ * telecomManager.addCall(callAttributes, Runnable::run, new OutcomeReceiver() {
+ * public void onResult(CallControl callControl) {
+ * // The call has been added successfully
+ * }
+ * }, new MyVoipCall());
+ * </pre>
+ *
+ * @param callAttributes attributes of the new call (incoming or outgoing, address, etc. )
+ * @param executor thread to run background CallEventCallback updates on
+ * @param pendingControl OutcomeReceiver that receives the result of addCall transaction
+ * @param callEventCallback object that overrides CallEventCallback
+ */
+ @RequiresPermission(android.Manifest.permission.MANAGE_OWN_CALLS)
+ @SuppressLint("SamShouldBeLast")
+ public void addCall(@NonNull CallAttributes callAttributes,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<CallControl, CallException> pendingControl,
+ @NonNull CallEventCallback callEventCallback) {
+ Objects.requireNonNull(callAttributes);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(pendingControl);
+ Objects.requireNonNull(callEventCallback);
+
+ ITelecomService service = getTelecomService();
+ if (service != null) {
+ try {
+ // create or add the new call to a service wrapper w/ the same phoneAccountHandle
+ ClientTransactionalServiceWrapper transactionalServiceWrapper =
+ mTransactionalServiceRepository.addNewCallForTransactionalServiceWrapper(
+ callAttributes.getPhoneAccountHandle());
+
+ // couple all the args passed by the client
+ String newCallId = transactionalServiceWrapper.trackCall(callAttributes, executor,
+ pendingControl, callEventCallback);
+
+ // send args to server to process new call
+ service.addCall(callAttributes, transactionalServiceWrapper.getCallEventCallback(),
+ newCallId, mContext.getOpPackageName());
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException addCall: " + e);
+ e.rethrowFromSystemServer();
+ }
+ } else {
+ throw new IllegalStateException("Telecom service is not present");
+ }
+ }
+
+ /**
* Handles {@link Intent#ACTION_CALL} intents trampolined from UserCallActivity.
* @param intent The {@link Intent#ACTION_CALL} intent to handle.
* @param callingPackageProxy The original package that called this before it was trampolined.
diff --git a/telecomm/java/com/android/internal/telecom/ClientTransactionalServiceRepository.java b/telecomm/java/com/android/internal/telecom/ClientTransactionalServiceRepository.java
new file mode 100644
index 0000000..2eebbdb
--- /dev/null
+++ b/telecomm/java/com/android/internal/telecom/ClientTransactionalServiceRepository.java
@@ -0,0 +1,90 @@
+/*
+ * 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.telecom;
+
+import android.telecom.PhoneAccountHandle;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * @hide
+ */
+public class ClientTransactionalServiceRepository {
+
+ private static final Map<PhoneAccountHandle, ClientTransactionalServiceWrapper> LOOKUP_TABLE =
+ new ConcurrentHashMap<>();
+
+ /**
+ * creates a new {@link ClientTransactionalServiceWrapper} if this is the first call being
+ * tracked for a particular package Or adds a new call for an existing
+ * {@link ClientTransactionalServiceWrapper}
+ *
+ * @param phoneAccountHandle for a particular package requesting to create a call
+ * @return the {@link ClientTransactionalServiceWrapper} that is tied tot the PhoneAccountHandle
+ */
+ public ClientTransactionalServiceWrapper addNewCallForTransactionalServiceWrapper(
+ PhoneAccountHandle phoneAccountHandle) {
+
+ ClientTransactionalServiceWrapper service = null;
+ if (!hasExistingServiceWrapper(phoneAccountHandle)) {
+ service = new ClientTransactionalServiceWrapper(phoneAccountHandle, this);
+ } else {
+ service = getTransactionalServiceWrapper(phoneAccountHandle);
+ }
+
+ LOOKUP_TABLE.put(phoneAccountHandle, service);
+
+ return service;
+ }
+
+ private ClientTransactionalServiceWrapper getTransactionalServiceWrapper(
+ PhoneAccountHandle pah) {
+ return LOOKUP_TABLE.get(pah);
+ }
+
+ private boolean hasExistingServiceWrapper(PhoneAccountHandle pah) {
+ return LOOKUP_TABLE.containsKey(pah);
+ }
+
+ /**
+ * @param pah that is tied to a particular package with potential tracked calls
+ * @return if the {@link ClientTransactionalServiceWrapper} was successfully removed
+ */
+ public boolean removeServiceWrapper(PhoneAccountHandle pah) {
+ if (!hasExistingServiceWrapper(pah)) {
+ return false;
+ }
+ LOOKUP_TABLE.remove(pah);
+ return true;
+ }
+
+ /**
+ * @param pah that is tied to a particular package with potential tracked calls
+ * @param callId of the TransactionalCall that you want to remove
+ * @return if the call was successfully removed from the service wrapper
+ */
+ public boolean removeCallFromServiceWrapper(PhoneAccountHandle pah, String callId) {
+ if (!hasExistingServiceWrapper(pah)) {
+ return false;
+ }
+ ClientTransactionalServiceWrapper service = LOOKUP_TABLE.get(pah);
+ service.untrackCall(callId);
+ return true;
+ }
+
+}
diff --git a/telecomm/java/com/android/internal/telecom/ClientTransactionalServiceWrapper.java b/telecomm/java/com/android/internal/telecom/ClientTransactionalServiceWrapper.java
new file mode 100644
index 0000000..16816ff
--- /dev/null
+++ b/telecomm/java/com/android/internal/telecom/ClientTransactionalServiceWrapper.java
@@ -0,0 +1,283 @@
+/*
+ * 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.telecom;
+
+import static android.telecom.TelecomManager.TELECOM_TRANSACTION_SUCCESS;
+
+import android.os.Binder;
+import android.os.OutcomeReceiver;
+import android.os.ResultReceiver;
+import android.telecom.CallAttributes;
+import android.telecom.CallAudioState;
+import android.telecom.CallControl;
+import android.telecom.CallEventCallback;
+import android.telecom.CallException;
+import android.telecom.PhoneAccountHandle;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * wraps {@link CallEventCallback} and {@link CallControl} on a
+ * per-{@link android.telecom.PhoneAccountHandle} basis to track ongoing calls.
+ *
+ * @hide
+ */
+public class ClientTransactionalServiceWrapper {
+
+ private static final String TAG = ClientTransactionalServiceWrapper.class.getSimpleName();
+ private final PhoneAccountHandle mPhoneAccountHandle;
+ private final ClientTransactionalServiceRepository mRepository;
+ private final ConcurrentHashMap<String, TransactionalCall> mCallIdToTransactionalCall =
+ new ConcurrentHashMap<>();
+ private static final String EXECUTOR_FAIL_MSG =
+ "Telecom hit an exception while handling a CallEventCallback on an executor: ";
+
+ public ClientTransactionalServiceWrapper(PhoneAccountHandle handle,
+ ClientTransactionalServiceRepository repo) {
+ mPhoneAccountHandle = handle;
+ mRepository = repo;
+ }
+
+ /**
+ * remove the given call from the class HashMap
+ *
+ * @param callId that is tied to TransactionalCall object
+ */
+ public void untrackCall(String callId) {
+ Log.i(TAG, TextUtils.formatSimple("removeCall: with id=[%s]", callId));
+ if (mCallIdToTransactionalCall.containsKey(callId)) {
+ // remove the call from the hashmap
+ TransactionalCall call = mCallIdToTransactionalCall.remove(callId);
+ // null out interface to avoid memory leaks
+ CallControl control = call.getCallControl();
+ if (control != null) {
+ call.setCallControl(null);
+ }
+ }
+ // possibly cleanup service wrapper if there are no more calls
+ if (mCallIdToTransactionalCall.size() == 0) {
+ mRepository.removeServiceWrapper(mPhoneAccountHandle);
+ }
+ }
+
+ /**
+ * start tracking a newly created call for a particular package
+ *
+ * @param callAttributes of the new call
+ * @param executor to run callbacks on
+ * @param pendingControl that allows telecom to call into the client
+ * @param callback that overrides the CallEventCallback
+ * @return the callId of the newly created call
+ */
+ public String trackCall(CallAttributes callAttributes, Executor executor,
+ OutcomeReceiver<CallControl, CallException> pendingControl,
+ CallEventCallback callback) {
+ // generate a new id for this new call
+ String newCallId = UUID.randomUUID().toString();
+
+ // couple the objects passed from the client side
+ mCallIdToTransactionalCall.put(newCallId, new TransactionalCall(newCallId, callAttributes,
+ executor, pendingControl, callback));
+
+ return newCallId;
+ }
+
+ public ICallEventCallback getCallEventCallback() {
+ return mCallEventCallback;
+ }
+
+ /**
+ * Consumers that is to be completed by the client and the result relayed back to telecom server
+ * side via a {@link ResultReceiver}. see com.android.server.telecom.TransactionalServiceWrapper
+ * for how the response is handled.
+ */
+ private class ReceiverWrapper implements Consumer<Boolean> {
+ private final ResultReceiver mRepeaterReceiver;
+
+ ReceiverWrapper(ResultReceiver resultReceiver) {
+ mRepeaterReceiver = resultReceiver;
+ }
+
+ @Override
+ public void accept(Boolean clientCompletedCallbackSuccessfully) {
+ if (clientCompletedCallbackSuccessfully) {
+ mRepeaterReceiver.send(TELECOM_TRANSACTION_SUCCESS, null);
+ } else {
+ mRepeaterReceiver.send(CallException.CODE_ERROR_UNKNOWN, null);
+ }
+ }
+
+ @Override
+ public Consumer<Boolean> andThen(Consumer<? super Boolean> after) {
+ return Consumer.super.andThen(after);
+ }
+ }
+
+ private final ICallEventCallback mCallEventCallback = new ICallEventCallback.Stub() {
+
+ private static final String ON_SET_ACTIVE = "onSetActive";
+ private static final String ON_SET_INACTIVE = "onSetInactive";
+ private static final String ON_ANSWER = "onAnswer";
+ private static final String ON_REJECT = "onReject";
+ private static final String ON_DISCONNECT = "onDisconnect";
+ private static final String ON_STREAMING_STARTED = "onStreamingStarted";
+
+ private void handleCallEventCallback(String action, String callId, int code,
+ ResultReceiver ackResultReceiver) {
+ Log.i(TAG, TextUtils.formatSimple("hCEC: id=[%s], action=[%s]", callId, action));
+ // lookup the callEventCallback associated with the particular call
+ TransactionalCall call = mCallIdToTransactionalCall.get(callId);
+
+ if (call != null) {
+ // Get the CallEventCallback interface
+ CallEventCallback callback = call.getCallEventCallback();
+ // Get Receiver to wait on client ack
+ ReceiverWrapper outcomeReceiverWrapper = new ReceiverWrapper(ackResultReceiver);
+
+ // wait for the client to complete the CallEventCallback
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ call.getExecutor().execute(() -> {
+ switch (action) {
+ case ON_SET_ACTIVE:
+ callback.onSetActive(outcomeReceiverWrapper);
+ break;
+ case ON_SET_INACTIVE:
+ callback.onSetInactive(outcomeReceiverWrapper);
+ break;
+ case ON_REJECT:
+ callback.onReject(outcomeReceiverWrapper);
+ untrackCall(callId);
+ break;
+ case ON_DISCONNECT:
+ callback.onDisconnect(outcomeReceiverWrapper);
+ untrackCall(callId);
+ break;
+ case ON_ANSWER:
+ callback.onAnswer(code, outcomeReceiverWrapper);
+ break;
+ case ON_STREAMING_STARTED:
+ callback.onCallStreamingStarted(outcomeReceiverWrapper);
+ break;
+ }
+ });
+ } catch (Exception e) {
+ Log.e(TAG, EXECUTOR_FAIL_MSG + e);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ }
+
+ @Override
+ public void onAddCallControl(String callId, int resultCode, ICallControl callControl,
+ CallException transactionalException) {
+ Log.i(TAG, TextUtils.formatSimple("oACC: id=[%s], code=[%d]", callId, resultCode));
+ TransactionalCall call = mCallIdToTransactionalCall.get(callId);
+
+ if (call != null) {
+ OutcomeReceiver<CallControl, CallException> pendingControl =
+ call.getPendingControl();
+
+ if (resultCode == TELECOM_TRANSACTION_SUCCESS) {
+
+ // create the interface object that the client will interact with
+ CallControl control = new CallControl(callId, callControl, mRepository,
+ mPhoneAccountHandle);
+ // give the client the object via the OR that was passed into addCall
+ pendingControl.onResult(control);
+
+ // store for later reference
+ call.setCallControl(control);
+ } else {
+ pendingControl.onError(transactionalException);
+ mCallIdToTransactionalCall.remove(callId);
+ }
+
+ } else {
+ untrackCall(callId);
+ Log.e(TAG, "oACC: TransactionalCall object not found for call w/ id=" + callId);
+ }
+ }
+
+ @Override
+ public void onSetActive(String callId, ResultReceiver resultReceiver) {
+ handleCallEventCallback(ON_SET_ACTIVE, callId, 0, resultReceiver);
+ }
+
+
+ @Override
+ public void onSetInactive(String callId, ResultReceiver resultReceiver) {
+ handleCallEventCallback(ON_SET_INACTIVE, callId, 0, resultReceiver);
+ }
+
+ @Override
+ public void onAnswer(String callId, int videoState, ResultReceiver resultReceiver) {
+ handleCallEventCallback(ON_ANSWER, callId, videoState, resultReceiver);
+ }
+
+ @Override
+ public void onReject(String callId, ResultReceiver resultReceiver) {
+ handleCallEventCallback(ON_REJECT, callId, 0, resultReceiver);
+ }
+
+ @Override
+ public void onDisconnect(String callId, ResultReceiver resultReceiver) {
+ handleCallEventCallback(ON_DISCONNECT, callId, 0, resultReceiver);
+ }
+
+ @Override
+ public void onCallAudioStateChanged(String callId, CallAudioState callAudioState) {
+ Log.i(TAG, TextUtils.formatSimple("onCallAudioStateChanged: callId=[%s]", callId));
+ // lookup the callEventCallback associated with the particular call
+ TransactionalCall call = mCallIdToTransactionalCall.get(callId);
+ if (call != null) {
+ CallEventCallback callback = call.getCallEventCallback();
+ Executor executor = call.getExecutor();
+ executor.execute(() -> callback.onCallAudioStateChanged(callAudioState));
+ }
+ }
+
+ @Override
+ public void removeCallFromTransactionalServiceWrapper(String callId) {
+ untrackCall(callId);
+ }
+
+ @Override
+ public void onCallStreamingStarted(String callId, ResultReceiver resultReceiver) {
+ handleCallEventCallback(ON_STREAMING_STARTED, callId, 0, resultReceiver);
+ }
+
+ @Override
+ public void onCallStreamingFailed(String callId, int reason) {
+ Log.i(TAG, TextUtils.formatSimple("onCallAudioStateChanged: callId=[%s], reason=[%s]",
+ callId, reason));
+ // lookup the callEventCallback associated with the particular call
+ TransactionalCall call = mCallIdToTransactionalCall.get(callId);
+ if (call != null) {
+ CallEventCallback callback = call.getCallEventCallback();
+ Executor executor = call.getExecutor();
+ executor.execute(() -> callback.onCallStreamingFailed(reason));
+ }
+ }
+ };
+}
diff --git a/telecomm/java/com/android/internal/telecom/ICallControl.aidl b/telecomm/java/com/android/internal/telecom/ICallControl.aidl
new file mode 100644
index 0000000..dc0aeac
--- /dev/null
+++ b/telecomm/java/com/android/internal/telecom/ICallControl.aidl
@@ -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.
+ */
+
+package com.android.internal.telecom;
+
+import android.telecom.CallControl;
+import android.telecom.DisconnectCause;
+import android.os.ResultReceiver;
+
+/**
+ * {@hide}
+ */
+oneway interface ICallControl {
+ void setActive(String callId, in ResultReceiver callback);
+ void setInactive(String callId, in ResultReceiver callback);
+ void disconnect(String callId, in DisconnectCause disconnectCause, in ResultReceiver callback);
+ void rejectCall(String callId, in ResultReceiver callback);
+ void startCallStreaming(String callId, in ResultReceiver callback);
+}
\ No newline at end of file
diff --git a/telecomm/java/com/android/internal/telecom/ICallEventCallback.aidl b/telecomm/java/com/android/internal/telecom/ICallEventCallback.aidl
new file mode 100644
index 0000000..c45ef97
--- /dev/null
+++ b/telecomm/java/com/android/internal/telecom/ICallEventCallback.aidl
@@ -0,0 +1,43 @@
+/*
+ * 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.telecom;
+
+import android.telecom.CallControl;
+import com.android.internal.telecom.ICallControl;
+import android.os.ResultReceiver;
+import android.telecom.CallAudioState;
+import android.telecom.CallException;
+
+/**
+ * {@hide}
+ */
+oneway interface ICallEventCallback {
+ // publicly exposed. Client should override
+ void onAddCallControl(String callId, int resultCode, in ICallControl callControl,
+ in CallException exception);
+ void onSetActive(String callId, in ResultReceiver callback);
+ void onSetInactive(String callId, in ResultReceiver callback);
+ void onAnswer(String callId, int videoState, in ResultReceiver callback);
+ void onReject(String callId, in ResultReceiver callback);
+ void onDisconnect(String callId, in ResultReceiver callback);
+ void onCallAudioStateChanged(String callId, in CallAudioState callAudioState);
+ // Streaming related. Client registered call streaming capabilities should override
+ void onCallStreamingStarted(String callId, in ResultReceiver callback);
+ void onCallStreamingFailed(String callId, int reason);
+ // hidden methods that help with cleanup
+ void removeCallFromTransactionalServiceWrapper(String callId);
+}
\ No newline at end of file
diff --git a/telecomm/java/com/android/internal/telecom/ICallStreamingService.aidl b/telecomm/java/com/android/internal/telecom/ICallStreamingService.aidl
new file mode 100644
index 0000000..6d53fd2
--- /dev/null
+++ b/telecomm/java/com/android/internal/telecom/ICallStreamingService.aidl
@@ -0,0 +1,35 @@
+/*
+ * 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.telecom;
+
+import android.telecom.StreamingCall;
+
+import com.android.internal.telecom.IStreamingCallAdapter;
+
+/**
+ * Internal remote interface for call streaming services.
+ *
+ * @see android.telecom.CallStreamingService
+ *
+ * {@hide}
+ */
+oneway interface ICallStreamingService {
+ void setStreamingCallAdapter(in IStreamingCallAdapter streamingCallAdapter);
+ void onCallStreamingStarted(in StreamingCall call);
+ void onCallStreamingStopped();
+ void onCallStreamingStateChanged(int state);
+}
\ No newline at end of file
diff --git a/telecomm/java/com/android/internal/telecom/IStreamingCallAdapter.aidl b/telecomm/java/com/android/internal/telecom/IStreamingCallAdapter.aidl
new file mode 100644
index 0000000..51424a6
--- /dev/null
+++ b/telecomm/java/com/android/internal/telecom/IStreamingCallAdapter.aidl
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.telecom;
+
+/**
+ * Internal remote callback interface for call streaming services.
+ *
+ * @see android.telecom.StreamingCallAdapter
+ *
+ * {@hide}
+ */
+oneway interface IStreamingCallAdapter {
+ void setStreamingState(int state);
+}
\ No newline at end of file
diff --git a/telecomm/java/com/android/internal/telecom/ITelecomService.aidl b/telecomm/java/com/android/internal/telecom/ITelecomService.aidl
index f1a6dd1..fdcb974 100644
--- a/telecomm/java/com/android/internal/telecom/ITelecomService.aidl
+++ b/telecomm/java/com/android/internal/telecom/ITelecomService.aidl
@@ -25,6 +25,8 @@
import android.os.UserHandle;
import android.telecom.PhoneAccount;
import android.content.pm.ParceledListSlice;
+import android.telecom.CallAttributes;
+import com.android.internal.telecom.ICallEventCallback;
/**
* Interface used to interact with Telecom. Mostly this is used by TelephonyManager for passing
@@ -391,4 +393,10 @@
*/
boolean isInSelfManagedCall(String packageName, in UserHandle userHandle,
String callingPackage);
+
+ /**
+ * @see TelecomServiceImpl#addCall
+ */
+ void addCall(in CallAttributes callAttributes, in ICallEventCallback callback, String callId,
+ String callingPackage);
}
diff --git a/telecomm/java/com/android/internal/telecom/TransactionalCall.java b/telecomm/java/com/android/internal/telecom/TransactionalCall.java
new file mode 100644
index 0000000..d9c8210
--- /dev/null
+++ b/telecomm/java/com/android/internal/telecom/TransactionalCall.java
@@ -0,0 +1,76 @@
+/*
+ * 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.telecom;
+
+import android.os.OutcomeReceiver;
+import android.telecom.CallAttributes;
+import android.telecom.CallControl;
+import android.telecom.CallEventCallback;
+import android.telecom.CallException;
+
+import java.util.concurrent.Executor;
+
+/**
+ * @hide
+ */
+public class TransactionalCall {
+
+ private final String mCallId;
+ private final CallAttributes mCallAttributes;
+ private final Executor mExecutor;
+ private final OutcomeReceiver<CallControl, CallException> mPendingControl;
+ private final CallEventCallback mCallEventCallback;
+ private CallControl mCallControl;
+
+ public TransactionalCall(String callId, CallAttributes callAttributes,
+ Executor executor, OutcomeReceiver<CallControl, CallException> pendingControl,
+ CallEventCallback callEventCallback) {
+ mCallId = callId;
+ mCallAttributes = callAttributes;
+ mExecutor = executor;
+ mPendingControl = pendingControl;
+ mCallEventCallback = callEventCallback;
+ }
+
+ public void setCallControl(CallControl callControl) {
+ mCallControl = callControl;
+ }
+
+ public CallControl getCallControl() {
+ return mCallControl;
+ }
+
+ public String getCallId() {
+ return mCallId;
+ }
+
+ public CallAttributes getCallAttributes() {
+ return mCallAttributes;
+ }
+
+ public Executor getExecutor() {
+ return mExecutor;
+ }
+
+ public OutcomeReceiver<CallControl, CallException> getPendingControl() {
+ return mPendingControl;
+ }
+
+ public CallEventCallback getCallEventCallback() {
+ return mCallEventCallback;
+ }
+}
diff --git a/telephony/common/com/android/internal/telephony/TelephonyPermissions.java b/telephony/common/com/android/internal/telephony/TelephonyPermissions.java
index fdf69430..f90eabc 100644
--- a/telephony/common/com/android/internal/telephony/TelephonyPermissions.java
+++ b/telephony/common/com/android/internal/telephony/TelephonyPermissions.java
@@ -18,6 +18,7 @@
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import android.Manifest;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.AppOpsManager;
import android.content.Context;
@@ -822,4 +823,35 @@
}
return Integer.MAX_VALUE;
}
+
+ /**
+ * Check if calling user is associated with the given subscription.
+ * @param context Context
+ * @param subId subscription ID
+ * @param callerUserHandle caller user handle
+ * @return false if user is not associated with the subscription.
+ */
+ public static boolean checkSubscriptionAssociatedWithUser(@NonNull Context context, int subId,
+ @NonNull UserHandle callerUserHandle) {
+ if (!SubscriptionManager.isValidSubscriptionId(subId)) {
+ // No subscription on device, return true.
+ return true;
+ }
+
+ SubscriptionManager subManager = context.getSystemService(SubscriptionManager.class);
+ final long token = Binder.clearCallingIdentity();
+ try {
+ if ((subManager != null) &&
+ (!subManager.isSubscriptionAssociatedWithUser(subId, callerUserHandle))) {
+ // If subId is not associated with calling user, return false.
+ Log.e(LOG_TAG,"User[User ID:" + callerUserHandle.getIdentifier()
+ + "] is not associated with Subscription ID:" + subId);
+ return false;
+
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ return true;
+ }
}
diff --git a/telephony/java/android/telephony/SmsManager.java b/telephony/java/android/telephony/SmsManager.java
index c53b463..e6f1349 100644
--- a/telephony/java/android/telephony/SmsManager.java
+++ b/telephony/java/android/telephony/SmsManager.java
@@ -2349,6 +2349,7 @@
RESULT_SMS_SEND_RETRY_FAILED,
RESULT_REMOTE_EXCEPTION,
RESULT_NO_DEFAULT_SMS_APP,
+ RESULT_USER_NOT_ALLOWED,
RESULT_RIL_RADIO_NOT_AVAILABLE,
RESULT_RIL_SMS_SEND_FAIL_RETRY,
RESULT_RIL_NETWORK_REJECT,
@@ -2543,6 +2544,13 @@
*/
public static final int RESULT_NO_DEFAULT_SMS_APP = 32;
+ /**
+ * User is not associated with the subscription.
+ * TODO(b/263279115): Make this error code public.
+ * @hide
+ */
+ public static final int RESULT_USER_NOT_ALLOWED = 33;
+
// Radio Error results
/**
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index 4afc943..0638189 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -4418,4 +4418,69 @@
}
return null;
}
+
+ /**
+ * Check if subscription and user are associated with each other.
+ *
+ * @param subscriptionId the subId of the subscription
+ * @param userHandle user handle of the user
+ * @return {@code true} if subscription is associated with user
+ * {code true} if there are no subscriptions on device
+ * else {@code false} if subscription is not associated with user.
+ *
+ * @throws IllegalArgumentException if subscription is invalid.
+ * @throws SecurityException if the caller doesn't have permissions required.
+ * @throws IllegalStateException if subscription service is not available.
+ *
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION)
+ public boolean isSubscriptionAssociatedWithUser(int subscriptionId,
+ @NonNull UserHandle userHandle) {
+ if (!isValidSubscriptionId(subscriptionId)) {
+ throw new IllegalArgumentException("[isSubscriptionAssociatedWithUser]: "
+ + "Invalid subscriptionId: " + subscriptionId);
+ }
+
+ try {
+ ISub iSub = TelephonyManager.getSubscriptionService();
+ if (iSub != null) {
+ return iSub.isSubscriptionAssociatedWithUser(subscriptionId, userHandle);
+ } else {
+ throw new IllegalStateException("[isSubscriptionAssociatedWithUser]: "
+ + "subscription service unavailable");
+ }
+ } catch (RemoteException ex) {
+ ex.rethrowAsRuntimeException();
+ }
+ return false;
+ }
+
+ /**
+ * Get list of subscriptions associated with user.
+ *
+ * @param userHandle user handle of the user
+ * @return list of subscriptionInfo associated with the user.
+ *
+ * @throws SecurityException if the caller doesn't have permissions required.
+ * @throws IllegalStateException if subscription service is not available.
+ *
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION)
+ public @NonNull List<SubscriptionInfo> getSubscriptionInfoListAssociatedWithUser(
+ @NonNull UserHandle userHandle) {
+ try {
+ ISub iSub = TelephonyManager.getSubscriptionService();
+ if (iSub != null) {
+ return iSub.getSubscriptionInfoListAssociatedWithUser(userHandle);
+ } else {
+ throw new IllegalStateException("[getSubscriptionInfoListAssociatedWithUser]: "
+ + "subscription service unavailable");
+ }
+ } catch (RemoteException ex) {
+ ex.rethrowAsRuntimeException();
+ }
+ return new ArrayList<>();
+ }
}
diff --git a/telephony/java/com/android/internal/telephony/ISub.aidl b/telephony/java/com/android/internal/telephony/ISub.aidl
index c5f6902..25a714a 100644
--- a/telephony/java/com/android/internal/telephony/ISub.aidl
+++ b/telephony/java/com/android/internal/telephony/ISub.aidl
@@ -326,4 +326,34 @@
* @throws IllegalArgumentException if subId is invalid.
*/
UserHandle getSubscriptionUserHandle(int subId);
+
+ /**
+ * Check if subscription and user are associated with each other.
+ *
+ * @param subscriptionId the subId of the subscription
+ * @param userHandle user handle of the user
+ * @return {@code true} if subscription is associated with user
+ * {code true} if there are no subscriptions on device
+ * else {@code false} if subscription is not associated with user.
+ *
+ * @throws IllegalArgumentException if subscription is invalid.
+ * @throws SecurityException if the caller doesn't have permissions required.
+ * @throws IllegalStateException if subscription service is not available.
+ *
+ * @hide
+ */
+ boolean isSubscriptionAssociatedWithUser(int subscriptionId, in UserHandle userHandle);
+
+ /**
+ * Get list of subscriptions associated with user.
+ *
+ * @param userHandle user handle of the user
+ * @return list of subscriptionInfo associated with the user.
+ *
+ * @throws SecurityException if the caller doesn't have permissions required.
+ * @throws IllegalStateException if subscription service is not available.
+ *
+ * @hide
+ */
+ List<SubscriptionInfo> getSubscriptionInfoListAssociatedWithUser(in UserHandle userHandle);
}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt
index f9a245a..f26ce25 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt
@@ -22,6 +22,8 @@
import com.android.server.wm.flicker.traces.region.RegionSubject
import com.android.server.wm.traces.common.ComponentNameMatcher
import com.android.server.wm.traces.common.IComponentNameMatcher
+import com.android.server.wm.traces.common.service.PlatformConsts
+import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
/**
* Checks that [ComponentNameMatcher.STATUS_BAR] window is visible and above the app windows in all
@@ -202,13 +204,15 @@
* Asserts that the [ComponentNameMatcher.STATUS_BAR] layer is at the correct position at the start
* of the SF trace
*/
-fun FlickerTest.statusBarLayerPositionAtStart() {
+fun FlickerTest.statusBarLayerPositionAtStart(
+ wmTrace: WindowManagerTrace? = this.reader.readWmTrace()
+) {
+ // collect navbar position for the equivalent WM state
+ val state = wmTrace?.firstOrNull() ?: error("WM state missing in $this")
+ val display = state.getDisplay(PlatformConsts.DEFAULT_DISPLAY) ?: error("Display not found")
+ val navBarPosition = WindowUtils.getExpectedStatusBarPosition(display)
assertLayersStart {
- val display =
- this.entry.displays.minByOrNull { it.id }
- ?: throw RuntimeException("There is no display!")
- this.visibleRegion(ComponentNameMatcher.STATUS_BAR)
- .coversExactly(WindowUtils.getStatusBarPosition(display))
+ this.visibleRegion(ComponentNameMatcher.STATUS_BAR).coversExactly(navBarPosition)
}
}
@@ -216,13 +220,15 @@
* Asserts that the [ComponentNameMatcher.STATUS_BAR] layer is at the correct position at the end of
* the SF trace
*/
-fun FlickerTest.statusBarLayerPositionAtEnd() {
+fun FlickerTest.statusBarLayerPositionAtEnd(
+ wmTrace: WindowManagerTrace? = this.reader.readWmTrace()
+) {
+ // collect navbar position for the equivalent WM state
+ val state = wmTrace?.lastOrNull() ?: error("WM state missing in $this")
+ val display = state.getDisplay(PlatformConsts.DEFAULT_DISPLAY) ?: error("Display not found")
+ val navBarPosition = WindowUtils.getExpectedStatusBarPosition(display)
assertLayersEnd {
- val display =
- this.entry.displays.minByOrNull { it.id }
- ?: throw RuntimeException("There is no display!")
- this.visibleRegion(ComponentNameMatcher.STATUS_BAR)
- .coversExactly(WindowUtils.getStatusBarPosition(display))
+ this.visibleRegion(ComponentNameMatcher.STATUS_BAR).coversExactly(navBarPosition)
}
}
diff --git a/tests/HwAccelerationTest/src/com/android/test/hwui/MeshActivity.java b/tests/HwAccelerationTest/src/com/android/test/hwui/MeshActivity.java
index efe242c..d3a5885 100644
--- a/tests/HwAccelerationTest/src/com/android/test/hwui/MeshActivity.java
+++ b/tests/HwAccelerationTest/src/com/android/test/hwui/MeshActivity.java
@@ -32,6 +32,7 @@
import java.nio.FloatBuffer;
import java.nio.ShortBuffer;
+import java.util.ArrayList;
public class MeshActivity extends Activity {
@Override
@@ -64,7 +65,9 @@
vertexBuffer.put(5, 400.0f);
vertexBuffer.rewind();
Mesh mesh = Mesh.make(
- meshSpec, Mesh.Mode.Triangles, vertexBuffer, 3, new Rect(0, 0, 1000, 1000));
+ meshSpec, Mesh.TRIANGLES, vertexBuffer, 3, new Rect(0, 0, 1000, 1000));
+
+ canvas.drawMesh(mesh, BlendMode.COLOR, new Paint());
int numTriangles = 100;
// number of triangles plus first 2 vertices
@@ -95,12 +98,10 @@
}
iVertexBuffer.rewind();
indexBuffer.rewind();
- Mesh mesh2 = Mesh.makeIndexed(meshSpec, Mesh.Mode.Triangles, iVertexBuffer, 102,
- indexBuffer, new Rect(0, 0, 1000, 1000));
-
+ Mesh mesh2 = Mesh.makeIndexed(meshSpec, Mesh.TRIANGLES, iVertexBuffer, 102, indexBuffer,
+ new Rect(0, 0, 1000, 1000));
Paint paint = new Paint();
paint.setColor(Color.RED);
- canvas.drawMesh(mesh, BlendMode.COLOR, new Paint());
canvas.drawMesh(mesh2, BlendMode.COLOR, paint);
}
@@ -114,10 +115,9 @@
+ " color = vec4(1.0, 0.0, 0.0, 1.0);"
+ " return varyings.position;\n"
+ "}";
- Attribute[] attList =
- new Attribute[] {new Attribute(MeshSpecification.FLOAT2, 0, "position")};
- Varying[] varyList =
- new MeshSpecification.Varying[] {};
+ ArrayList<Attribute> attList = new ArrayList<>();
+ attList.add(new Attribute(MeshSpecification.FLOAT2, 0, "position"));
+ ArrayList<Varying> varyList = new ArrayList<>();
return MeshSpecification.make(attList, 8, varyList, vs, fs);
}
}
diff --git a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/AutoShowTest.java b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/AutoShowTest.java
index 8e4ecf1..9b0f952 100644
--- a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/AutoShowTest.java
+++ b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/AutoShowTest.java
@@ -61,10 +61,11 @@
public final class AutoShowTest {
@Rule public UnlockScreenRule mUnlockScreenRule = new UnlockScreenRule();
-
- @Rule
- public ScreenCaptureRule mScreenCaptureRule =
+ @Rule public ScreenCaptureRule mScreenCaptureRule =
new ScreenCaptureRule("/sdcard/InputMethodStressTest");
+ @Rule public DisableLockScreenRule mDisableLockScreenRule = new DisableLockScreenRule();
+ @Rule public ScreenOrientationRule mScreenOrientationRule =
+ new ScreenOrientationRule(true /* isPortrait */);
// TODO(b/240359838): add test case {@code Configuration.SCREENLAYOUT_SIZE_LARGE}.
@Parameterized.Parameters(
diff --git a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/DisableLockScreenRule.java b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/DisableLockScreenRule.java
new file mode 100644
index 0000000..d95decf
--- /dev/null
+++ b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/DisableLockScreenRule.java
@@ -0,0 +1,53 @@
+/*
+ * 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.inputmethod.stresstest;
+
+import android.support.test.uiautomator.UiDevice;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+
+import java.io.IOException;
+
+/** Disable lock screen during the test. */
+public class DisableLockScreenRule extends TestWatcher {
+ private static final String LOCK_SCREEN_OFF_COMMAND = "locksettings set-disabled true";
+ private static final String LOCK_SCREEN_ON_COMMAND = "locksettings set-disabled false";
+
+ private final UiDevice mUiDevice =
+ UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+
+ @Override
+ protected void starting(Description description) {
+ try {
+ mUiDevice.executeShellCommand(LOCK_SCREEN_OFF_COMMAND);
+ } catch (IOException e) {
+ throw new RuntimeException("Could not disable lock screen.", e);
+ }
+ }
+
+ @Override
+ protected void finished(Description description) {
+ try {
+ mUiDevice.executeShellCommand(LOCK_SCREEN_ON_COMMAND);
+ } catch (IOException e) {
+ throw new RuntimeException("Could not enable lock screen.", e);
+ }
+ }
+}
diff --git a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeOpenCloseStressTest.java b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeOpenCloseStressTest.java
index 82acfb6..a6131ce 100644
--- a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeOpenCloseStressTest.java
+++ b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeOpenCloseStressTest.java
@@ -67,10 +67,11 @@
private static final int NUM_TEST_ITERATIONS = 10;
@Rule public UnlockScreenRule mUnlockScreenRule = new UnlockScreenRule();
-
- @Rule
- public ScreenCaptureRule mScreenCaptureRule =
+ @Rule public ScreenCaptureRule mScreenCaptureRule =
new ScreenCaptureRule("/sdcard/InputMethodStressTest");
+ @Rule public DisableLockScreenRule mDisableLockScreenRule = new DisableLockScreenRule();
+ @Rule public ScreenOrientationRule mScreenOrientationRule =
+ new ScreenOrientationRule(true /* isPortrait */);
private final Instrumentation mInstrumentation;
private final int mSoftInputFlags;
@@ -485,7 +486,6 @@
UiDevice uiDevice = UiDevice.getInstance(mInstrumentation);
- uiDevice.freezeRotation();
uiDevice.setOrientationRight();
uiDevice.waitForIdle();
Thread.sleep(1000);
@@ -502,7 +502,6 @@
uiDevice.setOrientationNatural();
uiDevice.waitForIdle();
- uiDevice.unfreezeRotation();
}
private static void verifyShowBehavior(TestActivity activity) {
diff --git a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ScreenOrientationRule.java b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ScreenOrientationRule.java
new file mode 100644
index 0000000..bc3b1ef
--- /dev/null
+++ b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ScreenOrientationRule.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.inputmethod.stresstest;
+
+import android.os.RemoteException;
+import android.support.test.uiautomator.UiDevice;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+
+import java.io.IOException;
+
+/**
+ * Disable auto-rotate during the test and set the screen orientation to portrait or landscape
+ * before the test starts.
+ */
+public class ScreenOrientationRule extends TestWatcher {
+ private static final String SET_PORTRAIT_MODE_CMD = "settings put system user_rotation 0";
+ private static final String SET_LANDSCAPE_MODE_CMD = "settings put system user_rotation 1";
+
+ private final boolean mIsPortrait;
+ private final UiDevice mUiDevice =
+ UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+
+ ScreenOrientationRule(boolean isPortrait) {
+ mIsPortrait = isPortrait;
+ }
+
+ @Override
+ protected void starting(Description description) {
+ try {
+ mUiDevice.freezeRotation();
+ mUiDevice.executeShellCommand(mIsPortrait ? SET_PORTRAIT_MODE_CMD :
+ SET_LANDSCAPE_MODE_CMD);
+ } catch (IOException e) {
+ throw new RuntimeException("Could not set screen orientation.", e);
+ } catch (RemoteException e) {
+ throw new RuntimeException("Could not freeze rotation.", e);
+ }
+ }
+
+ @Override
+ protected void finished(Description description) {
+ try {
+ mUiDevice.unfreezeRotation();
+ } catch (RemoteException e) {
+ throw new RuntimeException("Could not unfreeze screen rotation.", e);
+ }
+ }
+}
diff --git a/tests/Internal/src/com/android/internal/app/AppLocaleCollectorTest.java b/tests/Internal/src/com/android/internal/app/AppLocaleCollectorTest.java
new file mode 100644
index 0000000..b24ac3c
--- /dev/null
+++ b/tests/Internal/src/com/android/internal/app/AppLocaleCollectorTest.java
@@ -0,0 +1,170 @@
+/*
+ * 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.app;
+
+import static com.android.internal.app.AppLocaleStore.AppLocaleResult.LocaleStatus.GET_SUPPORTED_LANGUAGE_FROM_LOCAL_CONFIG;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.anyObject;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.app.LocaleStore.LocaleInfo;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * Unit tests for the {@link AppLocaleCollector}.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class AppLocaleCollectorTest {
+ private static final String TAG = "AppLocaleCollectorTest";
+ private AppLocaleCollector mAppLocaleCollector;
+ private LocaleStore.LocaleInfo mAppCurrentLocale;
+ private Set<LocaleInfo> mAllAppActiveLocales;
+ private Set<LocaleInfo> mImeLocales;
+ private List<LocaleInfo> mSystemCurrentLocales;
+ private Set<LocaleInfo> mSystemSupportedLocales;
+ private AppLocaleStore.AppLocaleResult mResult;
+ private static final String PKG1 = "pkg1";
+ private static final int NONE = LocaleInfo.SUGGESTION_TYPE_NONE;
+ private static final int SIM = LocaleInfo.SUGGESTION_TYPE_SIM;
+ private static final int CFG = LocaleInfo.SUGGESTION_TYPE_CFG;
+ private static final int SIM_CFG = SIM | CFG;
+ private static final int CURRENT = LocaleInfo.SUGGESTION_TYPE_CURRENT;
+ private static final int SYSTEM = LocaleInfo.SUGGESTION_TYPE_SYSTEM_LANGUAGE;
+ private static final int OTHERAPP = LocaleInfo.SUGGESTION_TYPE_OTHER_APP_LANGUAGE;
+
+ @Before
+ public void setUp() throws Exception {
+ mAppLocaleCollector = spy(
+ new AppLocaleCollector(InstrumentationRegistry.getContext(), PKG1));
+
+ mAppCurrentLocale = createLocaleInfo("en-US", CURRENT);
+ mAllAppActiveLocales = initAllAppActivatedLocales();
+ mImeLocales = initImeLocales();
+ mSystemSupportedLocales = initSystemSupportedLocales();
+ mSystemCurrentLocales = initSystemCurrentLocales();
+ mResult = new AppLocaleStore.AppLocaleResult(GET_SUPPORTED_LANGUAGE_FROM_LOCAL_CONFIG,
+ initAppSupportedLocale());
+ }
+
+ @Test
+ public void testGetSupportedLocaleList() {
+ doReturn(mAppCurrentLocale).when(mAppLocaleCollector).getAppCurrentLocale();
+ doReturn(mResult).when(mAppLocaleCollector).getAppSupportedLocales();
+ doReturn(mAllAppActiveLocales).when(mAppLocaleCollector).getAllAppActiveLocales();
+ doReturn(mImeLocales).when(mAppLocaleCollector).getActiveImeLocales();
+ doReturn(mSystemSupportedLocales).when(mAppLocaleCollector).getSystemSupportedLocale(
+ anyObject(), eq(null), eq(true));
+ doReturn(mSystemCurrentLocales).when(mAppLocaleCollector).getSystemCurrentLocale();
+
+ Set<LocaleInfo> result = mAppLocaleCollector.getSupportedLocaleList(null, true, false);
+
+ HashMap<String, Integer> expectedResult = getExpectedResult();
+ assertEquals(result.size(), expectedResult.size());
+ for (LocaleInfo source : result) {
+ int suggestionFlags = expectedResult.getOrDefault(source.getId(), -1);
+ assertEquals(source.mSuggestionFlags, suggestionFlags);
+ }
+ }
+
+ private HashMap<String, Integer> getExpectedResult() {
+ HashMap<String, Integer> map = new HashMap<>();
+ map.put("en-US", CURRENT); // The locale current App activates.
+ map.put("fr", NONE); // The locale App and system support.
+ map.put("zu", NONE); // The locale App and system support.
+ map.put("en", NONE); // Use en because System supports en while APP supports en-CA, en-GB.
+ map.put("ko", NONE); // The locale App and system support.
+ map.put("en-AU", OTHERAPP); // The locale other App activates and current App supports.
+ map.put("en-CA", OTHERAPP); // The locale other App activates and current App supports.
+ map.put("ja-JP", OTHERAPP); // The locale other App activates and current App supports.
+ map.put("zh-Hant-TW", SIM); // The locale system activates.
+ map.put(createLocaleInfo("", SYSTEM).getId(), SYSTEM); // System language title
+ return map;
+ }
+
+ private Set<LocaleInfo> initSystemSupportedLocales() {
+ return Set.of(
+ createLocaleInfo("en", NONE),
+ createLocaleInfo("fr", NONE),
+ createLocaleInfo("zu", NONE),
+ createLocaleInfo("ko", NONE),
+ // will be filtered because current App doesn't support.
+ createLocaleInfo("es-US", SIM_CFG)
+ );
+ }
+
+ private List<LocaleInfo> initSystemCurrentLocales() {
+ return List.of(createLocaleInfo("zh-Hant-TW", SIM),
+ // will be filtered because current App activates this locale.
+ createLocaleInfo("en-US", SIM));
+ }
+
+ private Set<LocaleInfo> initAllAppActivatedLocales() {
+ return Set.of(
+ createLocaleInfo("en-CA", OTHERAPP),
+ createLocaleInfo("en-AU", OTHERAPP),
+ createLocaleInfo("ja-JP", OTHERAPP),
+ // will be filtered because current App activates this locale.
+ createLocaleInfo("en-US", OTHERAPP));
+ }
+
+ private Set<LocaleInfo> initImeLocales() {
+ return Set.of(
+ // will be filtered because system activates zh-Hant-TW.
+ createLocaleInfo("zh-TW", OTHERAPP),
+ // will be filtered because current App's activats this locale.
+ createLocaleInfo("en-US", OTHERAPP));
+ }
+
+ private HashSet<Locale> initAppSupportedLocale() {
+ HashSet<Locale> hs = new HashSet();
+ hs.add(Locale.forLanguageTag("en-US"));
+ hs.add(Locale.forLanguageTag("en-CA"));
+ hs.add(Locale.forLanguageTag("en-GB"));
+ hs.add(Locale.forLanguageTag("zh-TW"));
+ hs.add(Locale.forLanguageTag("ja"));
+ hs.add(Locale.forLanguageTag("fr"));
+ hs.add(Locale.forLanguageTag("zu"));
+ hs.add(Locale.forLanguageTag("ko"));
+ // will be filtered because it's not in the system language.
+ hs.add(Locale.forLanguageTag("mn"));
+ return hs;
+ }
+
+ private LocaleInfo createLocaleInfo(String languageTag, int suggestionFlag) {
+ LocaleInfo localeInfo = LocaleStore.fromLocale(Locale.forLanguageTag(languageTag));
+ localeInfo.mSuggestionFlags = suggestionFlag;
+ localeInfo.setTranslated(true);
+ return localeInfo;
+ }
+}
diff --git a/tests/Internal/src/com/android/internal/app/LocaleStoreTest.java b/tests/Internal/src/com/android/internal/app/LocaleStoreTest.java
new file mode 100644
index 0000000..bf6ece1
--- /dev/null
+++ b/tests/Internal/src/com/android/internal/app/LocaleStoreTest.java
@@ -0,0 +1,63 @@
+/*
+ * 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.app;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.view.inputmethod.InputMethodSubtype;
+import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.app.LocaleStore.LocaleInfo;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Unit tests for the {@link LocaleStore}.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class LocaleStoreTest {
+ @Before
+ public void setUp() {
+ }
+
+ @Test
+ public void testTransformImeLanguageTagToLocaleInfo() {
+ List<InputMethodSubtype> list = List.of(
+ new InputMethodSubtypeBuilder().setLanguageTag("en-US").build(),
+ new InputMethodSubtypeBuilder().setLanguageTag("zh-TW").build(),
+ new InputMethodSubtypeBuilder().setLanguageTag("ja-JP").build());
+
+ Set<LocaleInfo> localeSet = LocaleStore.transformImeLanguageTagToLocaleInfo(list);
+
+ Set<String> expectedLanguageTag = Set.of("en-US", "zh-TW", "ja-JP");
+ assertEquals(localeSet.size(), expectedLanguageTag.size());
+ for (LocaleInfo info : localeSet) {
+ assertEquals(info.mSuggestionFlags, LocaleInfo.SUGGESTION_TYPE_OTHER_APP_LANGUAGE);
+ assertTrue(expectedLanguageTag.contains(info.getId()));
+ }
+ }
+}
diff --git a/tests/JobSchedulerPerfTests/src/com/android/frameworks/perftests/job/JobStorePerfTests.java b/tests/JobSchedulerPerfTests/src/com/android/frameworks/perftests/job/JobStorePerfTests.java
index afaeca1..00fc498 100644
--- a/tests/JobSchedulerPerfTests/src/com/android/frameworks/perftests/job/JobStorePerfTests.java
+++ b/tests/JobSchedulerPerfTests/src/com/android/frameworks/perftests/job/JobStorePerfTests.java
@@ -208,6 +208,6 @@
.setPersisted(true)
.build();
return JobStatus.createFromJobInfo(
- jobInfo, callingUid, SOURCE_PACKAGE, SOURCE_USER_ID, testTag);
+ jobInfo, callingUid, SOURCE_PACKAGE, SOURCE_USER_ID, null, testTag);
}
}
diff --git a/tests/SurfaceViewSyncTest/src/com/android/test/SurfaceViewSyncActivity.java b/tests/SurfaceViewSyncTest/src/com/android/test/SurfaceViewSyncActivity.java
index 03f61fa4..d5983d0 100644
--- a/tests/SurfaceViewSyncTest/src/com/android/test/SurfaceViewSyncActivity.java
+++ b/tests/SurfaceViewSyncTest/src/com/android/test/SurfaceViewSyncActivity.java
@@ -89,7 +89,7 @@
mLastExpanded = !mLastExpanded;
if (mEnableSyncSwitch.isChecked()) {
- mSyncGroup = new SurfaceSyncGroup();
+ mSyncGroup = new SurfaceSyncGroup(TAG);
mSyncGroup.addToSync(container.getRootSurfaceControl());
}
diff --git a/tools/lint/README.md b/tools/lint/README.md
index 99149c1..b235ad6 100644
--- a/tools/lint/README.md
+++ b/tools/lint/README.md
@@ -1,15 +1,44 @@
-# Android Framework Lint Checker
+# Android Lint Checks for AOSP
-Custom lint checks written here are going to be executed for modules that opt in to those (e.g. any
+Custom Android Lint checks are written here to be executed against java modules
+in AOSP. These checks are broken down into two subdirectories:
+
+1. [Global Checks](#android-global-lint-checker)
+2. [Framework Checks](#android-framework-lint-checker)
+
+# [Android Global Lint Checker](/global)
+Checks written here are executed for the entire tree. The `AndroidGlobalLintChecker`
+build target produces a jar file that is included in the overall build output
+(`AndroidGlobalLintChecker.jar`). This file is then downloaded as a prebuilt under the
+`prebuilts/cmdline-tools` subproject, and included by soong with all invocations of lint.
+
+## How to add new global lint checks
+1. Write your detector with its issues and put it into
+ `global/checks/src/main/java/com/google/android/lint`.
+2. Add your detector's issues into `AndroidGlobalIssueRegistry`'s `issues`
+ field.
+3. Write unit tests for your detector in one file and put it into
+ `global/checks/test/java/com/google/android/lint`.
+4. Have your change reviewed and merged. Once your change is merged,
+ obtain a build number from a successful build that includes your change.
+5. Run `prebuilts/cmdline-tools/update-android-global-lint-checker.sh
+ <build_number>`. The script will create a commit that you can upload for
+ approval to the `prebuilts/cmdline-tools` subproject.
+6. Done! Your lint check should be applied in lint report builds across the
+ entire tree!
+
+# [Android Framework Lint Checker](/framework)
+
+Checks written here are going to be executed for modules that opt in to those (e.g. any
`services.XXX` module) and results will be automatically reported on CLs on gerrit.
-## How to add new lint checks
+## How to add new framework lint checks
1. Write your detector with its issues and put it into
- `checks/src/main/java/com/google/android/lint`.
+ `framework/checks/src/main/java/com/google/android/lint`.
2. Add your detector's issues into `AndroidFrameworkIssueRegistry`'s `issues` field.
3. Write unit tests for your detector in one file and put it into
- `checks/test/java/com/google/android/lint`.
+ `framework/checks/test/java/com/google/android/lint`.
4. Done! Your lint checks should be applied in lint report builds for modules that include
`AndroidFrameworkLintChecker`.
@@ -44,11 +73,11 @@
environment variable with the id of the lint. For example:
`ANDROID_LINT_CHECK=UnusedTokenOfOriginalCallingIdentity m out/[...]/lint-report.html`
-## How to apply automatic fixes suggested by lint
+# How to apply automatic fixes suggested by lint
See [lint_fix](fix/README.md)
-## Create or update a baseline
+# Create or update a baseline
Baseline files can be used to silence known errors (and warnings) that are deemed to be safe. When
there is a lint-baseline.xml file in the root folder of the java library, soong will
@@ -79,7 +108,7 @@
[lint.go](http://cs/aosp-master/build/soong/java/lint.go;l=451;rcl=2e778d5bc4a8d1d77b4f4a3029a4a254ad57db75)
adding `cmd.Flag("--nowarn")` and running lint again.
-## Documentation
+# Documentation
- [Android Lint Docs](https://googlesamples.github.io/android-custom-lint-rules/)
- [Lint Check Unit Testing](https://googlesamples.github.io/android-custom-lint-rules/api-guide/unit-testing.md.html)
diff --git a/tools/lint/common/src/main/java/com/google/android/lint/PermissionMethodUtils.kt b/tools/lint/common/src/main/java/com/google/android/lint/PermissionMethodUtils.kt
index 0157596..9a7f8fa 100644
--- a/tools/lint/common/src/main/java/com/google/android/lint/PermissionMethodUtils.kt
+++ b/tools/lint/common/src/main/java/com/google/android/lint/PermissionMethodUtils.kt
@@ -19,8 +19,10 @@
import com.android.tools.lint.detector.api.getUMethod
import org.jetbrains.uast.UAnnotation
import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UElement
import org.jetbrains.uast.UMethod
import org.jetbrains.uast.UParameter
+import org.jetbrains.uast.UQualifiedReferenceExpression
fun isPermissionMethodCall(callExpression: UCallExpression): Boolean {
val method = callExpression.resolve()?.getUMethod() ?: return false
@@ -36,3 +38,15 @@
fun hasPermissionNameAnnotation(parameter: UParameter) = parameter.annotations.any {
it.hasQualifiedName(ANNOTATION_PERMISSION_NAME)
}
+
+/**
+ * Attempts to return a CallExpression from a QualifiedReferenceExpression (or returns it directly if passed directly)
+ * @param callOrReferenceCall expected to be UCallExpression or UQualifiedReferenceExpression
+ * @return UCallExpression, if available
+ */
+fun findCallExpression(callOrReferenceCall: UElement?): UCallExpression? =
+ when (callOrReferenceCall) {
+ is UCallExpression -> callOrReferenceCall
+ is UQualifiedReferenceExpression -> callOrReferenceCall.selector as? UCallExpression
+ else -> null
+ }
diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt
index ee7dd62..485765b 100644
--- a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt
+++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt
@@ -20,28 +20,36 @@
import com.android.tools.lint.detector.api.LintFix
import com.android.tools.lint.detector.api.Location
import com.android.tools.lint.detector.api.UastLintUtils.Companion.getAnnotationBooleanValue
+import com.android.tools.lint.detector.api.UastLintUtils.Companion.getAnnotationStringValues
+import com.android.tools.lint.detector.api.findSelector
import com.android.tools.lint.detector.api.getUMethod
+import com.google.android.lint.findCallExpression
import com.google.android.lint.getPermissionMethodAnnotation
import com.google.android.lint.hasPermissionNameAnnotation
import com.google.android.lint.isPermissionMethodCall
+import com.intellij.psi.PsiClassType
import com.intellij.psi.PsiType
import org.jetbrains.kotlin.psi.psiUtil.parameterIndex
+import org.jetbrains.uast.UBinaryExpression
+import org.jetbrains.uast.UBlockExpression
import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UExpression
+import org.jetbrains.uast.UExpressionList
+import org.jetbrains.uast.UIfExpression
+import org.jetbrains.uast.UThrowExpression
+import org.jetbrains.uast.UastBinaryOperator
import org.jetbrains.uast.evaluateString
+import org.jetbrains.uast.skipParenthesizedExprDown
import org.jetbrains.uast.visitor.AbstractUastVisitor
/**
* Helper class that facilitates the creation of lint auto fixes
- *
- * Handles "Single" permission checks that should be migrated to @EnforcePermission(...), as well as consecutive checks
- * that should be migrated to @EnforcePermission(allOf={...})
- *
- * TODO: handle anyOf style annotations
*/
data class EnforcePermissionFix(
val locations: List<Location>,
val permissionNames: List<String>,
val errorLevel: Boolean,
+ val anyOf: Boolean,
) {
fun toLintFix(annotationLocation: Location): LintFix {
val removeFixes = this.locations.map {
@@ -67,8 +75,13 @@
get() {
val quotedPermissions = permissionNames.joinToString(", ") { """"$it"""" }
+ val attributeName =
+ if (permissionNames.size > 1) {
+ if (anyOf) "anyOf" else "allOf"
+ } else null
+
val annotationParameter =
- if (permissionNames.size > 1) "allOf={$quotedPermissions}"
+ if (attributeName != null) "$attributeName={$quotedPermissions}"
else quotedPermissions
return "@$ANNOTATION_ENFORCE_PERMISSION($annotationParameter)"
@@ -76,7 +89,7 @@
companion object {
/**
- * conditionally constructs EnforcePermissionFix from a UCallExpression
+ * Conditionally constructs EnforcePermissionFix from a UCallExpression
* @return EnforcePermissionFix if the called method is annotated with @PermissionMethod, else null
*/
fun fromCallExpression(
@@ -85,26 +98,94 @@
): EnforcePermissionFix? {
val method = callExpression.resolve()?.getUMethod() ?: return null
val annotation = getPermissionMethodAnnotation(method) ?: return null
- val enforces = method.returnType == PsiType.VOID
+ val returnsVoid = method.returnType == PsiType.VOID
val orSelf = getAnnotationBooleanValue(annotation, "orSelf") ?: false
+ val anyOf = getAnnotationBooleanValue(annotation, "anyOf") ?: false
return EnforcePermissionFix(
listOf(getPermissionCheckLocation(context, callExpression)),
getPermissionCheckValues(callExpression),
- // If we detect that the PermissionMethod enforces that permission is granted,
- // AND is of the "orSelf" variety, we are very confident that this is a behavior
- // preserving migration to @EnforcePermission. Thus, the incident should be ERROR
- // level.
- errorLevel = enforces && orSelf
+ errorLevel = isErrorLevel(throws = returnsVoid, orSelf = orSelf),
+ anyOf,
+ )
+ }
+
+ /**
+ * Conditionally constructs EnforcePermissionFix from a UCallExpression
+ * @return EnforcePermissionFix IF AND ONLY IF:
+ * * The condition of the if statement compares the return value of a
+ * PermissionMethod to one of the PackageManager.PermissionResult values
+ * * The expression inside the if statement does nothing but throw SecurityException
+ */
+ fun fromIfExpression(
+ context: JavaContext,
+ ifExpression: UIfExpression
+ ): EnforcePermissionFix? {
+ val condition = ifExpression.condition.skipParenthesizedExprDown()
+ if (condition !is UBinaryExpression) return null
+
+ val maybeLeftCall = findCallExpression(condition.leftOperand)
+ val maybeRightCall = findCallExpression(condition.rightOperand)
+
+ val (callExpression, comparison) =
+ if (maybeLeftCall is UCallExpression) {
+ Pair(maybeLeftCall, condition.rightOperand)
+ } else if (maybeRightCall is UCallExpression) {
+ Pair(maybeRightCall, condition.leftOperand)
+ } else return null
+
+ val permissionMethodAnnotation = getPermissionMethodAnnotation(
+ callExpression.resolve()?.getUMethod()) ?: return null
+
+ val equalityCheck =
+ when (comparison.findSelector().asSourceString()
+ .filterNot(Char::isWhitespace)) {
+ "PERMISSION_GRANTED" -> UastBinaryOperator.IDENTITY_NOT_EQUALS
+ "PERMISSION_DENIED" -> UastBinaryOperator.IDENTITY_EQUALS
+ else -> return null
+ }
+
+ if (condition.operator != equalityCheck) return null
+
+ val throwExpression: UThrowExpression? =
+ ifExpression.thenExpression as? UThrowExpression
+ ?: (ifExpression.thenExpression as? UBlockExpression)
+ ?.expressions?.firstOrNull()
+ as? UThrowExpression
+
+
+ val thrownClass = (throwExpression?.thrownExpression?.getExpressionType()
+ as? PsiClassType)?.resolve() ?: return null
+ if (!context.evaluator.inheritsFrom(
+ thrownClass, "java.lang.SecurityException")){
+ return null
+ }
+
+ val orSelf = getAnnotationBooleanValue(permissionMethodAnnotation, "orSelf") ?: false
+ val anyOf = getAnnotationBooleanValue(permissionMethodAnnotation, "anyOf") ?: false
+
+ return EnforcePermissionFix(
+ listOf(context.getLocation(ifExpression)),
+ getPermissionCheckValues(callExpression),
+ errorLevel = isErrorLevel(throws = true, orSelf = orSelf),
+ anyOf = anyOf
)
}
- fun compose(individuals: List<EnforcePermissionFix>): EnforcePermissionFix =
- EnforcePermissionFix(
- individuals.flatMap { it.locations },
- individuals.flatMap { it.permissionNames },
- errorLevel = individuals.all(EnforcePermissionFix::errorLevel)
+ fun compose(individuals: List<EnforcePermissionFix>): EnforcePermissionFix {
+ val anyOfs = individuals.filter(EnforcePermissionFix::anyOf)
+ // anyOf/allOf should be consistent. If we encounter some @PermissionMethods that are anyOf
+ // and others that aren't, we don't know what to do.
+ if (anyOfs.isNotEmpty() && anyOfs.size < individuals.size) {
+ throw AnyOfAllOfException()
+ }
+ return EnforcePermissionFix(
+ individuals.flatMap(EnforcePermissionFix::locations),
+ individuals.flatMap(EnforcePermissionFix::permissionNames),
+ errorLevel = individuals.all(EnforcePermissionFix::errorLevel),
+ anyOf = anyOfs.isNotEmpty()
)
+ }
/**
* Given a permission check, get its proper location
@@ -130,6 +211,7 @@
* and pull out the permission value(s) being used. Also evaluates nested calls
* to @PermissionMethod(s) in the given method's body.
*/
+ @Throws(AnyOfAllOfException::class)
private fun getPermissionCheckValues(
callExpression: UCallExpression
): List<String> {
@@ -139,38 +221,110 @@
val visitedCalls = mutableSetOf<UCallExpression>() // don't visit the same call twice
val bfsQueue = ArrayDeque(listOf(callExpression))
- // Breadth First Search - evalutaing nested @PermissionMethod(s) in the available
+ var anyOfAllOfState: AnyOfAllOfState = AnyOfAllOfState.INITIAL
+
+ // Bread First Search - evaluating nested @PermissionMethod(s) in the available
// source code for @PermissionName(s).
while (bfsQueue.isNotEmpty()) {
- val current = bfsQueue.removeFirst()
- visitedCalls.add(current)
- result.addAll(findPermissions(current))
+ val currentCallExpression = bfsQueue.removeFirst()
+ visitedCalls.add(currentCallExpression)
+ val currentPermissions = findPermissions(currentCallExpression)
+ result.addAll(currentPermissions)
- current.resolve()?.getUMethod()?.accept(object : AbstractUastVisitor() {
- override fun visitCallExpression(node: UCallExpression): Boolean {
- if (isPermissionMethodCall(node) && node !in visitedCalls) {
- bfsQueue.add(node)
- }
- return false
- }
- })
+ val currentAnnotation = getPermissionMethodAnnotation(
+ currentCallExpression.resolve()?.getUMethod())
+ val currentAnyOf = getAnnotationBooleanValue(currentAnnotation, "anyOf") ?: false
+
+ // anyOf/allOf should be consistent. If we encounter a nesting of @PermissionMethods
+ // where we start in an anyOf state and switch to allOf, or vice versa,
+ // we don't know what to do.
+ if (anyOfAllOfState == AnyOfAllOfState.INITIAL) {
+ if (currentAnyOf) anyOfAllOfState = AnyOfAllOfState.ANY_OF
+ else if (result.isNotEmpty()) anyOfAllOfState = AnyOfAllOfState.ALL_OF
+ }
+
+ if (anyOfAllOfState == AnyOfAllOfState.ALL_OF && currentAnyOf) {
+ throw AnyOfAllOfException()
+ }
+
+ if (anyOfAllOfState == AnyOfAllOfState.ANY_OF &&
+ !currentAnyOf && currentPermissions.size > 1) {
+ throw AnyOfAllOfException()
+ }
+
+ currentCallExpression.resolve()?.getUMethod()
+ ?.accept(PermissionCheckValuesVisitor(visitedCalls, bfsQueue))
}
return result.toList()
}
+ private enum class AnyOfAllOfState {
+ INITIAL,
+ ANY_OF,
+ ALL_OF
+ }
+
+ /**
+ * Adds visited permission method calls to the provided
+ * queue in support of the BFS traversal happening while
+ * this is used
+ */
+ private class PermissionCheckValuesVisitor(
+ val visitedCalls: Set<UCallExpression>,
+ val bfsQueue: ArrayDeque<UCallExpression>
+ ) : AbstractUastVisitor() {
+ override fun visitCallExpression(node: UCallExpression): Boolean {
+ if (isPermissionMethodCall(node) && node !in visitedCalls) {
+ bfsQueue.add(node)
+ }
+ return false
+ }
+ }
+
private fun findPermissions(
callExpression: UCallExpression,
): List<String> {
- val indices = callExpression.resolve()?.getUMethod()
- ?.uastParameters
- ?.filter(::hasPermissionNameAnnotation)
- ?.mapNotNull { it.sourcePsi?.parameterIndex() }
- ?: emptyList()
+ val annotation = getPermissionMethodAnnotation(callExpression.resolve()?.getUMethod())
- return indices.mapNotNull {
- callExpression.getArgumentForParameter(it)?.evaluateString()
- }
+ val hardCodedPermissions = (getAnnotationStringValues(annotation, "value")
+ ?: emptyArray())
+ .toList()
+
+ val indices = callExpression.resolve()?.getUMethod()
+ ?.uastParameters
+ ?.filter(::hasPermissionNameAnnotation)
+ ?.mapNotNull { it.sourcePsi?.parameterIndex() }
+ ?: emptyList()
+
+ val argPermissions = indices
+ .flatMap { i ->
+ when (val argument = callExpression.getArgumentForParameter(i)) {
+ null -> listOf(null)
+ is UExpressionList -> // varargs e.g. someMethod(String...)
+ argument.expressions.map(UExpression::evaluateString)
+ else -> listOf(argument.evaluateString())
+ }
+ }
+ .filterNotNull()
+
+ return hardCodedPermissions + argPermissions
}
+
+ /**
+ * If we detect that the PermissionMethod enforces that permission is granted,
+ * AND is of the "orSelf" variety, we are very confident that this is a behavior
+ * preserving migration to @EnforcePermission. Thus, the incident should be ERROR
+ * level.
+ */
+ private fun isErrorLevel(throws: Boolean, orSelf: Boolean): Boolean = throws && orSelf
}
}
+/**
+ * anyOf/allOf @PermissionMethods must be consistent to apply @EnforcePermission -
+ * meaning if we encounter some @PermissionMethods that are anyOf, and others are allOf,
+ * we don't know which to apply.
+ */
+class AnyOfAllOfException : Exception() {
+ override val message: String = "anyOf/allOf permission methods cannot be mixed"
+}
diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetector.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetector.kt
index 9999a0b..11a283a 100644
--- a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetector.kt
+++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetector.kt
@@ -23,12 +23,11 @@
import com.android.tools.lint.detector.api.JavaContext
import com.android.tools.lint.detector.api.Scope
import com.android.tools.lint.detector.api.Severity
+import com.google.android.lint.findCallExpression
import org.jetbrains.uast.UBlockExpression
-import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UElement
import org.jetbrains.uast.UIfExpression
import org.jetbrains.uast.UMethod
-import org.jetbrains.uast.UQualifiedReferenceExpression
import org.jetbrains.uast.skipParenthesizedExprDown
/**
@@ -80,40 +79,41 @@
* as some other business logic is happening that prevents an automated fix.
*/
private fun accumulateSimplePermissionCheckFixes(
- methodBody: UBlockExpression,
- context: JavaContext
- ):
- EnforcePermissionFix? {
- val singleFixes = mutableListOf<EnforcePermissionFix>()
- for (expression in methodBody.expressions) {
- singleFixes.add(getPermissionCheckFix(expression.skipParenthesizedExprDown(), context)
- ?: break)
- }
- return when (singleFixes.size) {
- 0 -> null
- 1 -> singleFixes[0]
- else -> EnforcePermissionFix.compose(singleFixes)
+ methodBody: UBlockExpression,
+ context: JavaContext
+ ): EnforcePermissionFix? {
+ try {
+ val singleFixes = mutableListOf<EnforcePermissionFix>()
+ for (expression in methodBody.expressions) {
+ val fix = getPermissionCheckFix(
+ expression.skipParenthesizedExprDown(),
+ context) ?: break
+ singleFixes.add(fix)
+ }
+ return when (singleFixes.size) {
+ 0 -> null
+ 1 -> singleFixes[0]
+ else -> EnforcePermissionFix.compose(singleFixes)
+ }
+ } catch (e: AnyOfAllOfException) {
+ return null
}
}
+
/**
* If an expression boils down to a permission check, return
* the helper for creating a lint auto fix to @EnforcePermission
*/
private fun getPermissionCheckFix(startingExpression: UElement?, context: JavaContext):
EnforcePermissionFix? {
- return when (startingExpression) {
- is UQualifiedReferenceExpression -> getPermissionCheckFix(
- startingExpression.selector, context
- )
-
- is UIfExpression -> getPermissionCheckFix(startingExpression.condition, context)
-
- is UCallExpression -> return EnforcePermissionFix
- .fromCallExpression(context, startingExpression)
-
- else -> null
+ if (startingExpression is UIfExpression) {
+ return EnforcePermissionFix.fromIfExpression(context, startingExpression)
}
+ findCallExpression(startingExpression)?.let {
+ return EnforcePermissionFix.fromCallExpression(context, it)
+ }
+ return null
}
companion object {
diff --git a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetectorTest.kt b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetectorTest.kt
index bdf9c89..2ac550b 100644
--- a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetectorTest.kt
+++ b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetectorTest.kt
@@ -564,7 +564,228 @@
)
}
+ fun testIfExpression() {
+ lint().files(
+ java(
+ """
+ import android.content.Context;
+ import android.test.ITest;
+ public class Foo extends ITest.Stub {
+ private Context mContext;
+ @Override
+ public void test() throws android.os.RemoteException {
+ if (mContext.checkCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo")
+ != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("yikes!");
+ }
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expect(
+ """
+ src/Foo.java:7: Error: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement]
+ if (mContext.checkCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo")
+ ^
+ 1 errors, 0 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+ Fix for src/Foo.java line 7: Annotate with @EnforcePermission:
+ @@ -5 +5
+ + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS")
+ @@ -7 +8
+ - if (mContext.checkCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo")
+ - != PackageManager.PERMISSION_GRANTED) {
+ - throw new SecurityException("yikes!");
+ - }
+ """
+ )
+ }
+ fun testIfExpression_orSelfFalse_warning() {
+ lint().files(
+ java(
+ """
+ import android.content.Context;
+ import android.test.ITest;
+ public class Foo extends ITest.Stub {
+ private Context mContext;
+ @Override
+ public void test() throws android.os.RemoteException {
+ if (mContext.checkCallingPermission("android.permission.READ_CONTACTS", "foo")
+ != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("yikes!");
+ }
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expect(
+ """
+ src/Foo.java:7: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement]
+ if (mContext.checkCallingPermission("android.permission.READ_CONTACTS", "foo")
+ ^
+ 0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+ Fix for src/Foo.java line 7: Annotate with @EnforcePermission:
+ @@ -5 +5
+ + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS")
+ @@ -7 +8
+ - if (mContext.checkCallingPermission("android.permission.READ_CONTACTS", "foo")
+ - != PackageManager.PERMISSION_GRANTED) {
+ - throw new SecurityException("yikes!");
+ - }
+ """
+ )
+ }
+
+ fun testIfExpression_otherSideEffect_ignored() {
+ lint().files(
+ java(
+ """
+ import android.content.Context;
+ import android.test.ITest;
+ public class Foo extends ITest.Stub {
+ private Context mContext;
+ @Override
+ public void test() throws android.os.RemoteException {
+ if (mContext.checkCallingPermission("android.permission.READ_CONTACTS", "foo")
+ != PackageManager.PERMISSION_GRANTED) {
+ doSomethingElse();
+ throw new SecurityException("yikes!");
+ }
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expectClean()
+ }
+
+ fun testAnyOf_hardCodedAndVarArgs() {
+ lint().files(
+ java(
+ """
+ import android.content.Context;
+ import android.test.ITest;
+
+ public class Foo extends ITest.Stub {
+ private Context mContext;
+
+ @android.content.pm.PermissionMethod(anyOf = true)
+ private void helperHelper() {
+ helper("FOO", "BAR");
+ }
+
+ @android.content.pm.PermissionMethod(anyOf = true, value = {"BAZ", "BUZZ"})
+ private void helper(@android.content.pm.PermissionName String... extraPermissions) {}
+
+ @Override
+ public void test() throws android.os.RemoteException {
+ helperHelper();
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expect(
+ """
+ src/Foo.java:17: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement]
+ helperHelper();
+ ~~~~~~~~~~~~~~~
+ 0 errors, 1 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+ Fix for src/Foo.java line 17: Annotate with @EnforcePermission:
+ @@ -15 +15
+ + @android.annotation.EnforcePermission(anyOf={"BAZ", "BUZZ", "FOO", "BAR"})
+ @@ -17 +18
+ - helperHelper();
+ """
+ )
+ }
+
+
+ fun testAnyOfAllOf_mixedConsecutiveCalls_ignored() {
+ lint().files(
+ java(
+ """
+ import android.content.Context;
+ import android.test.ITest;
+
+ public class Foo extends ITest.Stub {
+ private Context mContext;
+
+ @android.content.pm.PermissionMethod
+ private void allOfhelper() {
+ mContext.enforceCallingOrSelfPermission("FOO");
+ mContext.enforceCallingOrSelfPermission("BAR");
+ }
+
+ @android.content.pm.PermissionMethod(anyOf = true, permissions = {"BAZ", "BUZZ"})
+ private void anyOfHelper() {}
+
+ @Override
+ public void test() throws android.os.RemoteException {
+ allOfhelper();
+ anyOfHelper();
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expectClean()
+ }
+
+ fun testAnyOfAllOf_mixedNestedCalls_ignored() {
+ lint().files(
+ java(
+ """
+ import android.content.Context;
+ import android.content.pm.PermissionName;import android.test.ITest;
+
+ public class Foo extends ITest.Stub {
+ private Context mContext;
+
+ @android.content.pm.PermissionMethod(anyOf = true)
+ private void anyOfCheck(@PermissionName String... permissions) {
+ allOfCheck("BAZ", "BUZZ");
+ }
+
+ @android.content.pm.PermissionMethod
+ private void allOfCheck(@PermissionName String... permissions) {}
+
+ @Override
+ public void test() throws android.os.RemoteException {
+ anyOfCheck("FOO", "BAR");
+ }
+ }
+ """
+ ).indented(),
+ *stubs
+ )
+ .run()
+ .expectClean()
+ }
companion object {
val stubs = arrayOf(
diff --git a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/Stubs.kt b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/Stubs.kt
index 5ac8a0b..362ac61 100644
--- a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/Stubs.kt
+++ b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/Stubs.kt
@@ -23,6 +23,8 @@
public void enforceCallingPermission(@android.content.pm.PermissionName String permission, String message) {}
@android.content.pm.PermissionMethod(orSelf = true)
public int checkCallingOrSelfPermission(@android.content.pm.PermissionName String permission, String message) {}
+ @android.content.pm.PermissionMethod
+ public int checkCallingPermission(@android.content.pm.PermissionName String permission, String message) {}
}
"""
).indented()
diff --git a/wifi/java/src/android/net/wifi/WifiKeystore.java b/wifi/java/src/android/net/wifi/WifiKeystore.java
new file mode 100644
index 0000000..ca86dde
--- /dev/null
+++ b/wifi/java/src/android/net/wifi/WifiKeystore.java
@@ -0,0 +1,126 @@
+/*
+ * 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.net.wifi;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Process;
+import android.os.ServiceManager;
+import android.os.ServiceSpecificException;
+import android.security.legacykeystore.ILegacyKeystore;
+import android.util.Log;
+
+/**
+ * @hide This class allows wifi framework to store and access wifi certificate blobs.
+ */
+@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+public final class WifiKeystore {
+ private static final String TAG = "WifiKeystore";
+ private static final String LEGACY_KEYSTORE_SERVICE_NAME = "android.security.legacykeystore";
+
+ private static ILegacyKeystore getService() {
+ return ILegacyKeystore.Stub.asInterface(
+ ServiceManager.checkService(LEGACY_KEYSTORE_SERVICE_NAME));
+ }
+
+ /** @hide */
+ WifiKeystore() {
+ }
+
+ /**
+ * Stores the blob under the alias in the keystore database. Existing blobs by the
+ * same name will be replaced.
+ * @param alias The name of the blob
+ * @param blob The blob.
+ * @return true if the blob was successfully added. False otherwise.
+ * @hide
+ */
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ public static boolean put(@NonNull String alias, @NonNull byte[] blob) {
+ try {
+ Log.i(TAG, "put blob. alias " + alias);
+ getService().put(alias, Process.WIFI_UID, blob);
+ return true;
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to put blob.", e);
+ return false;
+ }
+ }
+
+ /**
+ * Retrieves a blob by the name alias from the blob database.
+ * @param alias Name of the blob to retrieve.
+ * @return The unstructured blob, that is the blob that was stored using
+ * {@link android.net.wifi.WifiKeystore#put}.
+ * Returns null if no blob was found.
+ * @hide
+ */
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ public static @NonNull byte[] get(@NonNull String alias) {
+ try {
+ Log.i(TAG, "get blob. alias " + alias);
+ return getService().get(alias, Process.WIFI_UID);
+ } catch (ServiceSpecificException e) {
+ if (e.errorCode != ILegacyKeystore.ERROR_ENTRY_NOT_FOUND) {
+ Log.e(TAG, "Failed to get blob.", e);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to get blob.", e);
+ }
+ return null;
+ }
+
+ /**
+ * Removes a blob by the name alias from the database.
+ * @param alias Name of the blob to be removed.
+ * @return True if a blob was removed. False if no such blob was found.
+ * @hide
+ */
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ public static boolean remove(@NonNull String alias) {
+ try {
+ getService().remove(alias, Process.WIFI_UID);
+ return true;
+ } catch (ServiceSpecificException e) {
+ if (e.errorCode != ILegacyKeystore.ERROR_ENTRY_NOT_FOUND) {
+ Log.e(TAG, "Failed to remove blob.", e);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to remove blob.", e);
+ }
+ return false;
+ }
+
+ /**
+ * Lists the blobs stored in the database.
+ * @return An array of strings representing the aliases stored in the database.
+ * The return value may be empty but never null.
+ * @hide
+ */
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ public static @NonNull String[] list(@NonNull String prefix) {
+ try {
+ final String[] aliases = getService().list(prefix, Process.WIFI_UID);
+ for (int i = 0; i < aliases.length; ++i) {
+ aliases[i] = aliases[i].substring(prefix.length());
+ }
+ return aliases;
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to list blobs.", e);
+ }
+ return new String[0];
+ }
+}
\ No newline at end of file