Merge "Move fully drawn metric log out of wm lock"
diff --git a/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl b/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl
index d281da0..a3390b7 100644
--- a/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl
+++ b/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl
@@ -30,6 +30,25 @@
*/
interface IJobCallback {
/**
+ * Immediate callback to the system after sending a data transfer download progress request
+ * signal; used to quickly detect ANR.
+ *
+ * @param jobId Unique integer used to identify this job.
+ * @param workId Unique integer used to identify a specific work item.
+ * @param transferredBytes How much data has been downloaded, in bytes.
+ */
+ void acknowledgeGetTransferredDownloadBytesMessage(int jobId, int workId,
+ long transferredBytes);
+ /**
+ * Immediate callback to the system after sending a data transfer upload progress request
+ * signal; used to quickly detect ANR.
+ *
+ * @param jobId Unique integer used to identify this job.
+ * @param workId Unique integer used to identify a specific work item.
+ * @param transferredBytes How much data has been uploaded, in bytes.
+ */
+ void acknowledgeGetTransferredUploadBytesMessage(int jobId, int workId, long transferredBytes);
+ /**
* Immediate callback to the system after sending a start signal, used to quickly detect ANR.
*
* @param jobId Unique integer used to identify this job.
@@ -65,4 +84,24 @@
*/
@UnsupportedAppUsage
void jobFinished(int jobId, boolean reschedule);
+ /*
+ * Inform JobScheduler of a change in the estimated transfer payload.
+ *
+ * @param jobId Unique integer used to identify this job.
+ * @param item The particular JobWorkItem this progress is associated with, if any.
+ * @param downloadBytes How many bytes the app expects to download.
+ * @param uploadBytes How many bytes the app expects to upload.
+ */
+ void updateEstimatedNetworkBytes(int jobId, in JobWorkItem item,
+ long downloadBytes, long uploadBytes);
+ /*
+ * Update JobScheduler of how much data the job has successfully transferred.
+ *
+ * @param jobId Unique integer used to identify this job.
+ * @param item The particular JobWorkItem this progress is associated with, if any.
+ * @param transferredDownloadBytes The number of bytes that have successfully been downloaded.
+ * @param transferredUploadBytes The number of bytes that have successfully been uploaded.
+ */
+ void updateTransferredNetworkBytes(int jobId, in JobWorkItem item,
+ long transferredDownloadBytes, long transferredUploadBytes);
}
diff --git a/apex/jobscheduler/framework/java/android/app/job/IJobService.aidl b/apex/jobscheduler/framework/java/android/app/job/IJobService.aidl
index 22ad252..2bb82bd 100644
--- a/apex/jobscheduler/framework/java/android/app/job/IJobService.aidl
+++ b/apex/jobscheduler/framework/java/android/app/job/IJobService.aidl
@@ -17,6 +17,7 @@
package android.app.job;
import android.app.job.JobParameters;
+import android.app.job.JobWorkItem;
/**
* Interface that the framework uses to communicate with application code that implements a
@@ -31,4 +32,8 @@
/** Stop execution of application's job. */
@UnsupportedAppUsage
void stopJob(in JobParameters jobParams);
+ /** Update JS of how much data has been downloaded. */
+ void getTransferredDownloadBytes(in JobParameters jobParams, in JobWorkItem jobWorkItem);
+ /** Update JS of how much data has been uploaded. */
+ void getTransferredUploadBytes(in JobParameters jobParams, in JobWorkItem jobWorkItem);
}
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
index dfdb290..7448686 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
@@ -22,8 +22,11 @@
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.annotation.SystemService;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledAfter;
import android.content.ClipData;
import android.content.Context;
+import android.os.Build;
import android.os.Bundle;
import android.os.PersistableBundle;
@@ -93,6 +96,16 @@
*/
@SystemService(Context.JOB_SCHEDULER_SERVICE)
public abstract class JobScheduler {
+ /**
+ * Whether to throw an exception when an app doesn't properly implement all the necessary
+ * data transfer APIs.
+ *
+ * @hide
+ */
+ @ChangeId
+ @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.TIRAMISU)
+ public static final long THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION = 255371817L;
+
/** @hide */
@IntDef(prefix = { "RESULT_" }, value = {
RESULT_FAILURE,
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobService.java b/apex/jobscheduler/framework/java/android/app/job/JobService.java
index d184d44..dabf728 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobService.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobService.java
@@ -16,7 +16,13 @@
package android.app.job;
+import static android.app.job.JobScheduler.THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION;
+
+import android.annotation.BytesLong;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.app.Service;
+import android.compat.Compatibility;
import android.content.Intent;
import android.os.IBinder;
@@ -72,6 +78,28 @@
public boolean onStopJob(JobParameters params) {
return JobService.this.onStopJob(params);
}
+
+ @Override
+ @BytesLong
+ public long getTransferredDownloadBytes(@NonNull JobParameters params,
+ @Nullable JobWorkItem item) {
+ if (item == null) {
+ return JobService.this.getTransferredDownloadBytes();
+ } else {
+ return JobService.this.getTransferredDownloadBytes(item);
+ }
+ }
+
+ @Override
+ @BytesLong
+ public long getTransferredUploadBytes(@NonNull JobParameters params,
+ @Nullable JobWorkItem item) {
+ if (item == null) {
+ return JobService.this.getTransferredUploadBytes();
+ } else {
+ return JobService.this.getTransferredUploadBytes(item);
+ }
+ }
};
}
return mEngine.getBinder();
@@ -171,4 +199,161 @@
* to end the job entirely. Regardless of the value returned, your job must stop executing.
*/
public abstract boolean onStopJob(JobParameters params);
+
+ /**
+ * Update how much data this job will transfer. This method can
+ * be called multiple times within the first 30 seconds after
+ * {@link #onStartJob(JobParameters)} has been called. Only
+ * one call will be heeded after that time has passed.
+ *
+ * This method (or an overload) must be called within the first
+ * 30 seconds for a data transfer job if a payload size estimate
+ * was not provided at the time of scheduling.
+ *
+ * @see JobInfo.Builder#setEstimatedNetworkBytes(long, long)
+ */
+ public final void updateEstimatedNetworkBytes(@NonNull JobParameters params,
+ @BytesLong long downloadBytes, @BytesLong long uploadBytes) {
+ mEngine.updateEstimatedNetworkBytes(params, null, downloadBytes, uploadBytes);
+ }
+
+ /**
+ * Update how much data will transfer for the JobWorkItem. This
+ * method can be called multiple times within the first 30 seconds
+ * after {@link #onStartJob(JobParameters)} has been called.
+ * Only one call will be heeded after that time has passed.
+ *
+ * This method (or an overload) must be called within the first
+ * 30 seconds for a data transfer job if a payload size estimate
+ * was not provided at the time of scheduling.
+ *
+ * @see JobInfo.Builder#setEstimatedNetworkBytes(long, long)
+ */
+ public final void updateEstimatedNetworkBytes(@NonNull JobParameters params,
+ @NonNull JobWorkItem jobWorkItem,
+ @BytesLong long downloadBytes, @BytesLong long uploadBytes) {
+ mEngine.updateEstimatedNetworkBytes(params, jobWorkItem, downloadBytes, uploadBytes);
+ }
+
+ /**
+ * Tell JobScheduler how much data has successfully been transferred for the data transfer job.
+ */
+ public final void updateTransferredNetworkBytes(@NonNull JobParameters params,
+ @BytesLong long transferredDownloadBytes, @BytesLong long transferredUploadBytes) {
+ mEngine.updateTransferredNetworkBytes(params, null,
+ transferredDownloadBytes, transferredUploadBytes);
+ }
+
+ /**
+ * Tell JobScheduler how much data has been transferred for the data transfer
+ * {@link JobWorkItem}.
+ */
+ public final void updateTransferredNetworkBytes(@NonNull JobParameters params,
+ @NonNull JobWorkItem item,
+ @BytesLong long transferredDownloadBytes, @BytesLong long transferredUploadBytes) {
+ mEngine.updateTransferredNetworkBytes(params, item,
+ transferredDownloadBytes, transferredUploadBytes);
+ }
+
+ /**
+ * Get the number of bytes the app has successfully downloaded for this job. JobScheduler
+ * will call this if the job has specified positive estimated download bytes and
+ * {@link #updateTransferredNetworkBytes(JobParameters, long, long)}
+ * hasn't been called recently.
+ *
+ * <p>
+ * This must be implemented for all data transfer jobs.
+ *
+ * @see JobInfo.Builder#setEstimatedNetworkBytes(long, long)
+ * @see JobInfo#NETWORK_BYTES_UNKNOWN
+ */
+ // TODO(255371817): specify the actual time JS will wait for progress before requesting
+ @BytesLong
+ public long getTransferredDownloadBytes() {
+ if (Compatibility.isChangeEnabled(THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION)) {
+ // Regular jobs don't have to implement this and JobScheduler won't call this API for
+ // non-data transfer jobs.
+ throw new RuntimeException("Not implemented. Must override in a subclass.");
+ }
+ return 0;
+ }
+
+ /**
+ * Get the number of bytes the app has successfully downloaded for this job. JobScheduler
+ * will call this if the job has specified positive estimated upload bytes and
+ * {@link #updateTransferredNetworkBytes(JobParameters, long, long)}
+ * hasn't been called recently.
+ *
+ * <p>
+ * This must be implemented for all data transfer jobs.
+ *
+ * @see JobInfo.Builder#setEstimatedNetworkBytes(long, long)
+ * @see JobInfo#NETWORK_BYTES_UNKNOWN
+ */
+ // TODO(255371817): specify the actual time JS will wait for progress before requesting
+ @BytesLong
+ public long getTransferredUploadBytes() {
+ if (Compatibility.isChangeEnabled(THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION)) {
+ // Regular jobs don't have to implement this and JobScheduler won't call this API for
+ // non-data transfer jobs.
+ throw new RuntimeException("Not implemented. Must override in a subclass.");
+ }
+ return 0;
+ }
+
+ /**
+ * Get the number of bytes the app has successfully downloaded for this job. JobScheduler
+ * will call this if the job has specified positive estimated download bytes and
+ * {@link #updateTransferredNetworkBytes(JobParameters, JobWorkItem, long, long)}
+ * hasn't been called recently and the job has
+ * {@link JobWorkItem JobWorkItems} that have been
+ * {@link JobParameters#dequeueWork dequeued} but not
+ * {@link JobParameters#completeWork(JobWorkItem) completed}.
+ *
+ * <p>
+ * This must be implemented for all data transfer jobs.
+ *
+ * @see JobInfo#NETWORK_BYTES_UNKNOWN
+ */
+ // TODO(255371817): specify the actual time JS will wait for progress before requesting
+ @BytesLong
+ public long getTransferredDownloadBytes(@NonNull JobWorkItem item) {
+ if (item == null) {
+ return getTransferredDownloadBytes();
+ }
+ if (Compatibility.isChangeEnabled(THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION)) {
+ // Regular jobs don't have to implement this and JobScheduler won't call this API for
+ // non-data transfer jobs.
+ throw new RuntimeException("Not implemented. Must override in a subclass.");
+ }
+ return 0;
+ }
+
+ /**
+ * Get the number of bytes the app has successfully downloaded for this job. JobScheduler
+ * will call this if the job has specified positive estimated upload bytes and
+ * {@link #updateTransferredNetworkBytes(JobParameters, JobWorkItem, long, long)}
+ * hasn't been called recently and the job has
+ * {@link JobWorkItem JobWorkItems} that have been
+ * {@link JobParameters#dequeueWork dequeued} but not
+ * {@link JobParameters#completeWork(JobWorkItem) completed}.
+ *
+ * <p>
+ * This must be implemented for all data transfer jobs.
+ *
+ * @see JobInfo#NETWORK_BYTES_UNKNOWN
+ */
+ // TODO(255371817): specify the actual time JS will wait for progress before requesting
+ @BytesLong
+ public long getTransferredUploadBytes(@NonNull JobWorkItem item) {
+ if (item == null) {
+ return getTransferredUploadBytes();
+ }
+ if (Compatibility.isChangeEnabled(THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION)) {
+ // Regular jobs don't have to implement this and JobScheduler won't call this API for
+ // non-data transfer jobs.
+ throw new RuntimeException("Not implemented. Must override in a subclass.");
+ }
+ return 0;
+ }
}
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java b/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java
index 3d43d20..6c4b686 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java
@@ -16,7 +16,13 @@
package android.app.job;
+import static android.app.job.JobScheduler.THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION;
+
+import android.annotation.BytesLong;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.app.Service;
+import android.compat.Compatibility;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
@@ -25,6 +31,8 @@
import android.os.RemoteException;
import android.util.Log;
+import com.android.internal.os.SomeArgs;
+
import java.lang.ref.WeakReference;
/**
@@ -51,6 +59,20 @@
* Message that the client has completed execution of this job.
*/
private static final int MSG_JOB_FINISHED = 2;
+ /**
+ * Message that will result in a call to
+ * {@link #getTransferredDownloadBytes(JobParameters, JobWorkItem)}.
+ */
+ private static final int MSG_GET_TRANSFERRED_DOWNLOAD_BYTES = 3;
+ /**
+ * Message that will result in a call to
+ * {@link #getTransferredUploadBytes(JobParameters, JobWorkItem)}.
+ */
+ private static final int MSG_GET_TRANSFERRED_UPLOAD_BYTES = 4;
+ /** Message that the client wants to update JobScheduler of the data transfer progress. */
+ private static final int MSG_UPDATE_TRANSFERRED_NETWORK_BYTES = 5;
+ /** Message that the client wants to update JobScheduler of the estimated transfer size. */
+ private static final int MSG_UPDATE_ESTIMATED_NETWORK_BYTES = 6;
private final IJobService mBinder;
@@ -68,6 +90,32 @@
}
@Override
+ public void getTransferredDownloadBytes(@NonNull JobParameters jobParams,
+ @Nullable JobWorkItem jobWorkItem) throws RemoteException {
+ JobServiceEngine service = mService.get();
+ if (service != null) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = jobParams;
+ args.arg2 = jobWorkItem;
+ service.mHandler.obtainMessage(MSG_GET_TRANSFERRED_DOWNLOAD_BYTES, args)
+ .sendToTarget();
+ }
+ }
+
+ @Override
+ public void getTransferredUploadBytes(@NonNull JobParameters jobParams,
+ @Nullable JobWorkItem jobWorkItem) throws RemoteException {
+ JobServiceEngine service = mService.get();
+ if (service != null) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = jobParams;
+ args.arg2 = jobWorkItem;
+ service.mHandler.obtainMessage(MSG_GET_TRANSFERRED_UPLOAD_BYTES, args)
+ .sendToTarget();
+ }
+ }
+
+ @Override
public void startJob(JobParameters jobParams) throws RemoteException {
JobServiceEngine service = mService.get();
if (service != null) {
@@ -98,9 +146,9 @@
@Override
public void handleMessage(Message msg) {
- final JobParameters params = (JobParameters) msg.obj;
switch (msg.what) {
- case MSG_EXECUTE_JOB:
+ case MSG_EXECUTE_JOB: {
+ final JobParameters params = (JobParameters) msg.obj;
try {
boolean workOngoing = JobServiceEngine.this.onStartJob(params);
ackStartMessage(params, workOngoing);
@@ -109,7 +157,9 @@
throw new RuntimeException(e);
}
break;
- case MSG_STOP_JOB:
+ }
+ case MSG_STOP_JOB: {
+ final JobParameters params = (JobParameters) msg.obj;
try {
boolean ret = JobServiceEngine.this.onStopJob(params);
ackStopMessage(params, ret);
@@ -118,7 +168,9 @@
throw new RuntimeException(e);
}
break;
- case MSG_JOB_FINISHED:
+ }
+ case MSG_JOB_FINISHED: {
+ final JobParameters params = (JobParameters) msg.obj;
final boolean needsReschedule = (msg.arg2 == 1);
IJobCallback callback = params.getCallback();
if (callback != null) {
@@ -132,19 +184,117 @@
Log.e(TAG, "finishJob() called for a nonexistent job id.");
}
break;
+ }
+ case MSG_GET_TRANSFERRED_DOWNLOAD_BYTES: {
+ final SomeArgs args = (SomeArgs) msg.obj;
+ final JobParameters params = (JobParameters) args.arg1;
+ final JobWorkItem item = (JobWorkItem) args.arg2;
+ try {
+ long ret = JobServiceEngine.this.getTransferredDownloadBytes(params, item);
+ ackGetTransferredDownloadBytesMessage(params, item, ret);
+ } catch (Exception e) {
+ Log.e(TAG, "Application unable to handle getTransferredDownloadBytes.", e);
+ throw new RuntimeException(e);
+ }
+ args.recycle();
+ break;
+ }
+ case MSG_GET_TRANSFERRED_UPLOAD_BYTES: {
+ final SomeArgs args = (SomeArgs) msg.obj;
+ final JobParameters params = (JobParameters) args.arg1;
+ final JobWorkItem item = (JobWorkItem) args.arg2;
+ try {
+ long ret = JobServiceEngine.this.getTransferredUploadBytes(params, item);
+ ackGetTransferredUploadBytesMessage(params, item, ret);
+ } catch (Exception e) {
+ Log.e(TAG, "Application unable to handle getTransferredUploadBytes.", e);
+ throw new RuntimeException(e);
+ }
+ args.recycle();
+ break;
+ }
+ case MSG_UPDATE_TRANSFERRED_NETWORK_BYTES: {
+ final SomeArgs args = (SomeArgs) msg.obj;
+ final JobParameters params = (JobParameters) args.arg1;
+ IJobCallback callback = params.getCallback();
+ if (callback != null) {
+ try {
+ callback.updateTransferredNetworkBytes(params.getJobId(),
+ (JobWorkItem) args.arg2, args.argl1, args.argl2);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error updating data transfer progress to system:"
+ + " binder has gone away.");
+ }
+ } else {
+ Log.e(TAG, "updateDataTransferProgress() called for a nonexistent job id.");
+ }
+ args.recycle();
+ break;
+ }
+ case MSG_UPDATE_ESTIMATED_NETWORK_BYTES: {
+ final SomeArgs args = (SomeArgs) msg.obj;
+ final JobParameters params = (JobParameters) args.arg1;
+ IJobCallback callback = params.getCallback();
+ if (callback != null) {
+ try {
+ callback.updateEstimatedNetworkBytes(params.getJobId(),
+ (JobWorkItem) args.arg2, args.argl1, args.argl2);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error updating estimated transfer size to system:"
+ + " binder has gone away.");
+ }
+ } else {
+ Log.e(TAG,
+ "updateEstimatedNetworkBytes() called for a nonexistent job id.");
+ }
+ args.recycle();
+ break;
+ }
default:
Log.e(TAG, "Unrecognised message received.");
break;
}
}
+ private void ackGetTransferredDownloadBytesMessage(@NonNull JobParameters params,
+ @Nullable JobWorkItem item, long progress) {
+ final IJobCallback callback = params.getCallback();
+ final int jobId = params.getJobId();
+ final int workId = item == null ? -1 : item.getWorkId();
+ if (callback != null) {
+ try {
+ callback.acknowledgeGetTransferredDownloadBytesMessage(jobId, workId, progress);
+ } catch (RemoteException e) {
+ Log.e(TAG, "System unreachable for returning progress.");
+ }
+ } else if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Attempting to ack a job that has already been processed.");
+ }
+ }
+
+ private void ackGetTransferredUploadBytesMessage(@NonNull JobParameters params,
+ @Nullable JobWorkItem item, long progress) {
+ final IJobCallback callback = params.getCallback();
+ final int jobId = params.getJobId();
+ final int workId = item == null ? -1 : item.getWorkId();
+ if (callback != null) {
+ try {
+ callback.acknowledgeGetTransferredUploadBytesMessage(jobId, workId, progress);
+ } catch (RemoteException e) {
+ Log.e(TAG, "System unreachable for returning progress.");
+ }
+ } else if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Attempting to ack a job that has already been processed.");
+ }
+ }
+
private void ackStartMessage(JobParameters params, boolean workOngoing) {
final IJobCallback callback = params.getCallback();
final int jobId = params.getJobId();
if (callback != null) {
try {
callback.acknowledgeStartMessage(jobId, workOngoing);
- } catch(RemoteException e) {
+ } catch (RemoteException e) {
Log.e(TAG, "System unreachable for starting job.");
}
} else {
@@ -213,4 +363,69 @@
m.arg2 = needsReschedule ? 1 : 0;
m.sendToTarget();
}
+
+ /**
+ * Engine's request to get how much data has been downloaded.
+ *
+ * @see JobService#getTransferredDownloadBytes()
+ */
+ @BytesLong
+ public long getTransferredDownloadBytes(@NonNull JobParameters params,
+ @Nullable JobWorkItem item) {
+ if (Compatibility.isChangeEnabled(THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION)) {
+ throw new RuntimeException("Not implemented. Must override in a subclass.");
+ }
+ return 0;
+ }
+
+ /**
+ * Engine's request to get how much data has been uploaded.
+ *
+ * @see JobService#getTransferredUploadBytes()
+ */
+ @BytesLong
+ public long getTransferredUploadBytes(@NonNull JobParameters params,
+ @Nullable JobWorkItem item) {
+ if (Compatibility.isChangeEnabled(THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION)) {
+ throw new RuntimeException("Not implemented. Must override in a subclass.");
+ }
+ return 0;
+ }
+
+ /**
+ * Call in to engine to report data transfer progress.
+ *
+ * @see JobService#updateTransferredNetworkBytes(JobParameters, long, long)
+ */
+ public void updateTransferredNetworkBytes(@NonNull JobParameters params,
+ @Nullable JobWorkItem item, long downloadBytes, long uploadBytes) {
+ if (params == null) {
+ throw new NullPointerException("params");
+ }
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = params;
+ args.arg2 = item;
+ args.argl1 = downloadBytes;
+ args.argl2 = uploadBytes;
+ mHandler.obtainMessage(MSG_UPDATE_TRANSFERRED_NETWORK_BYTES, args).sendToTarget();
+ }
+
+ /**
+ * Call in to engine to report data transfer progress.
+ *
+ * @see JobService#updateEstimatedNetworkBytes(JobParameters, JobWorkItem, long, long)
+ */
+ public void updateEstimatedNetworkBytes(@NonNull JobParameters params,
+ @NonNull JobWorkItem item,
+ @BytesLong long downloadBytes, @BytesLong long uploadBytes) {
+ if (params == null) {
+ throw new NullPointerException("params");
+ }
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = params;
+ args.arg2 = item;
+ args.argl1 = downloadBytes;
+ args.argl2 = uploadBytes;
+ mHandler.obtainMessage(MSG_UPDATE_ESTIMATED_NETWORK_BYTES, args).sendToTarget();
+ }
}
\ No newline at end of file
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 358f009..d9fe30d 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -1310,7 +1310,16 @@
jobStatus.getJob().isPrefetch(),
jobStatus.getJob().getPriority(),
jobStatus.getEffectivePriority(),
- jobStatus.getNumPreviousAttempts());
+ jobStatus.getNumPreviousAttempts(),
+ jobStatus.getJob().getMaxExecutionDelayMillis(),
+ /* isDeadlineConstraintSatisfied */ false,
+ /* isCharging */ false,
+ /* batteryNotLow */ false,
+ /* storageNotLow */false,
+ /* timingDelayConstraintSatisfied */ false,
+ /* isDeviceIdle */ false,
+ /* hasConnectivityConstraintSatisfied */ false,
+ /* hasContentTriggerConstraintSatisfied */ false);
// If the job is immediately ready to run, then we can just immediately
// put it in the pending list and try to schedule it. This is especially
@@ -1509,7 +1518,16 @@
cancelled.getJob().isPrefetch(),
cancelled.getJob().getPriority(),
cancelled.getEffectivePriority(),
- cancelled.getNumPreviousAttempts());
+ cancelled.getNumPreviousAttempts(),
+ cancelled.getJob().getMaxExecutionDelayMillis(),
+ cancelled.isConstraintSatisfied(JobStatus.CONSTRAINT_DEADLINE),
+ cancelled.isConstraintSatisfied(JobInfo.CONSTRAINT_FLAG_CHARGING),
+ cancelled.isConstraintSatisfied(JobInfo.CONSTRAINT_FLAG_BATTERY_NOT_LOW),
+ cancelled.isConstraintSatisfied(JobInfo.CONSTRAINT_FLAG_STORAGE_NOT_LOW),
+ cancelled.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY),
+ cancelled.isConstraintSatisfied(JobInfo.CONSTRAINT_FLAG_DEVICE_IDLE),
+ cancelled.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY),
+ cancelled.isConstraintSatisfied(JobStatus.CONSTRAINT_CONTENT_TRIGGER));
}
// If this is a replacement, bring in the new version of the job
if (incomingJob != null) {
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 7c61a35..9aa6b1c 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
@@ -21,6 +21,7 @@
import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_NONE;
import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
+import android.annotation.BytesLong;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.job.IJobCallback;
@@ -187,6 +188,18 @@
public long mStoppedTime;
@Override
+ public void acknowledgeGetTransferredDownloadBytesMessage(int jobId, int workId,
+ @BytesLong long transferredBytes) {
+ doAcknowledgeGetTransferredDownloadBytesMessage(this, jobId, workId, transferredBytes);
+ }
+
+ @Override
+ public void acknowledgeGetTransferredUploadBytesMessage(int jobId, int workId,
+ @BytesLong long transferredBytes) {
+ doAcknowledgeGetTransferredUploadBytesMessage(this, jobId, workId, transferredBytes);
+ }
+
+ @Override
public void acknowledgeStartMessage(int jobId, boolean ongoing) {
doAcknowledgeStartMessage(this, jobId, ongoing);
}
@@ -210,6 +223,18 @@
public void jobFinished(int jobId, boolean reschedule) {
doJobFinished(this, jobId, reschedule);
}
+
+ @Override
+ public void updateEstimatedNetworkBytes(int jobId, JobWorkItem item,
+ long downloadBytes, long uploadBytes) {
+ doUpdateEstimatedNetworkBytes(this, jobId, item, downloadBytes, uploadBytes);
+ }
+
+ @Override
+ public void updateTransferredNetworkBytes(int jobId, JobWorkItem item,
+ long downloadBytes, long uploadBytes) {
+ doUpdateTransferredNetworkBytes(this, jobId, item, downloadBytes, uploadBytes);
+ }
}
JobServiceContext(JobSchedulerService service, JobConcurrencyManager concurrencyManager,
@@ -363,7 +388,16 @@
job.getJob().isPrefetch(),
job.getJob().getPriority(),
job.getEffectivePriority(),
- job.getNumPreviousAttempts());
+ job.getNumPreviousAttempts(),
+ job.getJob().getMaxExecutionDelayMillis(),
+ isDeadlineExpired,
+ job.isConstraintSatisfied(JobInfo.CONSTRAINT_FLAG_CHARGING),
+ job.isConstraintSatisfied(JobInfo.CONSTRAINT_FLAG_BATTERY_NOT_LOW),
+ job.isConstraintSatisfied(JobInfo.CONSTRAINT_FLAG_STORAGE_NOT_LOW),
+ job.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY),
+ job.isConstraintSatisfied(JobInfo.CONSTRAINT_FLAG_DEVICE_IDLE),
+ job.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY),
+ job.isConstraintSatisfied(JobStatus.CONSTRAINT_CONTENT_TRIGGER));
if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) {
// Use the context's ID to distinguish traces since there'll only be one job
// running per context.
@@ -497,6 +531,16 @@
}
}
+ private void doAcknowledgeGetTransferredDownloadBytesMessage(JobCallback jobCallback, int jobId,
+ int workId, @BytesLong long transferredBytes) {
+ // TODO(255393346): Make sure apps call this appropriately and monitor for abuse
+ }
+
+ private void doAcknowledgeGetTransferredUploadBytesMessage(JobCallback jobCallback, int jobId,
+ int workId, @BytesLong long transferredBytes) {
+ // TODO(255393346): Make sure apps call this appropriately and monitor for abuse
+ }
+
void doAcknowledgeStopMessage(JobCallback cb, int jobId, boolean reschedule) {
doCallback(cb, reschedule, null);
}
@@ -549,6 +593,16 @@
}
}
+ private void doUpdateTransferredNetworkBytes(JobCallback jobCallback, int jobId,
+ @Nullable JobWorkItem item, long downloadBytes, long uploadBytes) {
+ // TODO(255393346): Make sure apps call this appropriately and monitor for abuse
+ }
+
+ private void doUpdateEstimatedNetworkBytes(JobCallback jobCallback, int jobId,
+ @Nullable JobWorkItem item, long downloadBytes, long uploadBytes) {
+ // TODO(255393346): Make sure apps call this appropriately and monitor for abuse
+ }
+
/**
* We acquire/release a wakelock on onServiceConnected/unbindService. This mirrors the work
* we intend to send to the client - we stop sending work when the service is unbound so until
@@ -1036,7 +1090,16 @@
completedJob.getJob().isPrefetch(),
completedJob.getJob().getPriority(),
completedJob.getEffectivePriority(),
- completedJob.getNumPreviousAttempts());
+ completedJob.getNumPreviousAttempts(),
+ completedJob.getJob().getMaxExecutionDelayMillis(),
+ mParams.isOverrideDeadlineExpired(),
+ completedJob.isConstraintSatisfied(JobInfo.CONSTRAINT_FLAG_CHARGING),
+ completedJob.isConstraintSatisfied(JobInfo.CONSTRAINT_FLAG_BATTERY_NOT_LOW),
+ completedJob.isConstraintSatisfied(JobInfo.CONSTRAINT_FLAG_STORAGE_NOT_LOW),
+ completedJob.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY),
+ completedJob.isConstraintSatisfied(JobInfo.CONSTRAINT_FLAG_DEVICE_IDLE),
+ completedJob.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY),
+ completedJob.isConstraintSatisfied(JobStatus.CONSTRAINT_CONTENT_TRIGGER));
if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) {
Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_SYSTEM_SERVER, "JobScheduler",
completedJob.getTag(), getId());
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 2f94705f..c2602f2 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
@@ -48,6 +48,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.BitUtils;
+import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
import com.android.server.IoThread;
import com.android.server.job.JobSchedulerInternal.JobStorePersistStats;
@@ -746,9 +747,11 @@
* because currently store is not including everything (like, UIDs, bandwidth,
* signal strength etc. are lost).
*/
- private void writeConstraintsToXml(XmlSerializer out, JobStatus jobStatus) throws IOException {
+ private void writeConstraintsToXml(TypedXmlSerializer out, JobStatus jobStatus)
+ throws IOException {
out.startTag(null, XML_TAG_PARAMS_CONSTRAINTS);
if (jobStatus.hasConnectivityConstraint()) {
+ final JobInfo job = jobStatus.getJob();
final NetworkRequest network = jobStatus.getJob().getRequiredNetwork();
out.attribute(null, "net-capabilities-csv", intArrayToString(
network.getCapabilities()));
@@ -756,6 +759,18 @@
network.getForbiddenCapabilities()));
out.attribute(null, "net-transport-types-csv", intArrayToString(
network.getTransportTypes()));
+ if (job.getEstimatedNetworkDownloadBytes() != JobInfo.NETWORK_BYTES_UNKNOWN) {
+ out.attributeLong(null, "estimated-download-bytes",
+ job.getEstimatedNetworkDownloadBytes());
+ }
+ if (job.getEstimatedNetworkUploadBytes() != JobInfo.NETWORK_BYTES_UNKNOWN) {
+ out.attributeLong(null, "estimated-upload-bytes",
+ job.getEstimatedNetworkUploadBytes());
+ }
+ if (job.getMinimumNetworkChunkBytes() != JobInfo.NETWORK_BYTES_UNKNOWN) {
+ out.attributeLong(null, "minimum-network-chunk-bytes",
+ job.getMinimumNetworkChunkBytes());
+ }
}
if (jobStatus.hasIdleConstraint()) {
out.attribute(null, "idle", Boolean.toString(true));
@@ -946,7 +961,7 @@
private List<JobStatus> readJobMapImpl(InputStream fis, boolean rtcIsGood)
throws XmlPullParserException, IOException {
- XmlPullParser parser = Xml.resolvePullParser(fis);
+ TypedXmlPullParser parser = Xml.resolvePullParser(fis);
int eventType = parser.getEventType();
while (eventType != XmlPullParser.START_TAG &&
@@ -1006,7 +1021,7 @@
* will take the parser into the body of the job tag.
* @return Newly instantiated job holding all the information we just read out of the xml tag.
*/
- private JobStatus restoreJobFromXml(boolean rtcIsGood, XmlPullParser parser,
+ private JobStatus restoreJobFromXml(boolean rtcIsGood, TypedXmlPullParser parser,
int schemaVersion) throws XmlPullParserException, IOException {
JobInfo.Builder jobBuilder;
int uid, sourceUserId;
@@ -1252,7 +1267,7 @@
* reading, but in order to avoid issues with OEM-defined flags, the accepted capabilities
* are limited to that(maxNetCapabilityInR & maxTransportInR) defined in R.
*/
- private void buildConstraintsFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser)
+ private void buildConstraintsFromXml(JobInfo.Builder jobBuilder, TypedXmlPullParser parser)
throws XmlPullParserException, IOException {
String val;
String netCapabilitiesLong = null;
@@ -1289,7 +1304,17 @@
for (int transport : stringToIntArray(netTransportTypesIntArray)) {
builder.addTransportType(transport);
}
- jobBuilder.setRequiredNetwork(builder.build());
+ jobBuilder
+ .setRequiredNetwork(builder.build())
+ .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));
} else if (netCapabilitiesLong != null && netTransportTypesLong != null) {
// Format used on R- builds. Drop any unexpected capabilities and transports.
final NetworkRequest.Builder builder = new NetworkRequest.Builder()
@@ -1317,6 +1342,8 @@
}
}
jobBuilder.setRequiredNetwork(builder.build());
+ // Estimated bytes weren't persisted on R- builds, so no point querying for the
+ // attributes here.
} else {
// Read legacy values
val = parser.getAttributeValue(null, "connectivity");
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 92716f4..f6410ff 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
@@ -95,11 +95,11 @@
static final int CONSTRAINT_IDLE = JobInfo.CONSTRAINT_FLAG_DEVICE_IDLE; // 1 << 2
static final int CONSTRAINT_BATTERY_NOT_LOW = JobInfo.CONSTRAINT_FLAG_BATTERY_NOT_LOW; // 1 << 1
static final int CONSTRAINT_STORAGE_NOT_LOW = JobInfo.CONSTRAINT_FLAG_STORAGE_NOT_LOW; // 1 << 3
- static final int CONSTRAINT_TIMING_DELAY = 1<<31;
- static final int CONSTRAINT_DEADLINE = 1<<30;
- static final int CONSTRAINT_CONNECTIVITY = 1 << 28;
+ public static final int CONSTRAINT_TIMING_DELAY = 1 << 31;
+ public static final int CONSTRAINT_DEADLINE = 1 << 30;
+ public static final int CONSTRAINT_CONNECTIVITY = 1 << 28;
static final int CONSTRAINT_TARE_WEALTH = 1 << 27; // Implicit constraint
- static final int CONSTRAINT_CONTENT_TRIGGER = 1<<26;
+ public static final int CONSTRAINT_CONTENT_TRIGGER = 1 << 26;
static final int CONSTRAINT_DEVICE_NOT_DOZING = 1 << 25; // Implicit constraint
static final int CONSTRAINT_WITHIN_QUOTA = 1 << 24; // Implicit constraint
static final int CONSTRAINT_PREFETCH = 1 << 23;
@@ -1613,7 +1613,8 @@
}
}
- boolean isConstraintSatisfied(int constraint) {
+ /** @return whether or not the @param constraint is satisfied */
+ public boolean isConstraintSatisfied(int constraint) {
return (satisfiedConstraints&constraint) != 0;
}
diff --git a/core/api/current.txt b/core/api/current.txt
index 3766a0b9..3a0788d 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -7669,6 +7669,7 @@
method public boolean updateOverrideApn(@NonNull android.content.ComponentName, int, @NonNull android.telephony.data.ApnSetting);
method public void wipeData(int);
method public void wipeData(int, @NonNull CharSequence);
+ method public void wipeDevice(int);
field public static final String ACTION_ADD_DEVICE_ADMIN = "android.app.action.ADD_DEVICE_ADMIN";
field public static final String ACTION_ADMIN_POLICY_COMPLIANCE = "android.app.action.ADMIN_POLICY_COMPLIANCE";
field public static final String ACTION_APPLICATION_DELEGATION_SCOPES_CHANGED = "android.app.action.APPLICATION_DELEGATION_SCOPES_CHANGED";
@@ -8448,19 +8449,31 @@
public abstract class JobService extends android.app.Service {
ctor public JobService();
+ method public long getTransferredDownloadBytes();
+ method public long getTransferredDownloadBytes(@NonNull android.app.job.JobWorkItem);
+ method public long getTransferredUploadBytes();
+ method public long getTransferredUploadBytes(@NonNull android.app.job.JobWorkItem);
method public final void jobFinished(android.app.job.JobParameters, boolean);
method public final android.os.IBinder onBind(android.content.Intent);
method public abstract boolean onStartJob(android.app.job.JobParameters);
method public abstract boolean onStopJob(android.app.job.JobParameters);
+ method public final void updateEstimatedNetworkBytes(@NonNull android.app.job.JobParameters, long, long);
+ method public final void updateEstimatedNetworkBytes(@NonNull android.app.job.JobParameters, @NonNull android.app.job.JobWorkItem, long, long);
+ method public final void updateTransferredNetworkBytes(@NonNull android.app.job.JobParameters, long, long);
+ method public final void updateTransferredNetworkBytes(@NonNull android.app.job.JobParameters, @NonNull android.app.job.JobWorkItem, long, long);
field public static final String PERMISSION_BIND = "android.permission.BIND_JOB_SERVICE";
}
public abstract class JobServiceEngine {
ctor public JobServiceEngine(android.app.Service);
method public final android.os.IBinder getBinder();
+ method public long getTransferredDownloadBytes(@NonNull android.app.job.JobParameters, @Nullable android.app.job.JobWorkItem);
+ method public long getTransferredUploadBytes(@NonNull android.app.job.JobParameters, @Nullable android.app.job.JobWorkItem);
method public void jobFinished(android.app.job.JobParameters, boolean);
method public abstract boolean onStartJob(android.app.job.JobParameters);
method public abstract boolean onStopJob(android.app.job.JobParameters);
+ method public void updateEstimatedNetworkBytes(@NonNull android.app.job.JobParameters, @NonNull android.app.job.JobWorkItem, long, long);
+ method public void updateTransferredNetworkBytes(@NonNull android.app.job.JobParameters, @Nullable android.app.job.JobWorkItem, long, long);
}
public final class JobWorkItem implements android.os.Parcelable {
@@ -16585,6 +16598,7 @@
field public static final int FONT_WEIGHT_NORMAL = 400; // 0x190
field public static final int FONT_WEIGHT_SEMI_BOLD = 600; // 0x258
field public static final int FONT_WEIGHT_THIN = 100; // 0x64
+ field public static final int FONT_WEIGHT_UNSPECIFIED = -1; // 0xffffffff
}
public final class FontVariationAxis {
@@ -32169,7 +32183,12 @@
public static class PerformanceHintManager.Session implements java.io.Closeable {
method public void close();
method public void reportActualWorkDuration(long);
+ method public void sendHint(int);
method public void updateTargetWorkDuration(long);
+ field public static final int CPU_LOAD_DOWN = 1; // 0x1
+ field public static final int CPU_LOAD_RESET = 2; // 0x2
+ field public static final int CPU_LOAD_RESUME = 3; // 0x3
+ field public static final int CPU_LOAD_UP = 0; // 0x0
}
public final class PersistableBundle extends android.os.BaseBundle implements java.lang.Cloneable android.os.Parcelable {
@@ -32619,6 +32638,7 @@
field public static final String DISALLOW_SHARING_ADMIN_CONFIGURED_WIFI = "no_sharing_admin_configured_wifi";
field public static final String DISALLOW_SMS = "no_sms";
field public static final String DISALLOW_SYSTEM_ERROR_DIALOGS = "no_system_error_dialogs";
+ field public static final String DISALLOW_ULTRA_WIDEBAND_RADIO = "no_ultra_wideband_radio";
field public static final String DISALLOW_UNIFIED_PASSWORD = "no_unified_password";
field public static final String DISALLOW_UNINSTALL_APPS = "no_uninstall_apps";
field public static final String DISALLOW_UNMUTE_MICROPHONE = "no_unmute_microphone";
@@ -35801,6 +35821,7 @@
field public static final String ACTION_MANAGE_UNKNOWN_APP_SOURCES = "android.settings.MANAGE_UNKNOWN_APP_SOURCES";
field public static final String ACTION_MANAGE_WRITE_SETTINGS = "android.settings.action.MANAGE_WRITE_SETTINGS";
field public static final String ACTION_MEMORY_CARD_SETTINGS = "android.settings.MEMORY_CARD_SETTINGS";
+ field public static final String ACTION_MEMTAG_SETTINGS = "android.settings.MEMTAG_SETTINGS";
field public static final String ACTION_NETWORK_OPERATOR_SETTINGS = "android.settings.NETWORK_OPERATOR_SETTINGS";
field public static final String ACTION_NFCSHARING_SETTINGS = "android.settings.NFCSHARING_SETTINGS";
field public static final String ACTION_NFC_PAYMENT_SETTINGS = "android.settings.NFC_PAYMENT_SETTINGS";
@@ -44098,7 +44119,7 @@
field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_ENTITLEMENT_CHECK_FAILED = 13; // 0xd
field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_FEATURE_NOT_SUPPORTED = 10; // 0xa
field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_NOT_AVAILABLE = 12; // 0xc
- field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA_SUB = 14; // 0xe
+ field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA_SUBSCRIPTION = 14; // 0xe
field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_OVERRIDDEN = 5; // 0x5
field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_PENDING_NETWORK_SETUP = 15; // 0xf
field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_REQUEST_FAILED = 11; // 0xb
@@ -51906,6 +51927,7 @@
method public static int statusBars();
method public static int systemBars();
method public static int systemGestures();
+ method public static int systemOverlays();
method public static int tappableElement();
}
@@ -52354,6 +52376,7 @@
method public android.view.accessibility.AccessibilityNodeInfo getLabeledBy();
method public int getLiveRegion();
method public int getMaxTextLength();
+ method public int getMinMillisBetweenContentChanges();
method public int getMovementGranularities();
method public CharSequence getPackageName();
method @Nullable public CharSequence getPaneTitle();
@@ -52441,6 +52464,7 @@
method public void setLiveRegion(int);
method public void setLongClickable(boolean);
method public void setMaxTextLength(int);
+ method public void setMinMillisBetweenContentChanges(int);
method public void setMovementGranularities(int);
method public void setMultiLine(boolean);
method public void setPackageName(CharSequence);
@@ -52520,11 +52544,13 @@
field public static final int FOCUS_ACCESSIBILITY = 2; // 0x2
field public static final int FOCUS_INPUT = 1; // 0x1
field public static final int MAX_NUMBER_OF_PREFETCHED_NODES = 50; // 0x32
+ field public static final int MINIMUM_MIN_MILLIS_BETWEEN_CONTENT_CHANGES = 100; // 0x64
field public static final int MOVEMENT_GRANULARITY_CHARACTER = 1; // 0x1
field public static final int MOVEMENT_GRANULARITY_LINE = 4; // 0x4
field public static final int MOVEMENT_GRANULARITY_PAGE = 16; // 0x10
field public static final int MOVEMENT_GRANULARITY_PARAGRAPH = 8; // 0x8
field public static final int MOVEMENT_GRANULARITY_WORD = 2; // 0x2
+ field public static final int UNDEFINED_MIN_MILLIS_BETWEEN_CONTENT_CHANGES = -1; // 0xffffffff
}
public static final class AccessibilityNodeInfo.AccessibilityAction implements android.os.Parcelable {
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index ae6e58c..b5b87c1 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -14155,6 +14155,7 @@
ctor public QualifiedNetworksService.NetworkAvailabilityProvider(int);
method public abstract void close();
method public final int getSlotIndex();
+ method public void reportEmergencyDataNetworkPreferredTransportChanged(int);
method public void reportThrottleStatusChanged(@NonNull java.util.List<android.telephony.data.ThrottleStatus>);
method public final void updateQualifiedNetworkTypes(int, @NonNull java.util.List<java.lang.Integer>);
}
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index ae5a5ee..d8419a8 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -2945,6 +2945,7 @@
method public static int getHoverTooltipHideTimeout();
method public static int getHoverTooltipShowTimeout();
method public static int getLongPressTooltipHideTimeout();
+ method public static long getSendRecurringAccessibilityEventsInterval();
method public boolean isPreferKeepClearForFocusEnabled();
}
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index f4cee5a..6fedb41 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -171,6 +171,7 @@
private final boolean mParentInstance;
private final DevicePolicyResourcesManager mResourcesManager;
+
/** @hide */
public DevicePolicyManager(Context context, IDevicePolicyManager service) {
this(context, service, false);
@@ -6207,46 +6208,46 @@
public static final int WIPE_SILENTLY = 0x0008;
/**
- * Ask that all user data be wiped. If called as a secondary user, the user will be removed and
- * other users will remain unaffected. Calling from the primary user will cause the device to
- * reboot, erasing all device data - including all the secondary users and their data - while
- * booting up.
- * <p>
- * The calling device admin must have requested {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} to
- * be able to call this method; if it has not, a security exception will be thrown.
- *
- * If the caller is a profile owner of an organization-owned managed profile, it may
- * additionally call this method on the parent instance.
- * Calling this method on the parent {@link DevicePolicyManager} instance would wipe the
- * entire device, while calling it on the current profile instance would relinquish the device
- * for personal use, removing the managed profile and all policies set by the profile owner.
+ * See {@link #wipeData(int, CharSequence)}
*
* @param flags Bit mask of additional options: currently supported flags are
- * {@link #WIPE_EXTERNAL_STORAGE}, {@link #WIPE_RESET_PROTECTION_DATA},
- * {@link #WIPE_EUICC} and {@link #WIPE_SILENTLY}.
- * @throws SecurityException if the calling application does not own an active administrator
- * that uses {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} or is not granted the
- * {@link android.Manifest.permission#MASTER_CLEAR} permission.
+ * {@link #WIPE_EXTERNAL_STORAGE}, {@link #WIPE_RESET_PROTECTION_DATA},
+ * {@link #WIPE_EUICC} and {@link #WIPE_SILENTLY}.
+ * @throws SecurityException if the calling application does not own an active
+ * administrator
+ * that uses {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} and is
+ * not granted the
+ * {@link android.Manifest.permission#MASTER_CLEAR} permission.
+ * @throws IllegalStateException if called on last full-user or system-user
+ * @see #wipeDevice(int)
+ * @see #wipeData(int, CharSequence)
*/
public void wipeData(int flags) {
- wipeDataInternal(flags, "");
+ wipeDataInternal(flags,
+ /* wipeReasonForUser= */ "",
+ /* factoryReset= */ false);
}
/**
- * Ask that all user data be wiped. If called as a secondary user, the user will be removed and
- * other users will remain unaffected, the provided reason for wiping data can be shown to
- * user. Calling from the primary user will cause the device to reboot, erasing all device data
- * - including all the secondary users and their data - while booting up. In this case, we don't
- * show the reason to the user since the device would be factory reset.
- * <p>
- * The calling device admin must have requested {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} to
- * be able to call this method; if it has not, a security exception will be thrown.
+ * Ask that all user data be wiped.
*
- * If the caller is a profile owner of an organization-owned managed profile, it may
- * additionally call this method on the parent instance.
- * Calling this method on the parent {@link DevicePolicyManager} instance would wipe the
- * entire device, while calling it on the current profile instance would relinquish the device
- * for personal use, removing the managed profile and all policies set by the profile owner.
+ * <p>
+ * If called as a secondary user or managed profile, the user itself and its associated user
+ * data will be wiped. In particular, If the caller is a profile owner of an
+ * organization-owned managed profile, calling this method will relinquish the device for
+ * personal use, removing the managed profile and all policies set by the profile owner.
+ * </p>
+ *
+ * <p>
+ * Calling this method from the primary user will only work if the calling app is targeting
+ * Android 13 or below, in which case it will cause the device to reboot, erasing all device
+ * data - including all the secondary users and their data - while booting up. If an app
+ * targeting Android 13+ is calling this method from the primary user or last full user,
+ * {@link IllegalStateException} will be thrown.
+ * </p>
+ *
+ * If an app wants to wipe the entire device irrespective of which user they are from, they
+ * should use {@link #wipeDevice} instead.
*
* @param flags Bit mask of additional options: currently supported flags are
* {@link #WIPE_EXTERNAL_STORAGE}, {@link #WIPE_RESET_PROTECTION_DATA} and
@@ -6254,30 +6255,61 @@
* @param reason a string that contains the reason for wiping data, which can be
* presented to the user.
* @throws SecurityException if the calling application does not own an active administrator
- * that uses {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} or is not granted the
+ * that uses {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} and is not granted the
* {@link android.Manifest.permission#MASTER_CLEAR} permission.
* @throws IllegalArgumentException if the input reason string is null or empty, or if
* {@link #WIPE_SILENTLY} is set.
+ * @throws IllegalStateException if called on last full-user or system-user
+ * @see #wipeDevice(int)
+ * @see #wipeData(int)
*/
public void wipeData(int flags, @NonNull CharSequence reason) {
Objects.requireNonNull(reason, "reason string is null");
Preconditions.checkStringNotEmpty(reason, "reason string is empty");
Preconditions.checkArgument((flags & WIPE_SILENTLY) == 0, "WIPE_SILENTLY cannot be set");
- wipeDataInternal(flags, reason.toString());
+ wipeDataInternal(flags, reason.toString(), /* factoryReset= */ false);
}
/**
- * Internal function for both {@link #wipeData(int)} and
- * {@link #wipeData(int, CharSequence)} to call.
+ * Ask that the device be wiped and factory reset.
*
+ * <p>
+ * The calling Device Owner or Organization Owned Profile Owner must have requested
+ * {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} to be able to call this method; if it has
+ * not, a security exception will be thrown.
+ *
+ * @param flags Bit mask of additional options: currently supported flags are
+ * {@link #WIPE_EXTERNAL_STORAGE}, {@link #WIPE_RESET_PROTECTION_DATA},
+ * {@link #WIPE_EUICC} and {@link #WIPE_SILENTLY}.
+ * @throws SecurityException if the calling application does not own an active administrator
+ * that uses {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} and is not
+ * granted the {@link android.Manifest.permission#MASTER_CLEAR}
+ * permission.
* @see #wipeData(int)
* @see #wipeData(int, CharSequence)
- * @hide
*/
- private void wipeDataInternal(int flags, @NonNull String wipeReasonForUser) {
+ // TODO(b/255323293) Add host-side tests
+ public void wipeDevice(int flags) {
+ wipeDataInternal(flags,
+ /* wipeReasonForUser= */ "",
+ /* factoryReset= */ true);
+ }
+
+ /**
+ * Internal function for {@link #wipeData(int)}, {@link #wipeData(int, CharSequence)}
+ * and {@link #wipeDevice(int)} to call.
+ *
+ * @hide
+ * @see #wipeData(int)
+ * @see #wipeData(int, CharSequence)
+ * @see #wipeDevice(int)
+ */
+ private void wipeDataInternal(int flags, @NonNull String wipeReasonForUser,
+ boolean factoryReset) {
if (mService != null) {
try {
- mService.wipeDataWithReason(flags, wipeReasonForUser, mParentInstance);
+ mService.wipeDataWithReason(flags, wipeReasonForUser, mParentInstance,
+ factoryReset);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -8642,7 +8674,7 @@
public void reportFailedPasswordAttempt(int userHandle) {
if (mService != null) {
try {
- mService.reportFailedPasswordAttempt(userHandle);
+ mService.reportFailedPasswordAttempt(userHandle, mParentInstance);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index 75bfc25..6c27dd7 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -117,7 +117,10 @@
void lockNow(int flags, boolean parent);
- void wipeDataWithReason(int flags, String wipeReasonForUser, boolean parent);
+ /**
+ * @param factoryReset only applicable when `targetSdk >= U`, either tries to factoryReset/fail or removeUser/fail otherwise
+ **/
+ void wipeDataWithReason(int flags, String wipeReasonForUser, boolean parent, boolean factoryReset);
void setFactoryResetProtectionPolicy(in ComponentName who, in FactoryResetProtectionPolicy policy);
FactoryResetProtectionPolicy getFactoryResetProtectionPolicy(in ComponentName who);
@@ -161,7 +164,7 @@
boolean hasGrantedPolicy(in ComponentName policyReceiver, int usesPolicy, int userHandle);
void reportPasswordChanged(in PasswordMetrics metrics, int userId);
- void reportFailedPasswordAttempt(int userHandle);
+ void reportFailedPasswordAttempt(int userHandle, boolean parent);
void reportSuccessfulPasswordAttempt(int userHandle);
void reportFailedBiometricAttempt(int userHandle);
void reportSuccessfulBiometricAttempt(int userHandle);
diff --git a/core/java/android/content/pm/UserProperties.java b/core/java/android/content/pm/UserProperties.java
index 0d3c8db..fd35378 100644
--- a/core/java/android/content/pm/UserProperties.java
+++ b/core/java/android/content/pm/UserProperties.java
@@ -45,6 +45,7 @@
private static final String ATTR_START_WITH_PARENT = "startWithParent";
private static final String ATTR_SHOW_IN_SETTINGS = "showInSettings";
private static final String ATTR_INHERIT_DEVICE_POLICY = "inheritDevicePolicy";
+ private static final String ATTR_USE_PARENTS_CONTACTS = "useParentsContacts";
/** Index values of each property (to indicate whether they are present in this object). */
@IntDef(prefix = "INDEX_", value = {
@@ -52,6 +53,7 @@
INDEX_START_WITH_PARENT,
INDEX_SHOW_IN_SETTINGS,
INDEX_INHERIT_DEVICE_POLICY,
+ INDEX_USE_PARENTS_CONTACTS,
})
@Retention(RetentionPolicy.SOURCE)
private @interface PropertyIndex {
@@ -60,6 +62,7 @@
private static final int INDEX_START_WITH_PARENT = 1;
private static final int INDEX_SHOW_IN_SETTINGS = 2;
private static final int INDEX_INHERIT_DEVICE_POLICY = 3;
+ private static final int INDEX_USE_PARENTS_CONTACTS = 4;
/** A bit set, mapping each PropertyIndex to whether it is present (1) or absent (0). */
private long mPropertiesPresent = 0;
@@ -200,6 +203,7 @@
if (hasManagePermission) {
// Add items that require MANAGE_USERS or stronger.
setShowInSettings(orig.getShowInSettings());
+ setUseParentsContacts(orig.getUseParentsContacts());
}
if (hasQueryOrManagePermission) {
// Add items that require QUERY_USERS or stronger.
@@ -317,6 +321,39 @@
}
private @InheritDevicePolicy int mInheritDevicePolicy;
+ /**
+ * Returns whether the current user must use parent user's contacts. If true, writes to the
+ * ContactsProvider corresponding to the current user will be disabled and reads will be
+ * redirected to the parent.
+ *
+ * This only applies to users that have parents (i.e. profiles) and is used to ensure
+ * they can access contacts from the parent profile. This will be generally inapplicable for
+ * non-profile users.
+ *
+ * Please note that in case of the clone profiles, only the allow-listed apps would be allowed
+ * to access contacts across profiles and other apps will not see any contacts.
+ * TODO(b/256126819) Add link to the method returning apps allow-listed for app-cloning
+ *
+ * @return whether contacts access from an associated profile is enabled for the user
+ * @hide
+ */
+ public boolean getUseParentsContacts() {
+ if (isPresent(INDEX_USE_PARENTS_CONTACTS)) return mUseParentsContacts;
+ if (mDefaultProperties != null) return mDefaultProperties.mUseParentsContacts;
+ throw new SecurityException("You don't have permission to query useParentsContacts");
+ }
+ /** @hide */
+ public void setUseParentsContacts(boolean val) {
+ this.mUseParentsContacts = val;
+ setPresent(INDEX_USE_PARENTS_CONTACTS);
+ }
+ /**
+ * Indicates whether the current user should use parent user's contacts.
+ * If this property is set true, the user will be blocked from storing any contacts in its
+ * own contacts database and will serve all read contacts calls through the parent's contacts.
+ */
+ private boolean mUseParentsContacts;
+
@Override
public String toString() {
// Please print in increasing order of PropertyIndex.
@@ -326,6 +363,7 @@
+ ", mStartWithParent=" + getStartWithParent()
+ ", mShowInSettings=" + getShowInSettings()
+ ", mInheritDevicePolicy=" + getInheritDevicePolicy()
+ + ", mUseParentsContacts=" + getUseParentsContacts()
+ "}";
}
@@ -341,6 +379,7 @@
pw.println(prefix + " mStartWithParent=" + getStartWithParent());
pw.println(prefix + " mShowInSettings=" + getShowInSettings());
pw.println(prefix + " mInheritDevicePolicy=" + getInheritDevicePolicy());
+ pw.println(prefix + " mUseParentsContacts=" + getUseParentsContacts());
}
/**
@@ -386,6 +425,9 @@
case ATTR_INHERIT_DEVICE_POLICY:
setInheritDevicePolicy(parser.getAttributeInt(i));
break;
+ case ATTR_USE_PARENTS_CONTACTS:
+ setUseParentsContacts(parser.getAttributeBoolean(i));
+ break;
default:
Slog.w(LOG_TAG, "Skipping unknown property " + attributeName);
}
@@ -416,6 +458,10 @@
serializer.attributeInt(null, ATTR_INHERIT_DEVICE_POLICY,
mInheritDevicePolicy);
}
+ if (isPresent(INDEX_USE_PARENTS_CONTACTS)) {
+ serializer.attributeBoolean(null, ATTR_USE_PARENTS_CONTACTS,
+ mUseParentsContacts);
+ }
}
// For use only with an object that has already had any permission-lacking fields stripped out.
@@ -426,6 +472,7 @@
dest.writeBoolean(mStartWithParent);
dest.writeInt(mShowInSettings);
dest.writeInt(mInheritDevicePolicy);
+ dest.writeBoolean(mUseParentsContacts);
}
/**
@@ -440,6 +487,7 @@
mStartWithParent = source.readBoolean();
mShowInSettings = source.readInt();
mInheritDevicePolicy = source.readInt();
+ mUseParentsContacts = source.readBoolean();
}
@Override
@@ -468,6 +516,7 @@
private boolean mStartWithParent = false;
private @ShowInSettings int mShowInSettings = SHOW_IN_SETTINGS_WITH_PARENT;
private @InheritDevicePolicy int mInheritDevicePolicy = INHERIT_DEVICE_POLICY_NO;
+ private boolean mUseParentsContacts = false;
public Builder setShowInLauncher(@ShowInLauncher int showInLauncher) {
mShowInLauncher = showInLauncher;
@@ -492,13 +541,19 @@
return this;
}
+ public Builder setUseParentsContacts(boolean useParentsContacts) {
+ mUseParentsContacts = useParentsContacts;
+ return this;
+ }
+
/** Builds a UserProperties object with *all* values populated. */
public UserProperties build() {
return new UserProperties(
mShowInLauncher,
mStartWithParent,
mShowInSettings,
- mInheritDevicePolicy);
+ mInheritDevicePolicy,
+ mUseParentsContacts);
}
} // end Builder
@@ -507,12 +562,14 @@
@ShowInLauncher int showInLauncher,
boolean startWithParent,
@ShowInSettings int showInSettings,
- @InheritDevicePolicy int inheritDevicePolicy) {
+ @InheritDevicePolicy int inheritDevicePolicy,
+ boolean useParentsContacts) {
mDefaultProperties = null;
setShowInLauncher(showInLauncher);
setStartWithParent(startWithParent);
setShowInSettings(showInSettings);
setInheritDevicePolicy(inheritDevicePolicy);
+ setUseParentsContacts(useParentsContacts);
}
}
diff --git a/core/java/android/credentials/ui/CreateCredentialProviderData.java b/core/java/android/credentials/ui/CreateCredentialProviderData.java
index 98157d7..0444278 100644
--- a/core/java/android/credentials/ui/CreateCredentialProviderData.java
+++ b/core/java/android/credentials/ui/CreateCredentialProviderData.java
@@ -131,6 +131,13 @@
return this;
}
+ /** Sets the remote entry of the provider. */
+ @NonNull
+ public Builder setRemoteEntry(@Nullable Entry remoteEntry) {
+ mRemoteEntry = remoteEntry;
+ return this;
+ }
+
/** Builds a {@link CreateCredentialProviderData}. */
@NonNull
public CreateCredentialProviderData build() {
diff --git a/core/java/android/inputmethodservice/IInputMethodWrapper.java b/core/java/android/inputmethodservice/IInputMethodWrapper.java
index d23fb36..d55367f 100644
--- a/core/java/android/inputmethodservice/IInputMethodWrapper.java
+++ b/core/java/android/inputmethodservice/IInputMethodWrapper.java
@@ -32,6 +32,7 @@
import android.util.Log;
import android.view.InputChannel;
import android.view.MotionEvent;
+import android.view.inputmethod.ImeTracker;
import android.view.inputmethod.InputBinding;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethod;
@@ -93,9 +94,10 @@
final int mTargetSdkVersion;
/**
- * This is not {@null} only between {@link #bindInput(InputBinding)} and {@link #unbindInput()}
- * so that {@link RemoteInputConnection} can query if {@link #unbindInput()} has already been
- * called or not, mainly to avoid unnecessary blocking operations.
+ * This is not {@code null} only between {@link #bindInput(InputBinding)} and
+ * {@link #unbindInput()} so that {@link RemoteInputConnection} can query if
+ * {@link #unbindInput()} has already been called or not, mainly to avoid unnecessary
+ * blocking operations.
*
* <p>This field must be set and cleared only from the binder thread(s), where the system
* guarantees that {@link #bindInput(InputBinding)},
@@ -219,18 +221,26 @@
return;
case DO_SHOW_SOFT_INPUT: {
final SomeArgs args = (SomeArgs) msg.obj;
+ final ImeTracker.Token statsToken = (ImeTracker.Token) args.arg3;
if (isValid(inputMethod, target, "DO_SHOW_SOFT_INPUT")) {
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_IME_WRAPPER_DISPATCH);
inputMethod.showSoftInputWithToken(
- msg.arg1, (ResultReceiver) args.arg2, (IBinder) args.arg1);
+ msg.arg1, (ResultReceiver) args.arg2, (IBinder) args.arg1, statsToken);
+ } else {
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_IME_WRAPPER_DISPATCH);
}
args.recycle();
return;
}
case DO_HIDE_SOFT_INPUT: {
final SomeArgs args = (SomeArgs) msg.obj;
+ final ImeTracker.Token statsToken = (ImeTracker.Token) args.arg3;
if (isValid(inputMethod, target, "DO_HIDE_SOFT_INPUT")) {
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_IME_WRAPPER_DISPATCH);
inputMethod.hideSoftInputWithToken(msg.arg1, (ResultReceiver) args.arg2,
- (IBinder) args.arg1);
+ (IBinder) args.arg1, statsToken);
+ } else {
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_IME_WRAPPER_DISPATCH);
}
args.recycle();
return;
@@ -416,16 +426,20 @@
@BinderThread
@Override
- public void showSoftInput(IBinder showInputToken, int flags, ResultReceiver resultReceiver) {
- mCaller.executeOrSendMessage(mCaller.obtainMessageIOO(DO_SHOW_SOFT_INPUT,
- flags, showInputToken, resultReceiver));
+ public void showSoftInput(IBinder showInputToken, @Nullable ImeTracker.Token statsToken,
+ int flags, ResultReceiver resultReceiver) {
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_IME_WRAPPER);
+ mCaller.executeOrSendMessage(mCaller.obtainMessageIOOO(DO_SHOW_SOFT_INPUT,
+ flags, showInputToken, resultReceiver, statsToken));
}
@BinderThread
@Override
- public void hideSoftInput(IBinder hideInputToken, int flags, ResultReceiver resultReceiver) {
- mCaller.executeOrSendMessage(mCaller.obtainMessageIOO(DO_HIDE_SOFT_INPUT,
- flags, hideInputToken, resultReceiver));
+ public void hideSoftInput(IBinder hideInputToken, @Nullable ImeTracker.Token statsToken,
+ int flags, ResultReceiver resultReceiver) {
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_IME_WRAPPER);
+ mCaller.executeOrSendMessage(mCaller.obtainMessageIOOO(DO_HIDE_SOFT_INPUT,
+ flags, hideInputToken, resultReceiver, statsToken));
}
@BinderThread
diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index d902486..bf4fc4a 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -124,6 +124,7 @@
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.ExtractedText;
import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.ImeTracker;
import android.view.inputmethod.InlineSuggestionsRequest;
import android.view.inputmethod.InlineSuggestionsResponse;
import android.view.inputmethod.InputBinding;
@@ -669,6 +670,10 @@
*/
private IBinder mCurHideInputToken;
+ /** The token tracking the current IME request or {@code null} otherwise. */
+ @Nullable
+ private ImeTracker.Token mCurStatsToken;
+
final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsComputer = info -> {
onComputeInsets(mTmpInsets);
if (!mViewsCreated) {
@@ -870,10 +875,12 @@
@MainThread
@Override
public void hideSoftInputWithToken(int flags, ResultReceiver resultReceiver,
- IBinder hideInputToken) {
+ IBinder hideInputToken, @Nullable ImeTracker.Token statsToken) {
mSystemCallingHideSoftInput = true;
mCurHideInputToken = hideInputToken;
+ mCurStatsToken = statsToken;
hideSoftInput(flags, resultReceiver);
+ mCurStatsToken = null;
mCurHideInputToken = null;
mSystemCallingHideSoftInput = false;
}
@@ -884,6 +891,7 @@
@MainThread
@Override
public void hideSoftInput(int flags, ResultReceiver resultReceiver) {
+ ImeTracker.get().onProgress(mCurStatsToken, ImeTracker.PHASE_IME_HIDE_SOFT_INPUT);
if (DEBUG) Log.v(TAG, "hideSoftInput()");
if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.R
&& !mSystemCallingHideSoftInput) {
@@ -918,12 +926,17 @@
@MainThread
@Override
public void showSoftInputWithToken(int flags, ResultReceiver resultReceiver,
- IBinder showInputToken) {
+ IBinder showInputToken, @Nullable ImeTracker.Token statsToken) {
mSystemCallingShowSoftInput = true;
mCurShowInputToken = showInputToken;
- showSoftInput(flags, resultReceiver);
- mCurShowInputToken = null;
- mSystemCallingShowSoftInput = false;
+ mCurStatsToken = statsToken;
+ try {
+ showSoftInput(flags, resultReceiver);
+ } finally {
+ mCurStatsToken = null;
+ mCurShowInputToken = null;
+ mSystemCallingShowSoftInput = false;
+ }
}
/**
@@ -932,6 +945,7 @@
@MainThread
@Override
public void showSoftInput(int flags, ResultReceiver resultReceiver) {
+ ImeTracker.get().onProgress(mCurStatsToken, ImeTracker.PHASE_IME_SHOW_SOFT_INPUT);
if (DEBUG) Log.v(TAG, "showSoftInput()");
// TODO(b/148086656): Disallow IME developers from calling InputMethodImpl methods.
if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.R
@@ -947,7 +961,12 @@
null /* icProto */);
final boolean wasVisible = isInputViewShown();
if (dispatchOnShowInputRequested(flags, false)) {
+ ImeTracker.get().onProgress(mCurStatsToken,
+ ImeTracker.PHASE_IME_ON_SHOW_SOFT_INPUT_TRUE);
showWindow(true);
+ } else {
+ ImeTracker.get().onFailed(mCurStatsToken,
+ ImeTracker.PHASE_IME_ON_SHOW_SOFT_INPUT_TRUE);
}
setImeWindowStatus(mapToImeWindowStatus(), mBackDisposition);
@@ -2923,8 +2942,10 @@
ImeTracing.getInstance().triggerServiceDump(
"InputMethodService#applyVisibilityInInsetsConsumerIfNecessary", mDumper,
null /* icProto */);
+ ImeTracker.get().onProgress(mCurStatsToken,
+ ImeTracker.PHASE_IME_APPLY_VISIBILITY_INSETS_CONSUMER);
mPrivOps.applyImeVisibilityAsync(setVisible
- ? mCurShowInputToken : mCurHideInputToken, setVisible);
+ ? mCurShowInputToken : mCurHideInputToken, setVisible, mCurStatsToken);
}
private void finishViews(boolean finishingInput) {
diff --git a/core/java/android/nfc/NfcAdapter.java b/core/java/android/nfc/NfcAdapter.java
index 64c1211..3282d56 100644
--- a/core/java/android/nfc/NfcAdapter.java
+++ b/core/java/android/nfc/NfcAdapter.java
@@ -29,7 +29,6 @@
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.content.IntentFilter;
-import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.nfc.tech.MifareClassic;
@@ -525,66 +524,6 @@
}
/**
- * Helper to check if this device has FEATURE_NFC_BEAM, but without using
- * a context.
- * Equivalent to
- * context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_NFC_BEAM)
- */
- private static boolean hasBeamFeature() {
- IPackageManager pm = ActivityThread.getPackageManager();
- if (pm == null) {
- Log.e(TAG, "Cannot get package manager, assuming no Android Beam feature");
- return false;
- }
- try {
- return pm.hasSystemFeature(PackageManager.FEATURE_NFC_BEAM, 0);
- } catch (RemoteException e) {
- Log.e(TAG, "Package manager query failed, assuming no Android Beam feature", e);
- return false;
- }
- }
-
- /**
- * Helper to check if this device has FEATURE_NFC, but without using
- * a context.
- * Equivalent to
- * context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_NFC)
- */
- private static boolean hasNfcFeature() {
- IPackageManager pm = ActivityThread.getPackageManager();
- if (pm == null) {
- Log.e(TAG, "Cannot get package manager, assuming no NFC feature");
- return false;
- }
- try {
- return pm.hasSystemFeature(PackageManager.FEATURE_NFC, 0);
- } catch (RemoteException e) {
- Log.e(TAG, "Package manager query failed, assuming no NFC feature", e);
- return false;
- }
- }
-
- /**
- * Helper to check if this device is NFC HCE capable, by checking for
- * FEATURE_NFC_HOST_CARD_EMULATION and/or FEATURE_NFC_HOST_CARD_EMULATION_NFCF,
- * but without using a context.
- */
- private static boolean hasNfcHceFeature() {
- IPackageManager pm = ActivityThread.getPackageManager();
- if (pm == null) {
- Log.e(TAG, "Cannot get package manager, assuming no NFC feature");
- return false;
- }
- try {
- return pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION, 0)
- || pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION_NFCF, 0);
- } catch (RemoteException e) {
- Log.e(TAG, "Package manager query failed, assuming no NFC feature", e);
- return false;
- }
- }
-
- /**
* Return list of Secure Elements which support off host card emulation.
*
* @return List<String> containing secure elements on the device which supports
@@ -593,23 +532,21 @@
* @hide
*/
public @NonNull List<String> getSupportedOffHostSecureElements() {
+ if (mContext == null) {
+ throw new UnsupportedOperationException("You need a context on NfcAdapter to use the "
+ + " getSupportedOffHostSecureElements APIs");
+ }
List<String> offHostSE = new ArrayList<String>();
- IPackageManager pm = ActivityThread.getPackageManager();
+ PackageManager pm = mContext.getPackageManager();
if (pm == null) {
Log.e(TAG, "Cannot get package manager, assuming no off-host CE feature");
return offHostSE;
}
- try {
- if (pm.hasSystemFeature(PackageManager.FEATURE_NFC_OFF_HOST_CARD_EMULATION_UICC, 0)) {
- offHostSE.add("SIM");
- }
- if (pm.hasSystemFeature(PackageManager.FEATURE_NFC_OFF_HOST_CARD_EMULATION_ESE, 0)) {
- offHostSE.add("eSE");
- }
- } catch (RemoteException e) {
- Log.e(TAG, "Package manager query failed, assuming no off-host CE feature", e);
- offHostSE.clear();
- return offHostSE;
+ if (pm.hasSystemFeature(PackageManager.FEATURE_NFC_OFF_HOST_CARD_EMULATION_UICC)) {
+ offHostSE.add("SIM");
+ }
+ if (pm.hasSystemFeature(PackageManager.FEATURE_NFC_OFF_HOST_CARD_EMULATION_ESE)) {
+ offHostSE.add("eSE");
}
return offHostSE;
}
@@ -621,10 +558,19 @@
*/
@UnsupportedAppUsage
public static synchronized NfcAdapter getNfcAdapter(Context context) {
+ if (context == null) {
+ if (sNullContextNfcAdapter == null) {
+ sNullContextNfcAdapter = new NfcAdapter(null);
+ }
+ return sNullContextNfcAdapter;
+ }
if (!sIsInitialized) {
- sHasNfcFeature = hasNfcFeature();
- sHasBeamFeature = hasBeamFeature();
- boolean hasHceFeature = hasNfcHceFeature();
+ PackageManager pm = context.getPackageManager();
+ sHasNfcFeature = pm.hasSystemFeature(PackageManager.FEATURE_NFC);
+ sHasBeamFeature = pm.hasSystemFeature(PackageManager.FEATURE_NFC_BEAM);
+ boolean hasHceFeature =
+ pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION)
+ || pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION_NFCF);
/* is this device meant to have NFC */
if (!sHasNfcFeature && !hasHceFeature) {
Log.v(TAG, "this device does not have NFC support");
@@ -660,12 +606,6 @@
sIsInitialized = true;
}
- if (context == null) {
- if (sNullContextNfcAdapter == null) {
- sNullContextNfcAdapter = new NfcAdapter(null);
- }
- return sNullContextNfcAdapter;
- }
NfcAdapter adapter = sNfcAdapters.get(context);
if (adapter == null) {
adapter = new NfcAdapter(context);
@@ -676,8 +616,12 @@
/** get handle to NFC service interface */
private static INfcAdapter getServiceInterface() {
+ if (!sHasNfcFeature) {
+ /* NFC is not supported */
+ return null;
+ }
/* get a handle to NFC service */
- IBinder b = ServiceManager.getService("nfc");
+ IBinder b = ServiceManager.waitForService("nfc");
if (b == null) {
return null;
}
@@ -707,6 +651,15 @@
"context not associated with any application (using a mock context?)");
}
+ synchronized (NfcAdapter.class) {
+ if (!sIsInitialized) {
+ PackageManager pm = context.getPackageManager();
+ sHasNfcFeature = pm.hasSystemFeature(PackageManager.FEATURE_NFC);
+ }
+ if (!sHasNfcFeature) {
+ return null;
+ }
+ }
if (getServiceInterface() == null) {
// NFC is not available
return null;
diff --git a/core/java/android/nfc/cardemulation/CardEmulation.java b/core/java/android/nfc/cardemulation/CardEmulation.java
index 0b56d19..6a42091 100644
--- a/core/java/android/nfc/cardemulation/CardEmulation.java
+++ b/core/java/android/nfc/cardemulation/CardEmulation.java
@@ -22,11 +22,9 @@
import android.annotation.SdkConstant;
import android.annotation.SdkConstant.SdkConstantType;
import android.app.Activity;
-import android.app.ActivityThread;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
-import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
import android.nfc.INfcCardEmulation;
import android.nfc.NfcAdapter;
@@ -158,18 +156,13 @@
throw new UnsupportedOperationException();
}
if (!sIsInitialized) {
- IPackageManager pm = ActivityThread.getPackageManager();
+ PackageManager pm = context.getPackageManager();
if (pm == null) {
Log.e(TAG, "Cannot get PackageManager");
throw new UnsupportedOperationException();
}
- try {
- if (!pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION, 0)) {
- Log.e(TAG, "This device does not support card emulation");
- throw new UnsupportedOperationException();
- }
- } catch (RemoteException e) {
- Log.e(TAG, "PackageManager query failed.");
+ if (!pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION)) {
+ Log.e(TAG, "This device does not support card emulation");
throw new UnsupportedOperationException();
}
sIsInitialized = true;
diff --git a/core/java/android/nfc/cardemulation/NfcFCardEmulation.java b/core/java/android/nfc/cardemulation/NfcFCardEmulation.java
index 3c92455..48bbf5b6 100644
--- a/core/java/android/nfc/cardemulation/NfcFCardEmulation.java
+++ b/core/java/android/nfc/cardemulation/NfcFCardEmulation.java
@@ -17,10 +17,8 @@
package android.nfc.cardemulation;
import android.app.Activity;
-import android.app.ActivityThread;
import android.content.ComponentName;
import android.content.Context;
-import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
import android.nfc.INfcFCardEmulation;
import android.nfc.NfcAdapter;
@@ -70,18 +68,13 @@
throw new UnsupportedOperationException();
}
if (!sIsInitialized) {
- IPackageManager pm = ActivityThread.getPackageManager();
+ PackageManager pm = context.getPackageManager();
if (pm == null) {
Log.e(TAG, "Cannot get PackageManager");
throw new UnsupportedOperationException();
}
- try {
- if (!pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION_NFCF, 0)) {
- Log.e(TAG, "This device does not support NFC-F card emulation");
- throw new UnsupportedOperationException();
- }
- } catch (RemoteException e) {
- Log.e(TAG, "PackageManager query failed.");
+ if (!pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION_NFCF)) {
+ Log.e(TAG, "This device does not support NFC-F card emulation");
throw new UnsupportedOperationException();
}
sIsInitialized = true;
diff --git a/core/java/android/os/FileUtils.java b/core/java/android/os/FileUtils.java
index d5c3de1..b478a379 100644
--- a/core/java/android/os/FileUtils.java
+++ b/core/java/android/os/FileUtils.java
@@ -84,7 +84,6 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
-import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
@@ -1314,31 +1313,31 @@
private static long toBytes(long value, String unit) {
unit = unit.toUpperCase();
- if (List.of("B").contains(unit)) {
+ if ("B".equals(unit)) {
return value;
}
- if (List.of("K", "KB").contains(unit)) {
+ if ("K".equals(unit) || "KB".equals(unit)) {
return DataUnit.KILOBYTES.toBytes(value);
}
- if (List.of("M", "MB").contains(unit)) {
+ if ("M".equals(unit) || "MB".equals(unit)) {
return DataUnit.MEGABYTES.toBytes(value);
}
- if (List.of("G", "GB").contains(unit)) {
+ if ("G".equals(unit) || "GB".equals(unit)) {
return DataUnit.GIGABYTES.toBytes(value);
}
- if (List.of("KI", "KIB").contains(unit)) {
+ if ("KI".equals(unit) || "KIB".equals(unit)) {
return DataUnit.KIBIBYTES.toBytes(value);
}
- if (List.of("MI", "MIB").contains(unit)) {
+ if ("MI".equals(unit) || "MIB".equals(unit)) {
return DataUnit.MEBIBYTES.toBytes(value);
}
- if (List.of("GI", "GIB").contains(unit)) {
+ if ("GI".equals(unit) || "GIB".equals(unit)) {
return DataUnit.GIBIBYTES.toBytes(value);
}
@@ -1370,7 +1369,7 @@
sign = -1;
}
- fmtSize = fmtSize.replace(first + "", "");
+ fmtSize = fmtSize.substring(1);
}
int index = 0;
diff --git a/core/java/android/os/IHintSession.aidl b/core/java/android/os/IHintSession.aidl
index 09bc4cc..0d1dde1 100644
--- a/core/java/android/os/IHintSession.aidl
+++ b/core/java/android/os/IHintSession.aidl
@@ -22,4 +22,5 @@
void updateTargetWorkDuration(long targetDurationNanos);
void reportActualWorkDuration(in long[] actualDurationNanos, in long[] timeStampNanos);
void close();
+ void sendHint(int hint);
}
diff --git a/core/java/android/os/PerformanceHintManager.java b/core/java/android/os/PerformanceHintManager.java
index a75b5ef..86135bc 100644
--- a/core/java/android/os/PerformanceHintManager.java
+++ b/core/java/android/os/PerformanceHintManager.java
@@ -16,6 +16,7 @@
package android.os;
+import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemService;
@@ -24,6 +25,10 @@
import com.android.internal.util.Preconditions;
import java.io.Closeable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.ref.Reference;
+
/** The PerformanceHintManager allows apps to send performance hint to system. */
@SystemService(Context.PERFORMANCE_HINT_SERVICE)
@@ -104,6 +109,40 @@
mNativeSessionPtr = nativeSessionPtr;
}
+ /**
+ * This hint indicates a sudden increase in CPU workload intensity. It means
+ * that this hint session needs extra CPU resources immediately to meet the
+ * target duration for the current work cycle.
+ */
+ public static final int CPU_LOAD_UP = 0;
+ /**
+ * This hint indicates a decrease in CPU workload intensity. It means that
+ * this hint session can reduce CPU resources and still meet the target duration.
+ */
+ public static final int CPU_LOAD_DOWN = 1;
+ /*
+ * This hint indicates an upcoming CPU workload that is completely changed and
+ * unknown. It means that the hint session should reset CPU resources to a known
+ * baseline to prepare for an arbitrary load, and must wake up if inactive.
+ */
+ public static final int CPU_LOAD_RESET = 2;
+ /*
+ * This hint indicates that the most recent CPU workload is resuming after a
+ * period of inactivity. It means that the hint session should allocate similar
+ * CPU resources to what was used previously, and must wake up if inactive.
+ */
+ public static final int CPU_LOAD_RESUME = 3;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = {"CPU_LOAD_"}, value = {
+ CPU_LOAD_UP,
+ CPU_LOAD_DOWN,
+ CPU_LOAD_RESET,
+ CPU_LOAD_RESUME
+ })
+ public @interface Hint {}
+
/** @hide */
@Override
protected void finalize() throws Throwable {
@@ -152,6 +191,21 @@
mNativeSessionPtr = 0;
}
}
+
+ /**
+ * Sends performance hints to inform the hint session of changes in the workload.
+ *
+ * @param hint The hint to send to the session.
+ */
+ public void sendHint(@Hint int hint) {
+ Preconditions.checkArgumentNonNegative(hint, "the hint ID should be at least"
+ + " zero.");
+ try {
+ nativeSendHint(mNativeSessionPtr, hint);
+ } finally {
+ Reference.reachabilityFence(this);
+ }
+ }
}
private static native long nativeAcquireManager();
@@ -163,4 +217,5 @@
private static native void nativeReportActualWorkDuration(long nativeSessionPtr,
long actualDurationNanos);
private static native void nativeCloseSession(long nativeSessionPtr);
+ private static native void nativeSendHint(long nativeSessionPtr, int hint);
}
diff --git a/core/java/android/os/SystemVibrator.java b/core/java/android/os/SystemVibrator.java
index 6091bf9..bf72b1d 100644
--- a/core/java/android/os/SystemVibrator.java
+++ b/core/java/android/os/SystemVibrator.java
@@ -21,6 +21,7 @@
import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
+import android.hardware.vibrator.IVibrator;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Range;
@@ -313,8 +314,14 @@
private static final float EPSILON = 1e-5f;
public MultiVibratorInfo(VibratorInfo[] vibrators) {
+ // Need to use an extra constructor to share the computation in super initialization.
+ this(vibrators, frequencyProfileIntersection(vibrators));
+ }
+
+ private MultiVibratorInfo(VibratorInfo[] vibrators,
+ VibratorInfo.FrequencyProfile mergedProfile) {
super(/* id= */ -1,
- capabilitiesIntersection(vibrators),
+ capabilitiesIntersection(vibrators, mergedProfile.isEmpty()),
supportedEffectsIntersection(vibrators),
supportedBrakingIntersection(vibrators),
supportedPrimitivesAndDurationsIntersection(vibrators),
@@ -323,14 +330,19 @@
integerLimitIntersection(vibrators, VibratorInfo::getPwlePrimitiveDurationMax),
integerLimitIntersection(vibrators, VibratorInfo::getPwleSizeMax),
floatPropertyIntersection(vibrators, VibratorInfo::getQFactor),
- frequencyProfileIntersection(vibrators));
+ mergedProfile);
}
- private static int capabilitiesIntersection(VibratorInfo[] infos) {
+ private static int capabilitiesIntersection(VibratorInfo[] infos,
+ boolean frequencyProfileIsEmpty) {
int intersection = ~0;
for (VibratorInfo info : infos) {
intersection &= info.getCapabilities();
}
+ if (frequencyProfileIsEmpty) {
+ // Revoke frequency control if the merged frequency profile ended up empty.
+ intersection &= ~IVibrator.CAP_FREQUENCY_CONTROL;
+ }
return intersection;
}
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index a38d9da..bda2a35 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -1506,6 +1506,30 @@
public static final String DISALLOW_CELLULAR_2G = "no_cellular_2g";
/**
+ * This user restriction specifies if Ultra-wideband is disallowed on the device. If
+ * Ultra-wideband is disallowed it cannot be turned on via Settings.
+ *
+ * <p>This restriction can only be set by a device owner or a profile owner of an
+ * organization-owned managed profile on the parent profile.
+ * In both cases, the restriction applies globally on the device and will turn off the
+ * ultra-wideband radio if it's currently on and prevent the radio from being turned on in
+ * the future.
+ *
+ * <p>
+ * Ultra-wideband (UWB) is a radio technology that can use a very low energy level
+ * for short-range, high-bandwidth communications over a large portion of the radio spectrum.
+ *
+ * <p>Default is <code>false</code>.
+ *
+ * <p>Key for user restrictions.
+ * <p>Type: Boolean
+ * @see DevicePolicyManager#addUserRestriction(ComponentName, String)
+ * @see DevicePolicyManager#clearUserRestriction(ComponentName, String)
+ * @see #getUserRestrictions()
+ */
+ public static final String DISALLOW_ULTRA_WIDEBAND_RADIO = "no_ultra_wideband_radio";
+
+ /**
* List of key values that can be passed into the various user restriction related methods
* in {@link UserManager} & {@link DevicePolicyManager}.
* Note: This is slightly different from the real set of user restrictions listed in {@link
@@ -1587,6 +1611,7 @@
DISALLOW_WIFI_DIRECT,
DISALLOW_ADD_WIFI_CONFIG,
DISALLOW_CELLULAR_2G,
+ DISALLOW_ULTRA_WIDEBAND_RADIO,
})
@Retention(RetentionPolicy.SOURCE)
public @interface UserRestrictionKey {}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index fab6f7b..52b1adb 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -192,6 +192,21 @@
"android.settings.LOCATION_SCANNING_SETTINGS";
/**
+ * Activity Action: Show settings to manage creation/deletion of cloned apps.
+ * <p>
+ * In some cases, a matching Activity may not exist, so ensure you
+ * safeguard against this.
+ * <p>
+ * Input: Nothing.
+ * <p>
+ * Output: Nothing.
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_MANAGE_CLONED_APPS_SETTINGS =
+ "android.settings.MANAGE_CLONED_APPS_SETTINGS";
+
+ /**
* Activity Action: Show settings to allow configuration of users.
* <p>
* In some cases, a matching Activity may not exist, so ensure you
@@ -675,6 +690,22 @@
"android.settings.WIFI_SETTINGS";
/**
+ * Activity Action: Show settings to allow configuration of MTE.
+ * <p>
+ * Memory Tagging Extension (MTE) is a CPU extension that allows to protect against certain
+ * classes of security problems at a small runtime performance cost overhead.
+ * <p>
+ * In some cases, a matching Activity may not exist, so ensure you safeguard against this.
+ * <p>
+ * Input: Nothing.
+ * <p>
+ * Output: Nothing.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_MEMTAG_SETTINGS =
+ "android.settings.MEMTAG_SETTINGS";
+
+ /**
* Activity Action: Show settings to allow configuration of a static IP
* address for Wi-Fi.
* <p>
@@ -10400,11 +10431,11 @@
public static final String QS_AUTO_ADDED_TILES = "qs_auto_tiles";
/**
- * The duration of timeout, in milliseconds, to switch from a non-primary user to the
- * primary user when the device is docked.
+ * The duration of timeout, in milliseconds, to switch from a non-Dock User to the
+ * Dock User when the device is docked.
* @hide
*/
- public static final String TIMEOUT_TO_USER_ZERO = "timeout_to_user_zero";
+ public static final String TIMEOUT_TO_DOCK_USER = "timeout_to_dock_user";
/**
* Backup manager behavioral parameters.
@@ -18643,6 +18674,9 @@
/**
* Activity Action: For system or preinstalled apps to show their {@link Activity} embedded
* in Settings app on large screen devices.
+ *
+ * Developers should resolve the Intent action before using it.
+ *
* <p>
* Input: {@link #EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_INTENT_URI} must be included to
* specify the intent for the activity which will be embedded in Settings app.
diff --git a/core/java/android/text/style/TextAppearanceSpan.java b/core/java/android/text/style/TextAppearanceSpan.java
index 85b7ae9..d61228b 100644
--- a/core/java/android/text/style/TextAppearanceSpan.java
+++ b/core/java/android/text/style/TextAppearanceSpan.java
@@ -149,7 +149,7 @@
}
mTextFontWeight = a.getInt(com.android.internal.R.styleable
- .TextAppearance_textFontWeight, -1);
+ .TextAppearance_textFontWeight, /*defValue*/ FontStyle.FONT_WEIGHT_UNSPECIFIED);
final String localeString = a.getString(com.android.internal.R.styleable
.TextAppearance_textLocale);
@@ -215,7 +215,7 @@
mTextColorLink = linkColor;
mTypeface = null;
- mTextFontWeight = -1;
+ mTextFontWeight = FontStyle.FONT_WEIGHT_UNSPECIFIED;
mTextLocales = null;
mShadowRadius = 0.0f;
@@ -359,8 +359,8 @@
}
/**
- * Returns the text font weight specified by this span, or <code>-1</code>
- * if it does not specify one.
+ * Returns the text font weight specified by this span, or
+ * <code>FontStyle.FONT_WEIGHT_UNSPECIFIED</code> if it does not specify one.
*/
public int getTextFontWeight() {
return mTextFontWeight;
diff --git a/core/java/android/view/IDisplayWindowInsetsController.aidl b/core/java/android/view/IDisplayWindowInsetsController.aidl
index 0769f12..91270d4 100644
--- a/core/java/android/view/IDisplayWindowInsetsController.aidl
+++ b/core/java/android/view/IDisplayWindowInsetsController.aidl
@@ -19,6 +19,7 @@
import android.content.ComponentName;
import android.view.InsetsSourceControl;
import android.view.InsetsState;
+import android.view.inputmethod.ImeTracker;
/**
* Singular controller of insets to use when there isn't another obvious controller available.
@@ -48,10 +49,10 @@
/**
* @see IWindow#showInsets
*/
- void showInsets(int types, boolean fromIme);
+ void showInsets(int types, boolean fromIme, in @nullable ImeTracker.Token statsToken);
/**
* @see IWindow#hideInsets
*/
- void hideInsets(int types, boolean fromIme);
+ void hideInsets(int types, boolean fromIme, in @nullable ImeTracker.Token statsToken);
}
diff --git a/core/java/android/view/IWindow.aidl b/core/java/android/view/IWindow.aidl
index a856474..8e16f24 100644
--- a/core/java/android/view/IWindow.aidl
+++ b/core/java/android/view/IWindow.aidl
@@ -29,6 +29,7 @@
import android.view.IScrollCaptureResponseListener;
import android.view.KeyEvent;
import android.view.MotionEvent;
+import android.view.inputmethod.ImeTracker;
import android.window.ClientWindowFrames;
import com.android.internal.os.IResultReceiver;
@@ -68,16 +69,18 @@
*
* @param types internal insets types (WindowInsets.Type.InsetsType) to show
* @param fromIme true if this request originated from IME (InputMethodService).
+ * @param statsToken the token tracking the current IME show request or {@code null} otherwise.
*/
- void showInsets(int types, boolean fromIme);
+ void showInsets(int types, boolean fromIme, in @nullable ImeTracker.Token statsToken);
/**
* Called when a set of insets source window should be hidden by policy.
*
* @param types internal insets types (WindowInsets.Type.InsetsType) to hide
* @param fromIme true if this request originated from IME (InputMethodService).
+ * @param statsToken the token tracking the current IME hide request or {@code null} otherwise.
*/
- void hideInsets(int types, boolean fromIme);
+ void hideInsets(int types, boolean fromIme, in @nullable ImeTracker.Token statsToken);
void moved(int newX, int newY);
void dispatchAppVisibility(boolean visible);
diff --git a/core/java/android/view/InsetsAnimationControlImpl.java b/core/java/android/view/InsetsAnimationControlImpl.java
index 27b4d87..e775969 100644
--- a/core/java/android/view/InsetsAnimationControlImpl.java
+++ b/core/java/android/view/InsetsAnimationControlImpl.java
@@ -58,6 +58,7 @@
import android.view.WindowInsetsAnimation.Bounds;
import android.view.WindowManager.LayoutParams;
import android.view.animation.Interpolator;
+import android.view.inputmethod.ImeTracker;
import com.android.internal.annotations.VisibleForTesting;
@@ -68,7 +69,7 @@
* Implements {@link WindowInsetsAnimationController}
* @hide
*/
-@VisibleForTesting
+@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public class InsetsAnimationControlImpl implements InternalInsetsAnimationController,
InsetsAnimationControlRunner {
@@ -96,6 +97,8 @@
/** @see WindowInsetsAnimationController#hasZeroInsetsIme */
private final boolean mHasZeroInsetsIme;
private final CompatibilityInfo.Translator mTranslator;
+ @Nullable
+ private final ImeTracker.Token mStatsToken;
private Insets mCurrentInsets;
private Insets mPendingInsets;
private float mPendingFraction;
@@ -114,7 +117,7 @@
@InsetsType int types, InsetsAnimationControlCallbacks controller, long durationMs,
Interpolator interpolator, @AnimationType int animationType,
@LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation,
- CompatibilityInfo.Translator translator) {
+ CompatibilityInfo.Translator translator, @Nullable ImeTracker.Token statsToken) {
mControls = controls;
mListener = listener;
mTypes = types;
@@ -152,6 +155,7 @@
mAnimationType = animationType;
mLayoutInsetsDuringAnimation = layoutInsetsDuringAnimation;
mTranslator = translator;
+ mStatsToken = statsToken;
mController.startAnimation(this, listener, types, mAnimation,
new Bounds(mHiddenInsets, mShownInsets));
}
@@ -228,6 +232,11 @@
}
@Override
+ public ImeTracker.Token getStatsToken() {
+ return mStatsToken;
+ }
+
+ @Override
public void setInsetsAndAlpha(Insets insets, float alpha, float fraction) {
setInsetsAndAlpha(insets, alpha, fraction, false /* allowWhenFinished */);
}
@@ -253,10 +262,10 @@
}
}
- @VisibleForTesting
/**
* @return Whether the finish callback of this animation should be invoked.
*/
+ @VisibleForTesting
public boolean applyChangeInsets(@Nullable InsetsState outState) {
if (mCancelled) {
if (DEBUG) Log.d(TAG, "applyChangeInsets canceled");
diff --git a/core/java/android/view/InsetsAnimationControlRunner.java b/core/java/android/view/InsetsAnimationControlRunner.java
index 291351e..cf40e7e 100644
--- a/core/java/android/view/InsetsAnimationControlRunner.java
+++ b/core/java/android/view/InsetsAnimationControlRunner.java
@@ -16,10 +16,12 @@
package android.view;
+import android.annotation.Nullable;
import android.util.SparseArray;
import android.util.proto.ProtoOutputStream;
import android.view.InsetsController.AnimationType;
import android.view.WindowInsets.Type.InsetsType;
+import android.view.inputmethod.ImeTracker;
/**
* Interface representing a runner for an insets animation.
@@ -74,6 +76,12 @@
@AnimationType int getAnimationType();
/**
+ * @return The token tracking the current IME request or {@code null} otherwise.
+ */
+ @Nullable
+ ImeTracker.Token getStatsToken();
+
+ /**
*
* Export the state of classes that implement this interface into a protocol buffer
* output stream.
diff --git a/core/java/android/view/InsetsAnimationThreadControlRunner.java b/core/java/android/view/InsetsAnimationThreadControlRunner.java
index fc97541..f7b9aa2 100644
--- a/core/java/android/view/InsetsAnimationThreadControlRunner.java
+++ b/core/java/android/view/InsetsAnimationThreadControlRunner.java
@@ -34,6 +34,7 @@
import android.view.WindowInsets.Type.InsetsType;
import android.view.WindowInsetsAnimation.Bounds;
import android.view.animation.Interpolator;
+import android.view.inputmethod.ImeTracker;
/**
* Insets animation runner that uses {@link InsetsAnimationThread} to run the animation off from the
@@ -112,12 +113,13 @@
@InsetsType int types, InsetsAnimationControlCallbacks controller, long durationMs,
Interpolator interpolator, @AnimationType int animationType,
@LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation,
- CompatibilityInfo.Translator translator, Handler mainThreadHandler) {
+ CompatibilityInfo.Translator translator, Handler mainThreadHandler,
+ @Nullable ImeTracker.Token statsToken) {
mMainThreadHandler = mainThreadHandler;
mOuterCallbacks = controller;
mControl = new InsetsAnimationControlImpl(controls, frame, state, listener, types,
mCallbacks, durationMs, interpolator, animationType, layoutInsetsDuringAnimation,
- translator);
+ translator, statsToken);
InsetsAnimationThread.getHandler().post(() -> {
if (mControl.isCancelled()) {
return;
@@ -141,6 +143,11 @@
}
@Override
+ public ImeTracker.Token getStatsToken() {
+ return mControl.getStatsToken();
+ }
+
+ @Override
@UiThread
public int getTypes() {
return mControl.getTypes();
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index 35838a3..fbd8226 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -44,6 +44,7 @@
import android.os.Handler;
import android.os.IBinder;
import android.os.Trace;
+import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
import android.util.Pair;
@@ -60,6 +61,7 @@
import android.view.animation.Interpolator;
import android.view.animation.LinearInterpolator;
import android.view.animation.PathInterpolator;
+import android.view.inputmethod.ImeTracker;
import android.view.inputmethod.InputMethodManager;
import com.android.internal.annotations.VisibleForTesting;
@@ -929,10 +931,12 @@
hideTypes[0] &= ~animatingTypes;
if (showTypes[0] != 0) {
- applyAnimation(showTypes[0], true /* show */, false /* fromIme */);
+ applyAnimation(showTypes[0], true /* show */, false /* fromIme */,
+ null /* statsToken */);
}
if (hideTypes[0] != 0) {
- applyAnimation(hideTypes[0], false /* show */, false /* fromIme */);
+ applyAnimation(hideTypes[0], false /* show */, false /* fromIme */,
+ null /* statsToken */);
}
if (mControllableTypes != controllableTypes) {
@@ -948,11 +952,12 @@
@Override
public void show(@InsetsType int types) {
- show(types, false /* fromIme */);
+ show(types, false /* fromIme */, null /* statsToken */);
}
- @VisibleForTesting
- public void show(@InsetsType int types, boolean fromIme) {
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public void show(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
if ((types & ime()) != 0) {
Log.d(TAG, "show(ime(), fromIme=" + fromIme + ")");
}
@@ -979,7 +984,7 @@
true /* fromIme */, pendingRequest.durationMs, pendingRequest.interpolator,
pendingRequest.animationType,
pendingRequest.layoutInsetsDuringAnimation,
- pendingRequest.useInsetsAnimationThread);
+ pendingRequest.useInsetsAnimationThread, statsToken);
return;
}
@@ -990,8 +995,9 @@
if ((types & type) == 0) {
continue;
}
- final @AnimationType int animationType = getAnimationType(type);
+ @AnimationType final int animationType = getAnimationType(type);
final boolean requestedVisible = (type & mRequestedVisibleTypes) != 0;
+ final boolean isImeAnimation = type == ime();
if (requestedVisible && animationType == ANIMATION_TYPE_NONE
|| animationType == ANIMATION_TYPE_SHOW) {
// no-op: already shown or animating in (because window visibility is
@@ -999,25 +1005,36 @@
if (DEBUG) Log.d(TAG, String.format(
"show ignored for type: %d animType: %d requestedVisible: %s",
type, animationType, requestedVisible));
+ if (isImeAnimation) {
+ ImeTracker.get().onCancelled(statsToken,
+ ImeTracker.PHASE_CLIENT_APPLY_ANIMATION);
+ }
continue;
}
if (fromIme && animationType == ANIMATION_TYPE_USER) {
// App is already controlling the IME, don't cancel it.
+ if (isImeAnimation) {
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_CLIENT_APPLY_ANIMATION);
+ }
continue;
}
+ if (isImeAnimation) {
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_APPLY_ANIMATION);
+ }
typesReady |= type;
}
if (DEBUG) Log.d(TAG, "show typesReady: " + typesReady);
- applyAnimation(typesReady, true /* show */, fromIme);
+ applyAnimation(typesReady, true /* show */, fromIme, statsToken);
}
@Override
public void hide(@InsetsType int types) {
- hide(types, false /* fromIme */);
+ hide(types, false /* fromIme */, null /* statsToken */);
}
@VisibleForTesting
- public void hide(@InsetsType int types, boolean fromIme) {
+ public void hide(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
if (fromIme) {
ImeTracing.getInstance().triggerClientDump("InsetsController#hide",
mHost.getInputMethodManager(), null /* icProto */);
@@ -1030,16 +1047,25 @@
if ((types & type) == 0) {
continue;
}
- final @AnimationType int animationType = getAnimationType(type);
+ @AnimationType final int animationType = getAnimationType(type);
final boolean requestedVisible = (type & mRequestedVisibleTypes) != 0;
+ final boolean isImeAnimation = type == ime();
if (!requestedVisible && animationType == ANIMATION_TYPE_NONE
|| animationType == ANIMATION_TYPE_HIDE) {
- // no-op: already hidden or animating out.
+ // no-op: already hidden or animating out (because window visibility is
+ // applied before starting animation).
+ if (isImeAnimation) {
+ ImeTracker.get().onCancelled(statsToken,
+ ImeTracker.PHASE_CLIENT_APPLY_ANIMATION);
+ }
continue;
}
+ if (isImeAnimation) {
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_APPLY_ANIMATION);
+ }
typesReady |= type;
}
- applyAnimation(typesReady, false /* show */, fromIme /* fromIme */);
+ applyAnimation(typesReady, false /* show */, fromIme, statsToken);
}
@Override
@@ -1068,7 +1094,7 @@
controlAnimationUnchecked(types, cancellationSignal, listener, mFrame, fromIme, durationMs,
interpolator, animationType, getLayoutInsetsDuringAnimationMode(types),
- false /* useInsetsAnimationThread */);
+ false /* useInsetsAnimationThread */, null /* statsToken */);
}
private void controlAnimationUnchecked(@InsetsType int types,
@@ -1077,7 +1103,8 @@
long durationMs, Interpolator interpolator,
@AnimationType int animationType,
@LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation,
- boolean useInsetsAnimationThread) {
+ boolean useInsetsAnimationThread, @Nullable ImeTracker.Token statsToken) {
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_CONTROL_ANIMATION);
if ((types & mTypesBeingCancelled) != 0) {
throw new IllegalStateException("Cannot start a new insets animation of "
+ Type.toString(types)
@@ -1152,14 +1179,16 @@
? new InsetsAnimationThreadControlRunner(controls,
frame, mState, listener, typesReady, this, durationMs, interpolator,
animationType, layoutInsetsDuringAnimation, mHost.getTranslator(),
- mHost.getHandler())
+ mHost.getHandler(), statsToken)
: new InsetsAnimationControlImpl(controls,
frame, mState, listener, typesReady, this, durationMs, interpolator,
- animationType, layoutInsetsDuringAnimation, mHost.getTranslator());
+ animationType, layoutInsetsDuringAnimation, mHost.getTranslator(),
+ statsToken);
if ((typesReady & WindowInsets.Type.ime()) != 0) {
ImeTracing.getInstance().triggerClientDump("InsetsAnimationControlImpl",
mHost.getInputMethodManager(), null /* icProto */);
}
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_ANIMATION_RUNNING);
mRunningAnimations.add(new RunningAnimation(runner, animationType));
if (DEBUG) Log.d(TAG, "Animation added to runner. useInsetsAnimationThread: "
+ useInsetsAnimationThread);
@@ -1311,11 +1340,18 @@
// requested visibility.
return;
}
+ final ImeTracker.Token statsToken = runner.getStatsToken();
if (shown) {
+ ImeTracker.get().onProgress(statsToken,
+ ImeTracker.PHASE_CLIENT_ANIMATION_FINISHED_SHOW);
showDirectly(runner.getTypes(), true /* fromIme */);
+ ImeTracker.get().onShown(statsToken);
} else {
+ ImeTracker.get().onProgress(statsToken,
+ ImeTracker.PHASE_CLIENT_ANIMATION_FINISHED_HIDE);
hideDirectly(runner.getTypes(), true /* animationFinished */,
runner.getAnimationType(), true /* fromIme */);
+ ImeTracker.get().onHidden(statsToken);
}
}
@@ -1339,10 +1375,19 @@
}
private void cancelAnimation(InsetsAnimationControlRunner control, boolean invokeCallback) {
- if (DEBUG) Log.d(TAG, String.format("cancelAnimation of types: %d, animType: %d, host: %s",
- control.getTypes(), control.getAnimationType(), mHost.getRootViewTitle()));
if (invokeCallback) {
+ ImeTracker.get().onCancelled(control.getStatsToken(),
+ ImeTracker.PHASE_CLIENT_ANIMATION_CANCEL);
control.cancel();
+ } else {
+ // Succeeds if invokeCallback is false (i.e. when called from notifyFinished).
+ ImeTracker.get().onProgress(control.getStatsToken(),
+ ImeTracker.PHASE_CLIENT_ANIMATION_CANCEL);
+ }
+ if (DEBUG) {
+ Log.d(TAG, TextUtils.formatSimple(
+ "cancelAnimation of types: %d, animType: %d, host: %s",
+ control.getTypes(), control.getAnimationType(), mHost.getRootViewTitle()));
}
boolean stateChanged = false;
for (int i = mRunningAnimations.size() - 1; i >= 0; i--) {
@@ -1452,7 +1497,8 @@
}
@VisibleForTesting
- public void applyAnimation(@InsetsType final int types, boolean show, boolean fromIme) {
+ public void applyAnimation(@InsetsType final int types, boolean show, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
// TODO(b/166736352): We should only skip the animation of specific types, not all types.
boolean skipAnim = false;
if ((types & ime()) != 0) {
@@ -1465,12 +1511,12 @@
&& consumer.hasViewFocusWhenWindowFocusGain();
}
}
- applyAnimation(types, show, fromIme, skipAnim);
+ applyAnimation(types, show, fromIme, skipAnim, statsToken);
}
@VisibleForTesting
public void applyAnimation(@InsetsType final int types, boolean show, boolean fromIme,
- boolean skipAnim) {
+ boolean skipAnim, @Nullable ImeTracker.Token statsToken) {
if (types == 0) {
// nothing to animate.
if (DEBUG) Log.d(TAG, "applyAnimation, nothing to animate");
@@ -1490,12 +1536,11 @@
listener.getDurationMs(), listener.getInsetsInterpolator(),
show ? ANIMATION_TYPE_SHOW : ANIMATION_TYPE_HIDE,
show ? LAYOUT_INSETS_DURING_ANIMATION_SHOWN : LAYOUT_INSETS_DURING_ANIMATION_HIDDEN,
- !hasAnimationCallbacks /* useInsetsAnimationThread */);
+ !hasAnimationCallbacks /* useInsetsAnimationThread */, statsToken);
}
- private void hideDirectly(
- @InsetsType int types, boolean animationFinished, @AnimationType int animationType,
- boolean fromIme) {
+ private void hideDirectly(@InsetsType int types, boolean animationFinished,
+ @AnimationType int animationType, boolean fromIme) {
if ((types & ime()) != 0) {
ImeTracing.getInstance().triggerClientDump("InsetsController#hideDirectly",
mHost.getInputMethodManager(), null /* icProto */);
diff --git a/core/java/android/view/InsetsResizeAnimationRunner.java b/core/java/android/view/InsetsResizeAnimationRunner.java
index edcfc95..778c677 100644
--- a/core/java/android/view/InsetsResizeAnimationRunner.java
+++ b/core/java/android/view/InsetsResizeAnimationRunner.java
@@ -37,6 +37,7 @@
import android.view.WindowInsets.Type.InsetsType;
import android.view.WindowInsetsAnimation.Bounds;
import android.view.animation.Interpolator;
+import android.view.inputmethod.ImeTracker;
/**
* Runs a fake animation of resizing insets to produce insets animation callbacks.
@@ -92,6 +93,12 @@
}
@Override
+ public ImeTracker.Token getStatsToken() {
+ // Return null as resizing the IME view is not explicitly tracked.
+ return null;
+ }
+
+ @Override
public void cancel() {
if (mCancelled || mFinished) {
return;
diff --git a/core/java/android/view/InsetsState.java b/core/java/android/view/InsetsState.java
index e91839b..a8cc9b6 100644
--- a/core/java/android/view/InsetsState.java
+++ b/core/java/android/view/InsetsState.java
@@ -702,7 +702,7 @@
result.add(ITYPE_NAVIGATION_BAR);
result.add(ITYPE_EXTRA_NAVIGATION_BAR);
}
- if ((types & Type.GENERIC_OVERLAYS) != 0) {
+ if ((types & Type.SYSTEM_OVERLAYS) != 0) {
result.add(ITYPE_LEFT_GENERIC_OVERLAY);
result.add(ITYPE_TOP_GENERIC_OVERLAY);
result.add(ITYPE_RIGHT_GENERIC_OVERLAY);
@@ -752,7 +752,7 @@
case ITYPE_TOP_GENERIC_OVERLAY:
case ITYPE_RIGHT_GENERIC_OVERLAY:
case ITYPE_BOTTOM_GENERIC_OVERLAY:
- return Type.GENERIC_OVERLAYS;
+ return Type.SYSTEM_OVERLAYS;
case ITYPE_CAPTION_BAR:
return Type.CAPTION_BAR;
case ITYPE_IME:
diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java
index f51d9ba..58aee61 100644
--- a/core/java/android/view/ViewConfiguration.java
+++ b/core/java/android/view/ViewConfiguration.java
@@ -822,6 +822,7 @@
*
* @hide
*/
+ @TestApi
public static long getSendRecurringAccessibilityEventsInterval() {
return SEND_RECURRING_ACCESSIBILITY_EVENTS_INTERVAL_MILLIS;
}
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index e664ebf..63602393 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -192,6 +192,7 @@
import android.view.contentcapture.ContentCaptureManager;
import android.view.contentcapture.ContentCaptureSession;
import android.view.contentcapture.MainContentCaptureSession;
+import android.view.inputmethod.ImeTracker;
import android.view.inputmethod.InputMethodManager;
import android.widget.Scroller;
import android.window.ClientWindowFrames;
@@ -711,7 +712,7 @@
private final InsetsState mTempInsets = new InsetsState();
private final InsetsSourceControl[] mTempControls = new InsetsSourceControl[SIZE];
private final WindowConfiguration mTempWinConfig = new WindowConfiguration();
- private float mInvSizeCompatScale = 1f;
+ private float mInvCompatScale = 1f;
final ViewTreeObserver.InternalInsetsInfo mLastGivenInsets
= new ViewTreeObserver.InternalInsetsInfo();
@@ -1105,11 +1106,11 @@
private WindowConfiguration getCompatWindowConfiguration() {
final WindowConfiguration winConfig = getConfiguration().windowConfiguration;
- if (mInvSizeCompatScale == 1f) {
+ if (mInvCompatScale == 1f) {
return winConfig;
}
mTempWinConfig.setTo(winConfig);
- mTempWinConfig.scale(mInvSizeCompatScale);
+ mTempWinConfig.scale(mInvCompatScale);
return mTempWinConfig;
}
@@ -1241,11 +1242,11 @@
controlInsetsForCompatibility(mWindowAttributes);
Rect attachedFrame = new Rect();
- final float[] sizeCompatScale = { 1f };
+ final float[] compatScale = { 1f };
res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), userId,
mInsetsController.getRequestedVisibleTypes(), inputChannel, mTempInsets,
- mTempControls, attachedFrame, sizeCompatScale);
+ mTempControls, attachedFrame, compatScale);
if (!attachedFrame.isValid()) {
attachedFrame = null;
}
@@ -1255,8 +1256,8 @@
mTranslator.translateRectInScreenToAppWindow(attachedFrame);
}
mTmpFrames.attachedFrame = attachedFrame;
- mTmpFrames.sizeCompatScale = sizeCompatScale[0];
- mInvSizeCompatScale = 1f / sizeCompatScale[0];
+ mTmpFrames.compatScale = compatScale[0];
+ mInvCompatScale = 1f / compatScale[0];
} catch (RemoteException | RuntimeException e) {
mAdded = false;
mView = null;
@@ -1787,24 +1788,24 @@
mTranslator.translateRectInScreenToAppWindow(displayFrame);
mTranslator.translateRectInScreenToAppWindow(attachedFrame);
}
- final float sizeCompatScale = frames.sizeCompatScale;
+ final float compatScale = frames.compatScale;
final boolean frameChanged = !mWinFrame.equals(frame);
final boolean configChanged = !mLastReportedMergedConfiguration.equals(mergedConfiguration);
final boolean attachedFrameChanged = LOCAL_LAYOUT
&& !Objects.equals(mTmpFrames.attachedFrame, attachedFrame);
final boolean displayChanged = mDisplay.getDisplayId() != displayId;
final boolean resizeModeChanged = mResizeMode != resizeMode;
- final boolean sizeCompatScaleChanged = mTmpFrames.sizeCompatScale != sizeCompatScale;
+ final boolean compatScaleChanged = mTmpFrames.compatScale != compatScale;
if (msg == MSG_RESIZED && !frameChanged && !configChanged && !attachedFrameChanged
&& !displayChanged && !resizeModeChanged && !forceNextWindowRelayout
- && !sizeCompatScaleChanged) {
+ && !compatScaleChanged) {
return;
}
mPendingDragResizing = resizeMode != RESIZE_MODE_INVALID;
mResizeMode = resizeMode;
- mTmpFrames.sizeCompatScale = sizeCompatScale;
- mInvSizeCompatScale = 1f / sizeCompatScale;
+ mTmpFrames.compatScale = compatScale;
+ mInvCompatScale = 1f / compatScale;
if (configChanged) {
// If configuration changed - notify about that and, maybe, about move to display.
@@ -5649,17 +5650,23 @@
break;
}
case MSG_SHOW_INSETS: {
+ final ImeTracker.Token statsToken = (ImeTracker.Token) msg.obj;
+ ImeTracker.get().onProgress(statsToken,
+ ImeTracker.PHASE_CLIENT_HANDLE_SHOW_INSETS);
if (mView == null) {
Log.e(TAG,
String.format("Calling showInsets(%d,%b) on window that no longer"
+ " has views.", msg.arg1, msg.arg2 == 1));
}
clearLowProfileModeIfNeeded(msg.arg1, msg.arg2 == 1);
- mInsetsController.show(msg.arg1, msg.arg2 == 1);
+ mInsetsController.show(msg.arg1, msg.arg2 == 1, statsToken);
break;
}
case MSG_HIDE_INSETS: {
- mInsetsController.hide(msg.arg1, msg.arg2 == 1);
+ final ImeTracker.Token statsToken = (ImeTracker.Token) msg.obj;
+ ImeTracker.get().onProgress(statsToken,
+ ImeTracker.PHASE_CLIENT_HANDLE_HIDE_INSETS);
+ mInsetsController.hide(msg.arg1, msg.arg2 == 1, statsToken);
break;
}
case MSG_WINDOW_MOVED:
@@ -8225,7 +8232,7 @@
mTranslator.translateInsetsStateInScreenToAppWindow(mTempInsets);
mTranslator.translateSourceControlsInScreenToAppWindow(mTempControls);
}
- mInvSizeCompatScale = 1f / mTmpFrames.sizeCompatScale;
+ mInvCompatScale = 1f / mTmpFrames.compatScale;
CompatibilityInfo.applyOverrideScaleIfNeeded(mPendingMergedConfiguration);
mInsetsController.onStateChanged(mTempInsets);
mInsetsController.onControlsChanged(mTempControls);
@@ -8811,12 +8818,14 @@
mHandler.obtainMessage(MSG_INSETS_CONTROL_CHANGED, args).sendToTarget();
}
- private void showInsets(@InsetsType int types, boolean fromIme) {
- mHandler.obtainMessage(MSG_SHOW_INSETS, types, fromIme ? 1 : 0).sendToTarget();
+ private void showInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
+ mHandler.obtainMessage(MSG_SHOW_INSETS, types, fromIme ? 1 : 0, statsToken).sendToTarget();
}
- private void hideInsets(@InsetsType int types, boolean fromIme) {
- mHandler.obtainMessage(MSG_HIDE_INSETS, types, fromIme ? 1 : 0).sendToTarget();
+ private void hideInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
+ mHandler.obtainMessage(MSG_HIDE_INSETS, types, fromIme ? 1 : 0, statsToken).sendToTarget();
}
public void dispatchMoved(int newX, int newY) {
@@ -10180,7 +10189,8 @@
}
@Override
- public void showInsets(@InsetsType int types, boolean fromIme) {
+ public void showInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
final ViewRootImpl viewAncestor = mViewAncestor.get();
if (fromIme) {
ImeTracing.getInstance().triggerClientDump("ViewRootImpl.W#showInsets",
@@ -10188,13 +10198,16 @@
null /* icProto */);
}
if (viewAncestor != null) {
- viewAncestor.showInsets(types, fromIme);
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_SHOW_INSETS);
+ viewAncestor.showInsets(types, fromIme, statsToken);
+ } else {
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_CLIENT_SHOW_INSETS);
}
}
@Override
- public void hideInsets(@InsetsType int types, boolean fromIme) {
-
+ public void hideInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
final ViewRootImpl viewAncestor = mViewAncestor.get();
if (fromIme) {
ImeTracing.getInstance().triggerClientDump("ViewRootImpl.W#hideInsets",
@@ -10202,7 +10215,10 @@
null /* icProto */);
}
if (viewAncestor != null) {
- viewAncestor.hideInsets(types, fromIme);
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_HIDE_INSETS);
+ viewAncestor.hideInsets(types, fromIme, statsToken);
+ } else {
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_CLIENT_HIDE_INSETS);
}
}
diff --git a/core/java/android/view/WindowInsets.java b/core/java/android/view/WindowInsets.java
index 2a76c4e..d77e499 100644
--- a/core/java/android/view/WindowInsets.java
+++ b/core/java/android/view/WindowInsets.java
@@ -1425,8 +1425,8 @@
static final int WINDOW_DECOR = 1 << 8;
- static final int GENERIC_OVERLAYS = 1 << 9;
- static final int LAST = GENERIC_OVERLAYS;
+ static final int SYSTEM_OVERLAYS = 1 << 9;
+ static final int LAST = SYSTEM_OVERLAYS;
static final int SIZE = 10;
static final int DEFAULT_VISIBLE = ~IME;
@@ -1451,7 +1451,7 @@
return 7;
case WINDOW_DECOR:
return 8;
- case GENERIC_OVERLAYS:
+ case SYSTEM_OVERLAYS:
return 9;
default:
throw new IllegalArgumentException("type needs to be >= FIRST and <= LAST,"
@@ -1489,8 +1489,8 @@
if ((types & WINDOW_DECOR) != 0) {
result.append("windowDecor |");
}
- if ((types & GENERIC_OVERLAYS) != 0) {
- result.append("genericOverlays |");
+ if ((types & SYSTEM_OVERLAYS) != 0) {
+ result.append("systemOverlays |");
}
if (result.length() > 0) {
result.delete(result.length() - 2, result.length());
@@ -1505,7 +1505,7 @@
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true, value = {STATUS_BARS, NAVIGATION_BARS, CAPTION_BAR, IME, WINDOW_DECOR,
SYSTEM_GESTURES, MANDATORY_SYSTEM_GESTURES, TAPPABLE_ELEMENT, DISPLAY_CUTOUT,
- GENERIC_OVERLAYS})
+ SYSTEM_OVERLAYS})
public @interface InsetsType {
}
@@ -1593,11 +1593,27 @@
}
/**
+ * System overlays represent the insets caused by the system visible elements. Unlike
+ * {@link #navigationBars()} or {@link #statusBars()}, system overlays might not be
+ * hidden by the client.
+ *
+ * For compatibility reasons, this type is included in {@link #systemBars()}. In this
+ * way, views which fit {@link #systemBars()} fit {@link #systemOverlays()}.
+ *
+ * Examples include climate controls, multi-tasking affordances, etc.
+ *
+ * @return An insets type representing the system overlays.
+ */
+ public static @InsetsType int systemOverlays() {
+ return SYSTEM_OVERLAYS;
+ }
+
+ /**
* @return All system bars. Includes {@link #statusBars()}, {@link #captionBar()} as well as
- * {@link #navigationBars()}, but not {@link #ime()}.
+ * {@link #navigationBars()}, {@link #systemOverlays()}, but not {@link #ime()}.
*/
public static @InsetsType int systemBars() {
- return STATUS_BARS | NAVIGATION_BARS | CAPTION_BAR | GENERIC_OVERLAYS;
+ return STATUS_BARS | NAVIGATION_BARS | CAPTION_BAR | SYSTEM_OVERLAYS;
}
/**
diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
index 9dbabab..88adb2e 100644
--- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java
+++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
@@ -127,6 +127,16 @@
/** @hide */
public static final long UNDEFINED_NODE_ID = makeNodeId(UNDEFINED_ITEM_ID, UNDEFINED_ITEM_ID);
+ /**
+ * The default value for {@link #getMinMillisBetweenContentChanges};
+ */
+ public static final int UNDEFINED_MIN_MILLIS_BETWEEN_CONTENT_CHANGES = -1;
+
+ /**
+ * The minimum value for {@link #setMinMillisBetweenContentChanges};
+ */
+ public static final int MINIMUM_MIN_MILLIS_BETWEEN_CONTENT_CHANGES = 100;
+
/** @hide */
public static final long ROOT_NODE_ID = makeNodeId(ROOT_ITEM_ID,
AccessibilityNodeProvider.HOST_VIEW_ID);
@@ -879,6 +889,9 @@
private long mTraversalBefore = UNDEFINED_NODE_ID;
private long mTraversalAfter = UNDEFINED_NODE_ID;
+ private int mMinMillisBetweenContentChanges =
+ UNDEFINED_MIN_MILLIS_BETWEEN_CONTENT_CHANGES;
+
private int mBooleanProperties;
private final Rect mBoundsInParent = new Rect();
private final Rect mBoundsInScreen = new Rect();
@@ -1782,6 +1795,42 @@
}
/**
+ * Sets the minimum time duration between two content change events, which is used in throttling
+ * content change events in accessibility services.
+ *
+ * <p>
+ * <strong>Note:</strong>
+ * This value should not be smaller than {@link #MINIMUM_MIN_MILLIS_BETWEEN_CONTENT_CHANGES},
+ * otherwise it would be ignored by accessibility services.
+ * </p>
+ *
+ * <p>
+ * Example: An app can set MinMillisBetweenContentChanges as 1 min for a view which sends
+ * content change events to accessibility services one event per second.
+ * Accessibility service will throttle those content change events and only handle one event
+ * per minute for that view.
+ * </p>
+ *
+ * @see AccessibilityEvent#getContentChangeTypes for all content change types.
+ * @param minMillisBetweenContentChanges the minimum duration between content change events.
+ */
+ public void setMinMillisBetweenContentChanges(int minMillisBetweenContentChanges) {
+ enforceNotSealed();
+ mMinMillisBetweenContentChanges = minMillisBetweenContentChanges
+ >= MINIMUM_MIN_MILLIS_BETWEEN_CONTENT_CHANGES
+ ? minMillisBetweenContentChanges
+ : UNDEFINED_MIN_MILLIS_BETWEEN_CONTENT_CHANGES;
+ }
+
+ /**
+ * Gets the minimum time duration between two content change events. This method may return
+ * {@link #UNDEFINED_MIN_MILLIS_BETWEEN_CONTENT_CHANGES}
+ */
+ public int getMinMillisBetweenContentChanges() {
+ return mMinMillisBetweenContentChanges;
+ }
+
+ /**
* Performs an action on the node.
* <p>
* <strong>Note:</strong> An action can be performed only if the request is made
@@ -3951,6 +4000,11 @@
fieldIndex++;
if (mTraversalAfter != DEFAULT.mTraversalAfter) nonDefaultFields |= bitAt(fieldIndex);
fieldIndex++;
+ if (mMinMillisBetweenContentChanges
+ != DEFAULT.mMinMillisBetweenContentChanges) {
+ nonDefaultFields |= bitAt(fieldIndex);
+ }
+ fieldIndex++;
if (mConnectionId != DEFAULT.mConnectionId) nonDefaultFields |= bitAt(fieldIndex);
fieldIndex++;
if (!LongArray.elementsEqual(mChildNodeIds, DEFAULT.mChildNodeIds)) {
@@ -4080,6 +4134,9 @@
if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeLong(mLabeledById);
if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeLong(mTraversalBefore);
if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeLong(mTraversalAfter);
+ if (isBitSet(nonDefaultFields, fieldIndex++)) {
+ parcel.writeInt(mMinMillisBetweenContentChanges);
+ }
if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeInt(mConnectionId);
@@ -4235,6 +4292,7 @@
mLabeledById = other.mLabeledById;
mTraversalBefore = other.mTraversalBefore;
mTraversalAfter = other.mTraversalAfter;
+ mMinMillisBetweenContentChanges = other.mMinMillisBetweenContentChanges;
mWindowId = other.mWindowId;
mConnectionId = other.mConnectionId;
mUniqueId = other.mUniqueId;
@@ -4338,6 +4396,9 @@
if (isBitSet(nonDefaultFields, fieldIndex++)) mLabeledById = parcel.readLong();
if (isBitSet(nonDefaultFields, fieldIndex++)) mTraversalBefore = parcel.readLong();
if (isBitSet(nonDefaultFields, fieldIndex++)) mTraversalAfter = parcel.readLong();
+ if (isBitSet(nonDefaultFields, fieldIndex++)) {
+ mMinMillisBetweenContentChanges = parcel.readInt();
+ }
if (isBitSet(nonDefaultFields, fieldIndex++)) mConnectionId = parcel.readInt();
@@ -4686,6 +4747,8 @@
builder.append("; mParentNodeId: 0x").append(Long.toHexString(mParentNodeId));
builder.append("; traversalBefore: 0x").append(Long.toHexString(mTraversalBefore));
builder.append("; traversalAfter: 0x").append(Long.toHexString(mTraversalAfter));
+ builder.append("; minMillisBetweenContentChanges: ")
+ .append(mMinMillisBetweenContentChanges);
int granularities = mMovementGranularities;
builder.append("; MovementGranularities: [");
diff --git a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
index a66c67b..6eae63a 100644
--- a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
+++ b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
@@ -275,15 +275,16 @@
@AnyThread
static boolean showSoftInput(@NonNull IInputMethodClient client, @Nullable IBinder windowToken,
- int flags, int lastClickToolType, @Nullable ResultReceiver resultReceiver,
+ @Nullable ImeTracker.Token statsToken, int flags, int lastClickToolType,
+ @Nullable ResultReceiver resultReceiver,
@SoftInputShowHideReason int reason) {
final IInputMethodManager service = getService();
if (service == null) {
return false;
}
try {
- return service.showSoftInput(
- client, windowToken, flags, lastClickToolType, resultReceiver, reason);
+ return service.showSoftInput(client, windowToken, statsToken, flags, lastClickToolType,
+ resultReceiver, reason);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -291,14 +292,15 @@
@AnyThread
static boolean hideSoftInput(@NonNull IInputMethodClient client, @Nullable IBinder windowToken,
- int flags, @Nullable ResultReceiver resultReceiver,
- @SoftInputShowHideReason int reason) {
+ @Nullable ImeTracker.Token statsToken, int flags,
+ @Nullable ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
final IInputMethodManager service = getService();
if (service == null) {
return false;
}
try {
- return service.hideSoftInput(client, windowToken, flags, resultReceiver, reason);
+ return service.hideSoftInput(client, windowToken, statsToken, flags, resultReceiver,
+ reason);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/core/java/android/view/inputmethod/ImeTracker.aidl b/core/java/android/view/inputmethod/ImeTracker.aidl
new file mode 100644
index 0000000..1988f48
--- /dev/null
+++ b/core/java/android/view/inputmethod/ImeTracker.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.view.inputmethod;
+
+parcelable ImeTracker.Token;
diff --git a/core/java/android/view/inputmethod/ImeTracker.java b/core/java/android/view/inputmethod/ImeTracker.java
new file mode 100644
index 0000000..f4ecdff
--- /dev/null
+++ b/core/java/android/view/inputmethod/ImeTracker.java
@@ -0,0 +1,488 @@
+/*
+ * 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 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;
+import android.app.ActivityThread;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemProperties;
+import android.util.Log;
+
+import com.android.internal.inputmethod.InputMethodDebug;
+import com.android.internal.inputmethod.SoftInputShowHideReason;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Random;
+import java.util.stream.Collectors;
+
+/** @hide */
+public interface ImeTracker {
+
+ String TAG = "ImeTracker";
+
+ /**
+ * The origin of the IME request
+ *
+ * The name follows the format {@code PHASE_x_...} where {@code x} denotes
+ * where the origin is (i.e. {@code PHASE_SERVER_...} occurs in the server).
+ */
+ @IntDef(prefix = { "ORIGIN_" }, value = {
+ ORIGIN_CLIENT_SHOW_SOFT_INPUT,
+ ORIGIN_CLIENT_HIDE_SOFT_INPUT,
+ ORIGIN_SERVER_START_INPUT,
+ ORIGIN_SERVER_HIDE_INPUT
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface Origin {}
+
+ /**
+ * The IME show request originated in the client.
+ */
+ int ORIGIN_CLIENT_SHOW_SOFT_INPUT = 0;
+
+ /**
+ * The IME hide request originated in the client.
+ */
+ int ORIGIN_CLIENT_HIDE_SOFT_INPUT = 1;
+
+ /**
+ * The IME show request originated in the server.
+ */
+ int ORIGIN_SERVER_START_INPUT = 2;
+
+ /**
+ * The IME hide request originated in the server.
+ */
+ int ORIGIN_SERVER_HIDE_INPUT = 3;
+
+ /**
+ * The current phase of the IME request.
+ *
+ * The name follows the format {@code PHASE_x_...} where {@code x} denotes
+ * where the phase is (i.e. {@code PHASE_SERVER_...} occurs in the server).
+ */
+ @IntDef(prefix = { "PHASE_" }, value = {
+ PHASE_CLIENT_VIEW_SERVED,
+ PHASE_SERVER_CLIENT_KNOWN,
+ PHASE_SERVER_CLIENT_FOCUSED,
+ PHASE_SERVER_ACCESSIBILITY,
+ PHASE_SERVER_SYSTEM_READY,
+ PHASE_SERVER_HIDE_IMPLICIT,
+ PHASE_SERVER_HIDE_NOT_ALWAYS,
+ PHASE_SERVER_WAIT_IME,
+ PHASE_SERVER_HAS_IME,
+ PHASE_SERVER_SHOULD_HIDE,
+ PHASE_IME_WRAPPER,
+ PHASE_IME_WRAPPER_DISPATCH,
+ PHASE_IME_SHOW_SOFT_INPUT,
+ PHASE_IME_HIDE_SOFT_INPUT,
+ PHASE_IME_ON_SHOW_SOFT_INPUT_TRUE,
+ PHASE_IME_APPLY_VISIBILITY_INSETS_CONSUMER,
+ PHASE_SERVER_APPLY_IME_VISIBILITY,
+ PHASE_WM_SHOW_IME_RUNNER,
+ PHASE_WM_SHOW_IME_READY,
+ PHASE_WM_HAS_IME_INSETS_CONTROL_TARGET,
+ PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_SHOW_INSETS,
+ PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_HIDE_INSETS,
+ PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_SHOW_INSETS,
+ PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_HIDE_INSETS,
+ PHASE_WM_REMOTE_INSETS_CONTROLLER,
+ PHASE_WM_ANIMATION_CREATE,
+ PHASE_WM_ANIMATION_RUNNING,
+ PHASE_CLIENT_SHOW_INSETS,
+ PHASE_CLIENT_HIDE_INSETS,
+ PHASE_CLIENT_HANDLE_SHOW_INSETS,
+ PHASE_CLIENT_HANDLE_HIDE_INSETS,
+ PHASE_CLIENT_APPLY_ANIMATION,
+ PHASE_CLIENT_CONTROL_ANIMATION,
+ PHASE_CLIENT_ANIMATION_RUNNING,
+ PHASE_CLIENT_ANIMATION_CANCEL,
+ PHASE_CLIENT_ANIMATION_FINISHED_SHOW,
+ PHASE_CLIENT_ANIMATION_FINISHED_HIDE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface Phase {}
+
+ /** The view that requested the IME has been served by the IMM. */
+ int PHASE_CLIENT_VIEW_SERVED = 0;
+
+ /** The IME client that requested the IME has window manager focus. */
+ int PHASE_SERVER_CLIENT_KNOWN = 1;
+
+ /** The IME client that requested the IME has IME focus. */
+ int PHASE_SERVER_CLIENT_FOCUSED = 2;
+
+ /** The IME request complies with the current accessibility settings. */
+ int PHASE_SERVER_ACCESSIBILITY = 3;
+
+ /** The server is ready to run third party code. */
+ int PHASE_SERVER_SYSTEM_READY = 4;
+
+ /** Checked the implicit hide request against any explicit show requests. */
+ int PHASE_SERVER_HIDE_IMPLICIT = 5;
+
+ /** Checked the not-always hide request against any forced show requests. */
+ int PHASE_SERVER_HIDE_NOT_ALWAYS = 6;
+
+ /** The server is waiting for a connection to the IME. */
+ int PHASE_SERVER_WAIT_IME = 7;
+
+ /** The server has a connection to the IME. */
+ int PHASE_SERVER_HAS_IME = 8;
+
+ /** The server decided the IME should be hidden. */
+ int PHASE_SERVER_SHOULD_HIDE = 9;
+
+ /** Reached the IME wrapper. */
+ int PHASE_IME_WRAPPER = 10;
+
+ /** Dispatched from the IME wrapper to the IME. */
+ int PHASE_IME_WRAPPER_DISPATCH = 11;
+
+ /** Reached the IME' showSoftInput method. */
+ int PHASE_IME_SHOW_SOFT_INPUT = 12;
+
+ /** Reached the IME' hideSoftInput method. */
+ int PHASE_IME_HIDE_SOFT_INPUT = 13;
+
+ /** The server decided the IME should be shown. */
+ int PHASE_IME_ON_SHOW_SOFT_INPUT_TRUE = 14;
+
+ /** Requested applying the IME visibility in the insets source consumer. */
+ int PHASE_IME_APPLY_VISIBILITY_INSETS_CONSUMER = 15;
+
+ /** Applied the IME visibility. */
+ int PHASE_SERVER_APPLY_IME_VISIBILITY = 16;
+
+ /** Created the show IME runner. */
+ int PHASE_WM_SHOW_IME_RUNNER = 17;
+
+ /** Ready to show IME. */
+ int PHASE_WM_SHOW_IME_READY = 18;
+
+ /** The Window Manager has a connection to the IME insets control target. */
+ int PHASE_WM_HAS_IME_INSETS_CONTROL_TARGET = 19;
+
+ /** Reached the window insets control target's show insets method. */
+ int PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_SHOW_INSETS = 20;
+
+ /** Reached the window insets control target's hide insets method. */
+ int PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_HIDE_INSETS = 21;
+
+ /** Reached the remote insets control target's show insets method. */
+ int PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_SHOW_INSETS = 22;
+
+ /** Reached the remote insets control target's hide insets method. */
+ int PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_HIDE_INSETS = 23;
+
+ /** Reached the remote insets controller. */
+ int PHASE_WM_REMOTE_INSETS_CONTROLLER = 24;
+
+ /** Created the IME window insets show animation. */
+ int PHASE_WM_ANIMATION_CREATE = 25;
+
+ /** Started the IME window insets show animation. */
+ int PHASE_WM_ANIMATION_RUNNING = 26;
+
+ /** Reached the client's show insets method. */
+ int PHASE_CLIENT_SHOW_INSETS = 27;
+
+ /** Reached the client's hide insets method. */
+ int PHASE_CLIENT_HIDE_INSETS = 28;
+
+ /** Handling the IME window insets show request. */
+ int PHASE_CLIENT_HANDLE_SHOW_INSETS = 29;
+
+ /** Handling the IME window insets hide request. */
+ int PHASE_CLIENT_HANDLE_HIDE_INSETS = 30;
+
+ /** Applied the IME window insets show animation. */
+ int PHASE_CLIENT_APPLY_ANIMATION = 31;
+
+ /** Started the IME window insets show animation. */
+ int PHASE_CLIENT_CONTROL_ANIMATION = 32;
+
+ /** Queued the IME window insets show animation. */
+ int PHASE_CLIENT_ANIMATION_RUNNING = 33;
+
+ /** Cancelled the IME window insets show animation. */
+ int PHASE_CLIENT_ANIMATION_CANCEL = 34;
+
+ /** Finished the IME window insets show animation. */
+ int PHASE_CLIENT_ANIMATION_FINISHED_SHOW = 35;
+
+ /** Finished the IME window insets hide animation. */
+ int PHASE_CLIENT_ANIMATION_FINISHED_HIDE = 36;
+
+ /**
+ * Called when an IME show request is created.
+ *
+ * @param token the token tracking the current IME show request or {@code null} otherwise.
+ * @param origin the origin of the IME show request.
+ * @param reason the reason why the IME show request was created.
+ */
+ void onRequestShow(@Nullable Token token, @Origin int origin,
+ @SoftInputShowHideReason int reason);
+
+ /**
+ * Called when an IME hide request is created.
+ *
+ * @param token the token tracking the current IME hide request or {@code null} otherwise.
+ * @param origin the origin of the IME hide request.
+ * @param reason the reason why the IME hide request was created.
+ */
+ void onRequestHide(@Nullable Token token, @Origin int origin,
+ @SoftInputShowHideReason int reason);
+
+ /**
+ * Called when an IME request progresses to a further phase.
+ *
+ * @param token the token tracking the current IME request or {@code null} otherwise.
+ * @param phase the new phase the IME request reached.
+ */
+ void onProgress(@Nullable Token token, @Phase int phase);
+
+ /**
+ * Called when an IME request fails.
+ *
+ * @param token the token tracking the current IME request or {@code null} otherwise.
+ * @param phase the phase the IME request failed at.
+ */
+ void onFailed(@Nullable Token token, @Phase int phase);
+
+ /**
+ * Called when an IME request reached a flow that is not yet implemented.
+ *
+ * @param token the token tracking the current IME request or {@code null} otherwise.
+ * @param phase the phase the IME request was currently at.
+ */
+ void onTodo(@Nullable Token token, @Phase int phase);
+
+ /**
+ * Called when an IME request is cancelled.
+ *
+ * @param token the token tracking the current IME request or {@code null} otherwise.
+ * @param phase the phase the IME request was cancelled at.
+ */
+ void onCancelled(@Nullable Token token, @Phase int phase);
+
+ /**
+ * Called when the IME show request is successful.
+ *
+ * @param token the token tracking the current IME show request or {@code null} otherwise.
+ */
+ void onShown(@Nullable Token token);
+
+ /**
+ * Called when the IME hide request is successful.
+ *
+ * @param token the token tracking the current IME hide request or {@code null} otherwise.
+ */
+ void onHidden(@Nullable Token token);
+
+ /**
+ * Get the singleton instance of this class.
+ *
+ * @return the singleton instance of this class
+ */
+ @NonNull
+ static ImeTracker get() {
+ return SystemProperties.getBoolean("persist.debug.imetracker", false)
+ ? LOGGER
+ : NOOP_LOGGER;
+ }
+
+ /** The singleton IME tracker instance. */
+ ImeTracker LOGGER = new ImeTracker() {
+
+ @Override
+ public void onRequestShow(@Nullable Token token, int origin,
+ @SoftInputShowHideReason int reason) {
+ if (token == null) return;
+ Log.i(TAG, token.mTag + ": onRequestShow at " + originToString(origin)
+ + " reason " + InputMethodDebug.softInputDisplayReasonToString(reason));
+ }
+
+ @Override
+ public void onRequestHide(@Nullable Token token, int origin,
+ @SoftInputShowHideReason int reason) {
+ if (token == null) return;
+ Log.i(TAG, token.mTag + ": onRequestHide at " + originToString(origin)
+ + " reason " + InputMethodDebug.softInputDisplayReasonToString(reason));
+ }
+
+ @Override
+ public void onProgress(@Nullable Token token, int phase) {
+ if (token == null) return;
+ Log.i(TAG, token.mTag + ": onProgress at " + phaseToString(phase));
+ }
+
+ @Override
+ public void onFailed(@Nullable Token token, int phase) {
+ if (token == null) return;
+ Log.i(TAG, token.mTag + ": onFailed at " + phaseToString(phase));
+ }
+
+ @Override
+ public void onTodo(@Nullable Token token, int phase) {
+ if (token == null) return;
+ Log.i(TAG, token.mTag + ": onTodo at " + phaseToString(phase));
+ }
+
+ @Override
+ public void onCancelled(@Nullable Token token, int phase) {
+ if (token == null) return;
+ Log.i(TAG, token.mTag + ": onCancelled at " + phaseToString(phase));
+ }
+
+ @Override
+ public void onShown(@Nullable Token token) {
+ if (token == null) return;
+ Log.i(TAG, token.mTag + ": onShown");
+ }
+
+ @Override
+ public void onHidden(@Nullable Token token) {
+ if (token == null) return;
+ 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;
+ private final String mTag;
+
+ public Token() {
+ this(ActivityThread.currentProcessName());
+ }
+
+ public Token(String component) {
+ this(new Binder(), component + ":" + Integer.toHexString((new Random().nextInt())));
+ }
+
+ private Token(IBinder binder, String tag) {
+ mBinder = binder;
+ mTag = tag;
+ }
+
+ /** For Parcelable, no special marshalled objects. */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeStrongBinder(mBinder);
+ dest.writeString8(mTag);
+ }
+
+ @NonNull
+ public static final Creator<Token> CREATOR = new Creator<>() {
+ @Override
+ public Token createFromParcel(Parcel source) {
+ IBinder binder = source.readStrongBinder();
+ String tag = source.readString8();
+ return new Token(binder, tag);
+ }
+
+ @Override
+ public Token[] newArray(int size) {
+ return new Token[size];
+ }
+ };
+ }
+
+ /**
+ * Utilities for mapping phases and origins 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> sOrigins =
+ getFieldMapping(ImeTracker.class, "ORIGIN_");
+ private static final Map<Integer, String> sPhases =
+ getFieldMapping(ImeTracker.class, "PHASE_");
+
+ public static String originToString(int origin) {
+ return sOrigins.getOrDefault(origin, "ORIGIN_" + origin);
+ }
+
+ public static String phaseToString(int phase) {
+ return sPhases.getOrDefault(phase, "PHASE_" + phase);
+ }
+
+ private static Map<Integer, String> getFieldMapping(Class<?> cls, String fieldPrefix) {
+ return Arrays.stream(cls.getDeclaredFields())
+ .filter(field -> field.getName().startsWith(fieldPrefix))
+ .collect(Collectors.toMap(Debug::getFieldValue, Field::getName));
+ }
+
+ private static int getFieldValue(Field field) {
+ try {
+ return field.getInt(null);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+}
diff --git a/core/java/android/view/inputmethod/InputMethod.java b/core/java/android/view/inputmethod/InputMethod.java
index 4d5a17d..92380ed 100644
--- a/core/java/android/view/inputmethod/InputMethod.java
+++ b/core/java/android/view/inputmethod/InputMethod.java
@@ -300,11 +300,12 @@
* @param showInputToken an opaque {@link android.os.Binder} token to identify which API call
* of {@link InputMethodManager#showSoftInput(View, int)} is associated with
* this callback.
+ * @param statsToken the token tracking the current IME show request or {@code null} otherwise.
* @hide
*/
@MainThread
- default public void showSoftInputWithToken(int flags, ResultReceiver resultReceiver,
- IBinder showInputToken) {
+ public default void showSoftInputWithToken(int flags, ResultReceiver resultReceiver,
+ IBinder showInputToken, @Nullable ImeTracker.Token statsToken) {
showSoftInput(flags, resultReceiver);
}
@@ -338,11 +339,14 @@
* @param hideInputToken an opaque {@link android.os.Binder} token to identify which API call
* of {@link InputMethodManager#hideSoftInputFromWindow(IBinder, int)}} is associated
* with this callback.
+ * @param statsToken the token tracking the current IME hide request or {@code null} otherwise.
* @hide
*/
@MainThread
- public void hideSoftInputWithToken(int flags, ResultReceiver resultReceiver,
- IBinder hideInputToken);
+ public default void hideSoftInputWithToken(int flags, ResultReceiver resultReceiver,
+ IBinder hideInputToken, @Nullable ImeTracker.Token statsToken) {
+ hideSoftInput(flags, resultReceiver);
+ }
/**
* Request that any soft input part of the input method be hidden from the user.
@@ -369,7 +373,7 @@
/**
* Checks if IME is ready to start stylus handwriting session.
- * If yes, {@link #startStylusHandwriting(InputChannel, List)} is called.
+ * If yes, {@link #startStylusHandwriting(int, InputChannel, List)} is called.
* @param requestId
* @hide
*/
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index 74afced..ee31fd5 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -632,6 +632,8 @@
private final DelegateImpl mDelegate = new DelegateImpl();
+ private static boolean sPreventImeStartupUnlessTextEditor;
+
// -----------------------------------------------------------
private static final int MSG_DUMP = 1;
@@ -1435,6 +1437,10 @@
// display case.
final Looper looper = displayId == Display.DEFAULT_DISPLAY
? Looper.getMainLooper() : context.getMainLooper();
+ // Keep track of whether to expect the IME to be unavailable so as to avoid log spam in
+ // sendInputEventOnMainLooperLocked() by not logging a verbose message on every DPAD event
+ sPreventImeStartupUnlessTextEditor = context.getResources().getBoolean(
+ com.android.internal.R.bool.config_preventImeStartupUnlessTextEditor);
return forContextInternal(displayId, looper);
}
@@ -2001,6 +2007,10 @@
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);
+
ImeTracing.getInstance().triggerClientDump("InputMethodManager#showSoftInput", this,
null /* icProto */);
// Re-dispatch if there is a context mismatch.
@@ -2012,10 +2022,13 @@
checkFocus();
synchronized (mH) {
if (!hasServedByInputMethodLocked(view)) {
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
Log.w(TAG, "Ignoring showSoftInput() as view=" + view + " is not served.");
return false;
}
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
+
// Makes sure to call ImeInsetsSourceConsumer#onShowRequested on the UI thread.
// TODO(b/229426865): call WindowInsetsController#show instead.
mH.executeOrSendMessage(Message.obtain(mH, MSG_ON_SHOW_REQUESTED));
@@ -2024,6 +2037,7 @@
return IInputMethodManagerGlobalInvoker.showSoftInput(
mClient,
view.getWindowToken(),
+ statsToken,
flags,
mCurRootView.getLastClickToolType(),
resultReceiver,
@@ -2043,19 +2057,28 @@
@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,
+ SoftInputShowHideReason.SHOW_SOFT_INPUT);
+
Log.w(TAG, "showSoftInputUnchecked() is a hidden method, which will be"
+ " removed soon. If you are using androidx.appcompat.widget.SearchView,"
+ " please update to version 26.0 or newer version.");
if (mCurRootView == null || mCurRootView.getView() == null) {
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
Log.w(TAG, "No current root view, ignoring showSoftInputUnchecked()");
return;
}
+
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
+
// Makes sure to call ImeInsetsSourceConsumer#onShowRequested on the UI thread.
// TODO(b/229426865): call WindowInsetsController#show instead.
mH.executeOrSendMessage(Message.obtain(mH, MSG_ON_SHOW_REQUESTED));
IInputMethodManagerGlobalInvoker.showSoftInput(
mClient,
mCurRootView.getView().getWindowToken(),
+ statsToken,
flags,
mCurRootView.getLastClickToolType(),
resultReceiver,
@@ -2125,17 +2148,24 @@
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);
+
ImeTracing.getInstance().triggerClientDump("InputMethodManager#hideSoftInputFromWindow",
this, null /* icProto */);
checkFocus();
synchronized (mH) {
final View servedView = getServedViewLocked();
if (servedView == null || servedView.getWindowToken() != windowToken) {
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
return false;
}
- return IInputMethodManagerGlobalInvoker.hideSoftInput(mClient, windowToken, flags,
- resultReceiver, reason);
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
+
+ return IInputMethodManagerGlobalInvoker.hideSoftInput(mClient, windowToken, statsToken,
+ flags, resultReceiver, reason);
}
}
@@ -2763,14 +2793,23 @@
@UnsupportedAppUsage
void closeCurrentInput() {
+ final ImeTracker.Token statsToken = new ImeTracker.Token();
+ ImeTracker.get().onRequestHide(statsToken, ImeTracker.ORIGIN_CLIENT_HIDE_SOFT_INPUT,
+ SoftInputShowHideReason.HIDE_SOFT_INPUT);
+
synchronized (mH) {
if (mCurRootView == null || mCurRootView.getView() == null) {
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
Log.w(TAG, "No current root view, ignoring closeCurrentInput()");
return;
}
+
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
+
IInputMethodManagerGlobalInvoker.hideSoftInput(
mClient,
mCurRootView.getView().getWindowToken(),
+ statsToken,
HIDE_NOT_ALWAYS,
null,
SoftInputShowHideReason.HIDE_SOFT_INPUT);
@@ -2839,15 +2878,24 @@
* @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);
+
ImeTracing.getInstance().triggerClientDump("InputMethodManager#notifyImeHidden", this,
null /* icProto */);
synchronized (mH) {
- if (isImeSessionAvailableLocked() && mCurRootView != null
- && mCurRootView.getWindowToken() == windowToken) {
- IInputMethodManagerGlobalInvoker.hideSoftInput(mClient, windowToken, 0 /* flags */,
- null /* resultReceiver */,
- SoftInputShowHideReason.HIDE_SOFT_INPUT_BY_INSETS_API);
+ if (!isImeSessionAvailableLocked() || mCurRootView == null
+ || mCurRootView.getWindowToken() != windowToken) {
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
+ return;
}
+
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
+
+ IInputMethodManagerGlobalInvoker.hideSoftInput(mClient, windowToken, statsToken,
+ 0 /* flags */, null /* resultReceiver */,
+ SoftInputShowHideReason.HIDE_SOFT_INPUT_BY_INSETS_API);
}
}
@@ -3364,8 +3412,12 @@
return DISPATCH_IN_PROGRESS;
}
- Log.w(TAG, "Unable to send input event to IME: " + getImeIdLocked()
- + " dropping: " + event);
+ if (sPreventImeStartupUnlessTextEditor) {
+ Log.d(TAG, "Dropping event because IME is evicted: " + event);
+ } else {
+ Log.w(TAG, "Unable to send input event to IME: " + getImeIdLocked()
+ + " dropping: " + event);
+ }
}
return DISPATCH_NOT_HANDLED;
}
@@ -3967,7 +4019,7 @@
/**
* As reported by {@link InputBindResult}. This value is determined by
- * {@link com.android.internal.R.styleable#InputMethod_suppressesSpellChecking}.
+ * {@link com.android.internal.R.styleable#InputMethod_suppressesSpellChecker}.
*/
final boolean mIsInputMethodSuppressingSpellChecker;
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 810fde2..bf1a2bd 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -2289,11 +2289,13 @@
* @param familyName family name string, e.g. "serif"
* @param typefaceIndex an index of the typeface enum, e.g. SANS, SERIF.
* @param style a typeface style
- * @param weight a weight value for the Typeface or -1 if not specified.
+ * @param weight a weight value for the Typeface or {@code FontStyle.FONT_WEIGHT_UNSPECIFIED}
+ * if not specified.
*/
private void setTypefaceFromAttrs(@Nullable Typeface typeface, @Nullable String familyName,
@XMLTypefaceAttr int typefaceIndex, @Typeface.Style int style,
- @IntRange(from = -1, to = FontStyle.FONT_WEIGHT_MAX) int weight) {
+ @IntRange(from = FontStyle.FONT_WEIGHT_UNSPECIFIED, to = FontStyle.FONT_WEIGHT_MAX)
+ int weight) {
if (typeface == null && familyName != null) {
// Lookup normal Typeface from system font map.
final Typeface normalTypeface = Typeface.create(familyName, Typeface.NORMAL);
@@ -2320,7 +2322,8 @@
}
private void resolveStyleAndSetTypeface(@NonNull Typeface typeface, @Typeface.Style int style,
- @IntRange(from = -1, to = FontStyle.FONT_WEIGHT_MAX) int weight) {
+ @IntRange(from = FontStyle.FONT_WEIGHT_UNSPECIFIED, to = FontStyle.FONT_WEIGHT_MAX)
+ int weight) {
if (weight >= 0) {
weight = Math.min(FontStyle.FONT_WEIGHT_MAX, weight);
final boolean italic = (style & Typeface.ITALIC) != 0;
@@ -4021,7 +4024,7 @@
boolean mFontFamilyExplicit = false;
int mTypefaceIndex = -1;
int mTextStyle = 0;
- int mFontWeight = -1;
+ int mFontWeight = FontStyle.FONT_WEIGHT_UNSPECIFIED;
boolean mAllCaps = false;
int mShadowColor = 0;
float mShadowDx = 0, mShadowDy = 0, mShadowRadius = 0;
@@ -6946,18 +6949,18 @@
if (isPassword) {
setTransformationMethod(PasswordTransformationMethod.getInstance());
setTypefaceFromAttrs(null/* fontTypeface */, null /* fontFamily */, MONOSPACE,
- Typeface.NORMAL, -1 /* weight, not specifeid */);
+ Typeface.NORMAL, FontStyle.FONT_WEIGHT_UNSPECIFIED);
} else if (isVisiblePassword) {
if (mTransformation == PasswordTransformationMethod.getInstance()) {
forceUpdate = true;
}
setTypefaceFromAttrs(null/* fontTypeface */, null /* fontFamily */, MONOSPACE,
- Typeface.NORMAL, -1 /* weight, not specified */);
+ Typeface.NORMAL, FontStyle.FONT_WEIGHT_UNSPECIFIED);
} else if (wasPassword || wasVisiblePassword) {
// not in password mode, clean up typeface and transformation
setTypefaceFromAttrs(null/* fontTypeface */, null /* fontFamily */,
DEFAULT_TYPEFACE /* typeface index */, Typeface.NORMAL,
- -1 /* weight, not specified */);
+ FontStyle.FONT_WEIGHT_UNSPECIFIED);
if (mTransformation == PasswordTransformationMethod.getInstance()) {
forceUpdate = true;
}
diff --git a/core/java/android/window/ClientWindowFrames.java b/core/java/android/window/ClientWindowFrames.java
index f274d1a..0ce076b6 100644
--- a/core/java/android/window/ClientWindowFrames.java
+++ b/core/java/android/window/ClientWindowFrames.java
@@ -49,7 +49,7 @@
public boolean isParentFrameClippedByDisplayCutout;
- public float sizeCompatScale = 1f;
+ public float compatScale = 1f;
public ClientWindowFrames() {
}
@@ -62,7 +62,7 @@
attachedFrame = new Rect(other.attachedFrame);
}
isParentFrameClippedByDisplayCutout = other.isParentFrameClippedByDisplayCutout;
- sizeCompatScale = other.sizeCompatScale;
+ compatScale = other.compatScale;
}
private ClientWindowFrames(Parcel in) {
@@ -76,7 +76,7 @@
parentFrame.readFromParcel(in);
attachedFrame = in.readTypedObject(Rect.CREATOR);
isParentFrameClippedByDisplayCutout = in.readBoolean();
- sizeCompatScale = in.readFloat();
+ compatScale = in.readFloat();
}
@Override
@@ -86,7 +86,7 @@
parentFrame.writeToParcel(dest, flags);
dest.writeTypedObject(attachedFrame, flags);
dest.writeBoolean(isParentFrameClippedByDisplayCutout);
- dest.writeFloat(sizeCompatScale);
+ dest.writeFloat(compatScale);
}
@Override
@@ -97,7 +97,7 @@
+ " parentFrame=" + parentFrame.toShortString(sb)
+ (attachedFrame != null ? " attachedFrame=" + attachedFrame.toShortString() : "")
+ (isParentFrameClippedByDisplayCutout ? " parentClippedByDisplayCutout" : "")
- + (sizeCompatScale != 1f ? " sizeCompatScale=" + sizeCompatScale : "") + "}";
+ + (compatScale != 1f ? " sizeCompatScale=" + compatScale : "") + "}";
}
@Override
diff --git a/core/java/android/window/DisplayWindowPolicyController.java b/core/java/android/window/DisplayWindowPolicyController.java
index a5aefd5..f55932e 100644
--- a/core/java/android/window/DisplayWindowPolicyController.java
+++ b/core/java/android/window/DisplayWindowPolicyController.java
@@ -16,6 +16,8 @@
package android.window;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+
import android.annotation.NonNull;
import android.app.WindowConfiguration;
import android.content.ComponentName;
@@ -142,6 +144,14 @@
*/
public void onRunningAppsChanged(ArraySet<Integer> runningUids) {}
+ /**
+ * This is called when an Activity is entering PIP.
+ * Returns {@code true} if the Activity is allowed to enter PIP.
+ */
+ public boolean isEnteringPipAllowed(int uid) {
+ return isWindowingModeSupported(WINDOWING_MODE_PINNED);
+ }
+
/** Dump debug data */
public void dump(String prefix, final PrintWriter pw) {
pw.println(prefix + "DisplayWindowPolicyController{" + super.toString() + "}");
diff --git a/core/java/com/android/internal/inputmethod/IInputMethod.aidl b/core/java/com/android/internal/inputmethod/IInputMethod.aidl
index c62fba9..1e3714e 100644
--- a/core/java/com/android/internal/inputmethod/IInputMethod.aidl
+++ b/core/java/com/android/internal/inputmethod/IInputMethod.aidl
@@ -21,6 +21,7 @@
import android.view.InputChannel;
import android.view.MotionEvent;
import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.ImeTracker;
import android.view.inputmethod.InputBinding;
import android.view.inputmethod.InputMethodSubtype;
import android.window.ImeOnBackInvokedDispatcher;
@@ -69,9 +70,11 @@
void setSessionEnabled(IInputMethodSession session, boolean enabled);
- void showSoftInput(in IBinder showInputToken, int flags, in ResultReceiver resultReceiver);
+ void showSoftInput(in IBinder showInputToken, in @nullable ImeTracker.Token statsToken,
+ int flags, in ResultReceiver resultReceiver);
- void hideSoftInput(in IBinder hideInputToken, int flags, in ResultReceiver resultReceiver);
+ void hideSoftInput(in IBinder hideInputToken, in @nullable ImeTracker.Token statsToken,
+ int flags, in ResultReceiver resultReceiver);
void updateEditorToolType(int toolType);
diff --git a/core/java/com/android/internal/inputmethod/IInputMethodPrivilegedOperations.aidl b/core/java/com/android/internal/inputmethod/IInputMethodPrivilegedOperations.aidl
index 4babb70..f77e962 100644
--- a/core/java/com/android/internal/inputmethod/IInputMethodPrivilegedOperations.aidl
+++ b/core/java/com/android/internal/inputmethod/IInputMethodPrivilegedOperations.aidl
@@ -17,6 +17,7 @@
package com.android.internal.inputmethod;
import android.net.Uri;
+import android.view.inputmethod.ImeTracker;
import android.view.inputmethod.InputMethodSubtype;
import com.android.internal.infra.AndroidFuture;
@@ -41,7 +42,8 @@
void switchToNextInputMethod(boolean onlyCurrentIme, in AndroidFuture future /* T=Boolean */);
void shouldOfferSwitchingToNextInputMethod(in AndroidFuture future /* T=Boolean */);
void notifyUserActionAsync();
- void applyImeVisibilityAsync(IBinder showOrHideInputToken, boolean setVisible);
+ void applyImeVisibilityAsync(IBinder showOrHideInputToken, boolean setVisible,
+ in @nullable ImeTracker.Token statsToken);
void onStylusHandwritingReady(int requestId, int pid);
void resetStylusHandwriting(int requestId);
}
diff --git a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
index 67c2103..66e3333 100644
--- a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
+++ b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
@@ -25,6 +25,7 @@
import android.os.RemoteException;
import android.util.Log;
import android.view.View;
+import android.view.inputmethod.ImeTracker;
import android.view.inputmethod.InputMethodSubtype;
import com.android.internal.annotations.GuardedBy;
@@ -100,7 +101,7 @@
}
/**
- * Calls {@link IInputMethodPrivilegedOperations#setImeWindowStatusAsync(int, int}.
+ * Calls {@link IInputMethodPrivilegedOperations#setImeWindowStatusAsync(int, int)}.
*
* @param vis visibility flags
* @param backDisposition disposition flags
@@ -250,7 +251,7 @@
}
/**
- * Calls {@link IInputMethodPrivilegedOperations#hideMySoftInput(int, IVoidResultCallback)}
+ * Calls {@link IInputMethodPrivilegedOperations#hideMySoftInput(int, int, AndroidFuture)}
*
* @param flags additional operating flags
* @param reason the reason to hide soft input
@@ -316,7 +317,7 @@
/**
* Calls {@link IInputMethodPrivilegedOperations#switchToNextInputMethod(boolean,
- * IBooleanResultCallback)}
+ * AndroidFuture)}
*
* @param onlyCurrentIme {@code true} to switch to a {@link InputMethodSubtype} within the same
* IME
@@ -375,22 +376,25 @@
}
/**
- * Calls {@link IInputMethodPrivilegedOperations#applyImeVisibilityAsync(IBinder, boolean)}.
+ * Calls {@link IInputMethodPrivilegedOperations#applyImeVisibilityAsync(IBinder, boolean,
+ * ImeTracker.Token)}.
*
* @param showOrHideInputToken placeholder token that maps to window requesting
* {@link android.view.inputmethod.InputMethodManager#showSoftInput(View, int)} or
- * {@link android.view.inputmethod.InputMethodManager#hideSoftInputFromWindow
- * (IBinder, int)}
+ * {@link android.view.inputmethod.InputMethodManager#hideSoftInputFromWindow(IBinder,
+ * int)}
* @param setVisible {@code true} to set IME visible, else hidden.
+ * @param statsToken the token tracking the current IME request or {@code null} otherwise.
*/
@AnyThread
- public void applyImeVisibilityAsync(IBinder showOrHideInputToken, boolean setVisible) {
+ public void applyImeVisibilityAsync(IBinder showOrHideInputToken, boolean setVisible,
+ @Nullable ImeTracker.Token statsToken) {
final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
if (ops == null) {
return;
}
try {
- ops.applyImeVisibilityAsync(showOrHideInputToken, setVisible);
+ ops.applyImeVisibilityAsync(showOrHideInputToken, setVisible, statsToken);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/core/java/com/android/internal/view/BaseIWindow.java b/core/java/com/android/internal/view/BaseIWindow.java
index fe77236..2ac4309 100644
--- a/core/java/com/android/internal/view/BaseIWindow.java
+++ b/core/java/com/android/internal/view/BaseIWindow.java
@@ -16,6 +16,7 @@
package com.android.internal.view;
+import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
import android.hardware.input.InputManager;
import android.os.Bundle;
@@ -31,6 +32,7 @@
import android.view.PointerIcon;
import android.view.ScrollCaptureResponse;
import android.view.WindowInsets.Type.InsetsType;
+import android.view.inputmethod.ImeTracker;
import android.window.ClientWindowFrames;
import com.android.internal.os.IResultReceiver;
@@ -66,11 +68,13 @@
}
@Override
- public void showInsets(@InsetsType int types, boolean fromIme) {
+ public void showInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
}
@Override
- public void hideInsets(@InsetsType int types, boolean fromIme) {
+ public void hideInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
}
@Override
diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl
index 40c6a05..00bc3f2 100644
--- a/core/java/com/android/internal/view/IInputMethodManager.aidl
+++ b/core/java/com/android/internal/view/IInputMethodManager.aidl
@@ -17,6 +17,7 @@
package com.android.internal.view;
import android.os.ResultReceiver;
+import android.view.inputmethod.ImeTracker;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodSubtype;
import android.view.inputmethod.EditorInfo;
@@ -54,11 +55,12 @@
+ "android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)")
InputMethodSubtype getLastInputMethodSubtype(int userId);
- boolean showSoftInput(in IInputMethodClient client, @nullable IBinder windowToken, int flags,
- int lastClickToolType, in @nullable ResultReceiver resultReceiver, int reason);
- boolean hideSoftInput(in IInputMethodClient client, @nullable IBinder windowToken, int flags,
+ boolean showSoftInput(in IInputMethodClient client, @nullable IBinder windowToken,
+ in @nullable ImeTracker.Token statsToken, int flags, int lastClickToolType,
in @nullable ResultReceiver resultReceiver, int reason);
-
+ boolean hideSoftInput(in IInputMethodClient client, @nullable IBinder windowToken,
+ in @nullable ImeTracker.Token statsToken, int flags,
+ in @nullable ResultReceiver resultReceiver, int reason);
// If windowToken is null, this just does startInput(). Otherwise this reports that a window
// has gained focus, and if 'editorInfo' is non-null then also does startInput.
// @NonNull
diff --git a/core/java/com/android/internal/widget/RemeasuringLinearLayout.java b/core/java/com/android/internal/widget/RemeasuringLinearLayout.java
index 7b154a5..80d1457 100644
--- a/core/java/com/android/internal/widget/RemeasuringLinearLayout.java
+++ b/core/java/com/android/internal/widget/RemeasuringLinearLayout.java
@@ -18,6 +18,7 @@
import android.annotation.Nullable;
import android.content.Context;
+import android.os.Trace;
import android.util.AttributeSet;
import android.view.View;
import android.widget.LinearLayout;
@@ -54,6 +55,7 @@
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ Trace.beginSection("RemeasuringLinearLayout#onMeasure");
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int count = getChildCount();
int height = 0;
@@ -86,5 +88,6 @@
}
mMatchParentViews.clear();
setMeasuredDimension(getMeasuredWidth(), height);
+ Trace.endSection();
}
}
diff --git a/core/jni/android_os_PerformanceHintManager.cpp b/core/jni/android_os_PerformanceHintManager.cpp
index d05a24f..ac1401d 100644
--- a/core/jni/android_os_PerformanceHintManager.cpp
+++ b/core/jni/android_os_PerformanceHintManager.cpp
@@ -40,6 +40,7 @@
typedef void (*APH_updateTargetWorkDuration)(APerformanceHintSession*, int64_t);
typedef void (*APH_reportActualWorkDuration)(APerformanceHintSession*, int64_t);
typedef void (*APH_closeSession)(APerformanceHintSession* session);
+typedef void (*APH_sendHint)(APerformanceHintSession*, int32_t);
bool gAPerformanceHintBindingInitialized = false;
APH_getManager gAPH_getManagerFn = nullptr;
@@ -48,6 +49,7 @@
APH_updateTargetWorkDuration gAPH_updateTargetWorkDurationFn = nullptr;
APH_reportActualWorkDuration gAPH_reportActualWorkDurationFn = nullptr;
APH_closeSession gAPH_closeSessionFn = nullptr;
+APH_sendHint gAPH_sendHintFn = nullptr;
void ensureAPerformanceHintBindingInitialized() {
if (gAPerformanceHintBindingInitialized) return;
@@ -88,6 +90,11 @@
LOG_ALWAYS_FATAL_IF(gAPH_closeSessionFn == nullptr,
"Failed to find required symbol APerformanceHint_closeSession!");
+ gAPH_sendHintFn = (APH_sendHint)dlsym(handle_, "APerformanceHint_sendHint");
+ LOG_ALWAYS_FATAL_IF(gAPH_sendHintFn == nullptr,
+ "Failed to find required symbol "
+ "APerformanceHint_sendHint!");
+
gAPerformanceHintBindingInitialized = true;
}
@@ -138,6 +145,11 @@
gAPH_closeSessionFn(reinterpret_cast<APerformanceHintSession*>(nativeSessionPtr));
}
+static void nativeSendHint(JNIEnv* env, jclass clazz, jlong nativeSessionPtr, jint hint) {
+ ensureAPerformanceHintBindingInitialized();
+ gAPH_sendHintFn(reinterpret_cast<APerformanceHintSession*>(nativeSessionPtr), hint);
+}
+
static const JNINativeMethod gPerformanceHintMethods[] = {
{"nativeAcquireManager", "()J", (void*)nativeAcquireManager},
{"nativeGetPreferredUpdateRateNanos", "(J)J", (void*)nativeGetPreferredUpdateRateNanos},
@@ -145,6 +157,7 @@
{"nativeUpdateTargetWorkDuration", "(JJ)V", (void*)nativeUpdateTargetWorkDuration},
{"nativeReportActualWorkDuration", "(JJ)V", (void*)nativeReportActualWorkDuration},
{"nativeCloseSession", "(J)V", (void*)nativeCloseSession},
+ {"nativeSendHint", "(JI)V", (void*)nativeSendHint},
};
int register_android_os_PerformanceHintManager(JNIEnv* env) {
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 0218215..ac383e6 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -1855,6 +1855,10 @@
-->
<string name="config_defaultCaptivePortalLoginPackageName" translatable="false">com.android.captiveportallogin</string>
+ <!-- The package name of the dock manager app. Must be granted the
+ POST_NOTIFICATIONS permission. -->
+ <string name="config_defaultDockManagerPackageName" translatable="false"></string>
+
<!-- Whether to enable geocoder overlay which allows geocoder to be replaced
by an app at run-time. When disabled, only the
config_geocoderProviderPackageName package will be searched for
@@ -2422,8 +2426,8 @@
<integer name="config_dreamsBatteryLevelDrainCutoff">5</integer>
<!-- Limit of how long the device can remain unlocked due to attention checking. -->
<integer name="config_attentionMaximumExtension">900000</integer> <!-- 15 minutes. -->
- <!-- Is the system user the only user allowed to dream. -->
- <bool name="config_dreamsOnlyEnabledForSystemUser">false</bool>
+ <!-- Whether there is to be a chosen Dock User who is the only user allowed to dream. -->
+ <bool name="config_dreamsOnlyEnabledForDockUser">false</bool>
<!-- Whether dreams are disabled when ambient mode is suppressed. -->
<bool name="config_dreamsDisabledByAmbientModeSuppressionConfig">false</bool>
@@ -2672,9 +2676,9 @@
Should be false for most devices, except automotive vehicle with passenger displays. -->
<bool name="config_multiuserUsersOnSecondaryDisplays">false</bool>
- <!-- Whether to automatically switch a non-primary user back to the primary user after a
- timeout when the device is docked. -->
- <bool name="config_enableTimeoutToUserZeroWhenDocked">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. -->
+ <bool name="config_enableTimeoutToDockUserWhenDocked">false</bool>
<!-- Whether to only install system packages on a user if they're allowlisted for that user
type. These are flags and can be freely combined.
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 48484c7..216975d 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -6331,6 +6331,8 @@
<string name="vdm_camera_access_denied" product="tablet">Can’t access the tablet’s camera from your <xliff:g id="device" example="Chromebook">%1$s</xliff:g></string>
<!-- Error message indicating the user cannot access secure content when running on a virtual device. [CHAR LIMIT=NONE] -->
<string name="vdm_secure_window">This can’t be accessed while streaming. Try on your phone instead.</string>
+ <!-- Error message indicating the user cannot view picture-in-picture when running on a virtual device. [CHAR LIMIT=NONE] -->
+ <string name="vdm_pip_blocked">Can’t view picture-in-picture while streaming</string>
<!-- Title for preference of the system default locale. [CHAR LIMIT=50]-->
<string name="system_locale_title">System default</string>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index b5d534f..89ec5ba 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -466,7 +466,7 @@
<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_enableTimeoutToUserZeroWhenDocked" />
+ <java-symbol type="bool" name="config_enableTimeoutToDockUserWhenDocked" />
<java-symbol type="integer" name="config_userTypePackageWhitelistMode"/>
<java-symbol type="xml" name="config_user_types" />
<java-symbol type="integer" name="config_safe_media_volume_index" />
@@ -2236,7 +2236,7 @@
<java-symbol type="integer" name="config_dreamsBatteryLevelDrainCutoff" />
<java-symbol type="string" name="config_dreamsDefaultComponent" />
<java-symbol type="bool" name="config_dreamsDisabledByAmbientModeSuppressionConfig" />
- <java-symbol type="bool" name="config_dreamsOnlyEnabledForSystemUser" />
+ <java-symbol type="bool" name="config_dreamsOnlyEnabledForDockUser" />
<java-symbol type="integer" name="config_dreamOpenAnimationDuration" />
<java-symbol type="integer" name="config_dreamCloseAnimationDuration" />
<java-symbol type="array" name="config_supportedDreamComplications" />
@@ -3480,6 +3480,9 @@
<!-- Captive Portal Login -->
<java-symbol type="string" name="config_defaultCaptivePortalLoginPackageName" />
+ <!-- Dock Manager -->
+ <java-symbol type="string" name="config_defaultDockManagerPackageName" />
+
<!-- Optional IPsec algorithms -->
<java-symbol type="array" name="config_optionalIpSecAlgorithms" />
@@ -4858,6 +4861,7 @@
<!-- For VirtualDeviceManager -->
<java-symbol type="string" name="vdm_camera_access_denied" />
<java-symbol type="string" name="vdm_secure_window" />
+ <java-symbol type="string" name="vdm_pip_blocked" />
<java-symbol type="color" name="camera_privacy_light_day"/>
<java-symbol type="color" name="camera_privacy_light_night"/>
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceAidlImplTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceAidlImplTest.java
index 14dd5df..a2df426 100644
--- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceAidlImplTest.java
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceAidlImplTest.java
@@ -20,9 +20,9 @@
import static com.google.common.truth.Truth.assertWithMessage;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceHidlImplTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceHidlImplTest.java
index f28e27d..5ab9435 100644
--- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceHidlImplTest.java
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceHidlImplTest.java
@@ -18,9 +18,9 @@
import static com.google.common.truth.Truth.assertWithMessage;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.verify;
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 2cb058b..a421218 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
@@ -31,6 +31,16 @@
throw new UnsupportedOperationException("AidlTestUtils class is noninstantiable");
}
+ static RadioManager.ModuleProperties makeDefaultModuleProperties() {
+ return new RadioManager.ModuleProperties(
+ /* id= */ 0, /* serviceName= */ "", /* classId= */ 0, /* implementor= */ "",
+ /* product= */ "", /* version= */ "", /* serial= */ "", /* numTuners= */ 0,
+ /* numAudioSources= */ 0, /* isInitializationRequired= */ false,
+ /* isCaptureSupported= */ false, /* bands= */ null,
+ /* isBgScanSupported= */ false, new int[] {}, new int[] {},
+ /* dabFrequencyTable= */ null, /* vendorInfo= */ null);
+ }
+
static RadioManager.ProgramInfo makeProgramInfo(ProgramSelector selector, int signalQuality) {
return new RadioManager.ProgramInfo(selector,
selector.getPrimaryId(), selector.getPrimaryId(), /* relatedContents= */ null,
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 d061a77..635d1e7 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
@@ -122,7 +122,7 @@
createBroadcastRadioService();
ITuner session = mBroadcastRadioService.openSession(FM_RADIO_MODULE_ID,
- /*legacyConfig= */ null, /* withAudio= */ true, mTunerCallbackMock);
+ /* legacyConfig= */ null, /* withAudio= */ true, mTunerCallbackMock);
assertWithMessage("Session opened in FM radio module")
.that(session).isEqualTo(mFmTunerSessionMock);
@@ -133,7 +133,7 @@
createBroadcastRadioService();
ITuner session = mBroadcastRadioService.openSession(DAB_RADIO_MODULE_ID + 1,
- /*legacyConfig= */ null, /* withAudio= */ true, mTunerCallbackMock);
+ /* legacyConfig= */ null, /* withAudio= */ true, mTunerCallbackMock);
assertWithMessage("Session opened with id not found").that(session).isNull();
}
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/RadioModuleTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/RadioModuleTest.java
index c1a4336..7a8475f 100644
--- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/RadioModuleTest.java
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/RadioModuleTest.java
@@ -19,9 +19,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.anyInt;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.eq;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -48,14 +48,7 @@
private static final int TEST_ENABLED_TYPE = Announcement.TYPE_EVENT;
private static final RadioManager.ModuleProperties TEST_MODULE_PROPERTIES =
- new RadioManager.ModuleProperties(/* id= */ 0, /* serviceName= */ "", /* classId= */ 0,
- /* implementor= */ "", /* product= */ "", /* version= */ "",
- /* serial= */ "", /* numTuners= */ 0, /* numAudioSources= */ 0,
- /* isInitializationRequired= */ false, /* isCaptureSupported= */ false,
- /* bands= */ null, /* isBgScanSupported= */ false,
- /* supportedProgramTypes= */ new int[]{},
- /* supportedIdentifierTypes */ new int[]{},
- /* dabFrequencyTable= */ null, /* vendorInfo= */ null);
+ AidlTestUtils.makeDefaultModuleProperties();
// Mocks
@Mock
@@ -108,7 +101,7 @@
Bitmap imageTest = mRadioModule.getImage(imageId);
- assertWithMessage("Image got from radio module").that(imageTest).isNull();
+ assertWithMessage("Image from radio module").that(imageTest).isNull();
}
@Test
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 c3623a2..3bf993c 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
@@ -19,10 +19,10 @@
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.Matchers.any;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.eq;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.timeout;
@@ -93,13 +93,8 @@
@Before
public void setup() throws Exception {
- mRadioModule = new RadioModule(mBroadcastRadioMock, new RadioManager.ModuleProperties(
- /* id= */ 0, /* serviceName= */ "", /* classId= */ 0, /* implementor= */ "",
- /* product= */ "", /* version= */ "", /* serial= */ "", /* numTuners= */ 0,
- /* numAudioSources= */ 0, /* isInitializationRequired= */ false,
- /* isCaptureSupported= */ false, /* bands= */ null, /* isBgScanSupported= */ false,
- new int[] {}, new int[] {},
- /* dabFrequencyTable= */ null, /* vendorInfo= */ null), mLock);
+ mRadioModule = new RadioModule(mBroadcastRadioMock,
+ AidlTestUtils.makeDefaultModuleProperties(), mLock);
doAnswer(invocation -> {
mHalTunerCallback = (ITunerCallback) invocation.getArguments()[0];
@@ -424,7 +419,7 @@
mTunerSessions[0].getImage(imageId);
});
- assertWithMessage("Exception for getting image with invalid ID")
+ assertWithMessage("Get image exception")
.that(thrown).hasMessageThat().contains("Image ID is missing");
}
@@ -467,7 +462,7 @@
boolean isSupported = mTunerSessions[0].isConfigFlagSupported(flag);
verify(mBroadcastRadioMock).isConfigFlagSet(flag);
- assertWithMessage("Config flag %s is supported", flag).that(isSupported).isFalse();
+ assertWithMessage("Config flag %s is supported", flag).that(isSupported).isFalse();
}
@Test
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/BroadcastRadioServiceHidlTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/BroadcastRadioServiceHidlTest.java
new file mode 100644
index 0000000..4d0b753
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/BroadcastRadioServiceHidlTest.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.broadcastradio.hal2;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.hardware.broadcastradio.V2_0.IBroadcastRadio;
+import android.hardware.radio.Announcement;
+import android.hardware.radio.IAnnouncementListener;
+import android.hardware.radio.ICloseHandle;
+import android.hardware.radio.ITuner;
+import android.hardware.radio.ITunerCallback;
+import android.hardware.radio.RadioManager;
+import android.hardware.radio.RadioTuner;
+import android.hidl.manager.V1_0.IServiceManager;
+import android.hidl.manager.V1_0.IServiceNotification;
+import android.os.IBinder;
+import android.os.IHwBinder.DeathRecipient;
+import android.os.RemoteException;
+
+import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder;
+import com.android.server.broadcastradio.ExtendedRadioMockitoTestCase;
+
+import org.junit.Test;
+import org.mockito.Mock;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+public final class BroadcastRadioServiceHidlTest extends ExtendedRadioMockitoTestCase {
+
+ private static final int FM_RADIO_MODULE_ID = 0;
+ private static final int DAB_RADIO_MODULE_ID = 1;
+ private static final ArrayList<String> SERVICE_LIST =
+ new ArrayList<>(Arrays.asList("FmService", "DabService"));
+ private static final int[] TEST_ENABLED_TYPES = new int[]{Announcement.TYPE_TRAFFIC};
+
+ private final Object mLock = new Object();
+
+ private BroadcastRadioService mBroadcastRadioService;
+ private DeathRecipient mFmDeathRecipient;
+
+ @Mock
+ private IServiceManager mServiceManagerMock;
+ @Mock
+ private RadioManager.ModuleProperties mFmModuleMock;
+ @Mock
+ private RadioManager.ModuleProperties mDabModuleMock;
+ @Mock
+ private RadioModule mFmRadioModuleMock;
+ @Mock
+ private RadioModule mDabRadioModuleMock;
+ @Mock
+ private IBroadcastRadio mFmHalServiceMock;
+ @Mock
+ private IBroadcastRadio mDabHalServiceMock;
+ @Mock
+ private TunerSession mFmTunerSessionMock;
+ @Mock
+ private ITunerCallback mTunerCallbackMock;
+ @Mock
+ private ICloseHandle mFmCloseHandleMock;
+ @Mock
+ private ICloseHandle mDabCloseHandleMock;
+ @Mock
+ private IAnnouncementListener mAnnouncementListenerMock;
+ @Mock
+ private IBinder mBinderMock;
+
+ @Override
+ protected void initializeSession(StaticMockitoSessionBuilder builder) {
+ builder.spyStatic(RadioModule.class);
+ }
+
+ @Test
+ public void listModules_withMultipleServiceNames() throws Exception {
+ createBroadcastRadioService();
+
+ assertWithMessage("Radio modules in HIDL broadcast radio HAL client")
+ .that(mBroadcastRadioService.listModules())
+ .containsExactly(mFmModuleMock, mDabModuleMock);
+ }
+
+ @Test
+ public void hasModules_withIdFoundInModules() throws Exception {
+ createBroadcastRadioService();
+
+ assertWithMessage("DAB radio module in HIDL broadcast radio HAL client")
+ .that(mBroadcastRadioService.hasModule(FM_RADIO_MODULE_ID)).isTrue();
+ }
+
+ @Test
+ public void hasModules_withIdNotFoundInModules() throws Exception {
+ createBroadcastRadioService();
+
+ assertWithMessage("Radio module of id not found in HIDL broadcast radio HAL client")
+ .that(mBroadcastRadioService.hasModule(DAB_RADIO_MODULE_ID + 1)).isFalse();
+ }
+
+ @Test
+ public void hasAnyModules_withModulesExist() throws Exception {
+ createBroadcastRadioService();
+
+ assertWithMessage("Any radio module in HIDL broadcast radio HAL client")
+ .that(mBroadcastRadioService.hasAnyModules()).isTrue();
+ }
+
+ @Test
+ public void openSession_withIdFound() throws Exception {
+ createBroadcastRadioService();
+
+ ITuner session = mBroadcastRadioService.openSession(FM_RADIO_MODULE_ID,
+ /* legacyConfig= */ null, /* withAudio= */ true, mTunerCallbackMock);
+
+ assertWithMessage("Session opened in FM radio module")
+ .that(session).isEqualTo(mFmTunerSessionMock);
+ }
+
+ @Test
+ public void openSession_withIdNotFound() throws Exception {
+ createBroadcastRadioService();
+ int moduleIdInvalid = DAB_RADIO_MODULE_ID + 1;
+
+ IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> {
+ mBroadcastRadioService.openSession(moduleIdInvalid, /* legacyConfig= */ null,
+ /* withAudio= */ true, mTunerCallbackMock);
+ });
+
+ assertWithMessage("Exception for opening session with module id %s", moduleIdInvalid)
+ .that(thrown).hasMessageThat().contains("Invalid module ID");
+ }
+
+ @Test
+ public void addAnnouncementListener_addsOnAllRadioModules() throws Exception {
+ createBroadcastRadioService();
+ when(mAnnouncementListenerMock.asBinder()).thenReturn(mBinderMock);
+ when(mFmRadioModuleMock.addAnnouncementListener(any(), any()))
+ .thenReturn(mFmCloseHandleMock);
+ when(mDabRadioModuleMock.addAnnouncementListener(any(), any()))
+ .thenReturn(mDabCloseHandleMock);
+
+ mBroadcastRadioService.addAnnouncementListener(TEST_ENABLED_TYPES,
+ mAnnouncementListenerMock);
+
+ verify(mFmRadioModuleMock).addAnnouncementListener(any(), any());
+ verify(mDabRadioModuleMock).addAnnouncementListener(any(), any());
+ }
+
+ @Test
+ public void binderDied_forDeathRecipient() throws Exception {
+ createBroadcastRadioService();
+
+ mFmDeathRecipient.serviceDied(FM_RADIO_MODULE_ID);
+
+ verify(mFmRadioModuleMock).closeSessions(eq(RadioTuner.ERROR_HARDWARE_FAILURE));
+ assertWithMessage("FM radio module after FM broadcast radio HAL service died")
+ .that(mBroadcastRadioService.hasModule(FM_RADIO_MODULE_ID)).isFalse();
+ }
+
+ private void createBroadcastRadioService() throws RemoteException {
+ mockServiceManager();
+ mBroadcastRadioService = new BroadcastRadioService(/* nextModuleId= */ FM_RADIO_MODULE_ID,
+ mLock, mServiceManagerMock);
+ }
+
+ private void mockServiceManager() throws RemoteException {
+ doAnswer(invocation -> {
+ mFmDeathRecipient = (DeathRecipient) invocation.getArguments()[0];
+ return null;
+ }).when(mFmHalServiceMock).linkToDeath(any(), eq((long) FM_RADIO_MODULE_ID));
+
+ when(mServiceManagerMock.registerForNotifications(anyString(), anyString(),
+ any(IServiceNotification.class))).thenAnswer(invocation -> {
+ IServiceNotification serviceCallback =
+ (IServiceNotification) invocation.getArguments()[2];
+ for (int index = 0; index < SERVICE_LIST.size(); index++) {
+ serviceCallback.onRegistration(IBroadcastRadio.kInterfaceName,
+ SERVICE_LIST.get(index), /* b= */ false);
+ }
+ return true;
+ }).thenReturn(true);
+
+ doReturn(mFmRadioModuleMock).when(() -> RadioModule.tryLoadingModule(
+ eq(FM_RADIO_MODULE_ID), anyString(), any(Object.class)));
+ doReturn(mDabRadioModuleMock).when(() -> RadioModule.tryLoadingModule(
+ eq(DAB_RADIO_MODULE_ID), anyString(), any(Object.class)));
+
+ when(mFmRadioModuleMock.getProperties()).thenReturn(mFmModuleMock);
+ when(mDabRadioModuleMock.getProperties()).thenReturn(mDabModuleMock);
+
+ when(mFmRadioModuleMock.getService()).thenReturn(mFmHalServiceMock);
+ when(mDabRadioModuleMock.getService()).thenReturn(mDabHalServiceMock);
+
+ when(mFmRadioModuleMock.openSession(mTunerCallbackMock))
+ .thenReturn(mFmTunerSessionMock);
+ }
+}
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/RadioModuleHidlTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/RadioModuleHidlTest.java
new file mode 100644
index 0000000..48f5a46
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/RadioModuleHidlTest.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.broadcastradio.hal2;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.graphics.Bitmap;
+import android.hardware.broadcastradio.V2_0.Constants;
+import android.hardware.broadcastradio.V2_0.IBroadcastRadio;
+import android.hardware.broadcastradio.V2_0.Result;
+import android.hardware.radio.Announcement;
+import android.hardware.radio.IAnnouncementListener;
+import android.hardware.radio.ICloseHandle;
+import android.hardware.radio.RadioManager;
+import android.os.RemoteException;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * Tests for HIDL HAL RadioModule.
+ */
+@RunWith(MockitoJUnitRunner.class)
+public final class RadioModuleHidlTest {
+
+ private static final int TEST_ENABLED_TYPE = Announcement.TYPE_EVENT;
+ private static final RadioManager.ModuleProperties TEST_MODULE_PROPERTIES =
+ TestUtils.makeDefaultModuleProperties();
+
+ @Mock
+ private IBroadcastRadio mBroadcastRadioMock;
+ @Mock
+ private IAnnouncementListener mListenerMock;
+ @Mock
+ private android.hardware.broadcastradio.V2_0.ICloseHandle mHalCloseHandleMock;
+
+ private final Object mLock = new Object();
+ private RadioModule mRadioModule;
+ private android.hardware.broadcastradio.V2_0.IAnnouncementListener mHalListener;
+
+ @Before
+ public void setup() throws RemoteException {
+ mRadioModule = new RadioModule(mBroadcastRadioMock, TEST_MODULE_PROPERTIES, mLock);
+
+ when(mBroadcastRadioMock.getImage(anyInt())).thenReturn(new ArrayList<Byte>(0));
+
+ doAnswer(invocation -> {
+ mHalListener = (android.hardware.broadcastradio.V2_0.IAnnouncementListener) invocation
+ .getArguments()[1];
+ IBroadcastRadio.registerAnnouncementListenerCallback cb =
+ (IBroadcastRadio.registerAnnouncementListenerCallback)
+ invocation.getArguments()[2];
+ cb.onValues(Result.OK, mHalCloseHandleMock);
+ return null;
+ }).when(mBroadcastRadioMock).registerAnnouncementListener(any(), any(), any());
+ }
+
+ @Test
+ public void getService() {
+ assertWithMessage("Service of radio module")
+ .that(mRadioModule.getService()).isEqualTo(mBroadcastRadioMock);
+ }
+
+ @Test
+ public void getProperties() {
+ assertWithMessage("Module properties of radio module")
+ .that(mRadioModule.getProperties()).isEqualTo(TEST_MODULE_PROPERTIES);
+ }
+
+ @Test
+ public void getImage_withValidIdFromRadioModule() {
+ int imageId = 1;
+
+ Bitmap imageTest = mRadioModule.getImage(imageId);
+
+ assertWithMessage("Image from radio module").that(imageTest).isNull();
+ }
+
+ @Test
+ public void getImage_withInvalidIdFromRadioModule_throwsIllegalArgumentException() {
+ int invalidImageId = Constants.INVALID_IMAGE;
+
+ IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> {
+ mRadioModule.getImage(invalidImageId);
+ });
+
+ assertWithMessage("Exception for getting image with invalid ID")
+ .that(thrown).hasMessageThat().contains("Image ID is missing");
+ }
+
+ @Test
+ public void addAnnouncementListener_listenerRegistered() throws Exception {
+ ArrayList<Byte> enabledListExpected = new ArrayList<Byte>(Arrays.asList(
+ (byte) TEST_ENABLED_TYPE));
+ mRadioModule.addAnnouncementListener(new int[]{TEST_ENABLED_TYPE}, mListenerMock);
+
+ verify(mBroadcastRadioMock)
+ .registerAnnouncementListener(eq(enabledListExpected), any(), any());
+ }
+
+ @Test
+ public void onListUpdate_forAnnouncementListener() throws Exception {
+ android.hardware.broadcastradio.V2_0.Announcement halAnnouncement =
+ TestUtils.makeAnnouncement(TEST_ENABLED_TYPE, /* selectorFreq= */ 96300);
+ mRadioModule.addAnnouncementListener(new int[]{TEST_ENABLED_TYPE}, mListenerMock);
+
+ mHalListener.onListUpdated(
+ new ArrayList<android.hardware.broadcastradio.V2_0.Announcement>(
+ Arrays.asList(halAnnouncement)));
+
+ verify(mListenerMock).onListUpdated(any());
+ }
+
+ @Test
+ public void close_forCloseHandle() throws Exception {
+ ICloseHandle closeHandle =
+ mRadioModule.addAnnouncementListener(new int[]{TEST_ENABLED_TYPE}, mListenerMock);
+
+ closeHandle.close();
+
+ verify(mHalCloseHandleMock).close();
+ }
+}
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/StartProgramListUpdatesFanoutTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/StartProgramListUpdatesFanoutTest.java
index 25bf93f..d104359 100644
--- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/StartProgramListUpdatesFanoutTest.java
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/StartProgramListUpdatesFanoutTest.java
@@ -95,9 +95,8 @@
public void setup() throws RemoteException {
MockitoAnnotations.initMocks(this);
- mRadioModule = new RadioModule(mBroadcastRadioMock, new RadioManager.ModuleProperties(0, "",
- 0, "", "", "", "", 0, 0, false, false, null, false, new int[] {}, new int[] {},
- null, null), mLock);
+ mRadioModule = new RadioModule(mBroadcastRadioMock,
+ TestUtils.makeDefaultModuleProperties(), mLock);
doAnswer((Answer) invocation -> {
mHalTunerCallback = (ITunerCallback) invocation.getArguments()[0];
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TestUtils.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TestUtils.java
index ad1b872..4eedd2f 100644
--- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TestUtils.java
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TestUtils.java
@@ -33,6 +33,16 @@
throw new UnsupportedOperationException("TestUtils class is noninstantiable");
}
+ static RadioManager.ModuleProperties makeDefaultModuleProperties() {
+ return new RadioManager.ModuleProperties(
+ /* id= */ 0, /* serviceName= */ "", /* classId= */ 0, /* implementor= */ "",
+ /* product= */ "", /* version= */ "", /* serial= */ "", /* numTuners= */ 0,
+ /* numAudioSources= */ 0, /* isInitializationRequired= */ false,
+ /* isCaptureSupported= */ false, /* bands= */ null,
+ /* isBgScanSupported= */ false, new int[] {}, new int[] {},
+ /* dabFrequencyTable= */ null, /* vendorInfo= */ null);
+ }
+
static RadioManager.ProgramInfo makeProgramInfo(ProgramSelector selector, int signalQuality) {
return new RadioManager.ProgramInfo(selector,
selector.getPrimaryId(), selector.getPrimaryId(), /* relatedContents= */ null,
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 9b62dc7..936e606 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
@@ -37,12 +37,15 @@
import android.hardware.broadcastradio.V2_0.IdentifierType;
import android.hardware.broadcastradio.V2_0.ProgramInfo;
import android.hardware.broadcastradio.V2_0.Result;
+import android.hardware.broadcastradio.V2_0.VendorKeyValue;
import android.hardware.radio.ProgramList;
import android.hardware.radio.ProgramSelector;
import android.hardware.radio.RadioManager;
import android.hardware.radio.RadioTuner;
+import android.util.ArrayMap;
import android.util.ArraySet;
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -51,6 +54,8 @@
import org.mockito.verification.VerificationWithTimeout;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Map;
/**
* Tests for HIDL HAL TunerSession.
@@ -73,6 +78,7 @@
private static final int UNSUPPORTED_CONFIG_FLAG = 0;
private final Object mLock = new Object();
+ private final ArrayMap<Integer, Boolean> mHalConfigMap = new ArrayMap<>();
private RadioModule mRadioModule;
private ITunerCallback mHalTunerCallback;
private ProgramInfo mHalCurrentInfo;
@@ -84,13 +90,8 @@
@Before
public void setup() throws Exception {
- mRadioModule = new RadioModule(mBroadcastRadioMock, new RadioManager.ModuleProperties(
- /* id= */ 0, /* serviceName= */ "", /* classId= */ 0, /* implementor= */ "",
- /* product= */ "", /* version= */ "", /* serial= */ "", /* numTuners= */ 0,
- /* numAudioSources= */ 0, /* isInitializationRequired= */ false,
- /* isCaptureSupported= */ false, /* bands= */ null, /* isBgScanSupported= */ false,
- new int[] {}, new int[] {},
- /* dabFrequencyTable= */ null, /* vendorInfo= */ null), mLock);
+ mRadioModule = new RadioModule(mBroadcastRadioMock,
+ TestUtils.makeDefaultModuleProperties(), mLock);
doAnswer(invocation -> {
mHalTunerCallback = (ITunerCallback) invocation.getArguments()[0];
@@ -142,6 +143,32 @@
}).when(mHalTunerSessionMock).scan(anyBoolean(), anyBoolean());
when(mBroadcastRadioMock.getImage(anyInt())).thenReturn(new ArrayList<Byte>(0));
+
+ doAnswer(invocation -> {
+ int configFlag = (int) invocation.getArguments()[0];
+ ITunerSession.isConfigFlagSetCallback cb = (ITunerSession.isConfigFlagSetCallback)
+ invocation.getArguments()[1];
+ if (configFlag == UNSUPPORTED_CONFIG_FLAG) {
+ cb.onValues(Result.NOT_SUPPORTED, false);
+ return null;
+ }
+ cb.onValues(Result.OK, mHalConfigMap.getOrDefault(configFlag, false));
+ return null;
+ }).when(mHalTunerSessionMock).isConfigFlagSet(anyInt(), any());
+
+ doAnswer(invocation -> {
+ int configFlag = (int) invocation.getArguments()[0];
+ if (configFlag == UNSUPPORTED_CONFIG_FLAG) {
+ return Result.NOT_SUPPORTED;
+ }
+ mHalConfigMap.put(configFlag, (boolean) invocation.getArguments()[1]);
+ return Result.OK;
+ }).when(mHalTunerSessionMock).setConfigFlag(anyInt(), anyBoolean());
+ }
+
+ @After
+ public void cleanUp() {
+ mHalConfigMap.clear();
}
@Test
@@ -395,7 +422,7 @@
mTunerSessions[0].getImage(imageId);
});
- assertWithMessage("Exception for getting image with invalid ID")
+ assertWithMessage("Get image exception")
.that(thrown).hasMessageThat().contains("Image ID is missing");
}
@@ -430,6 +457,138 @@
verify(mHalTunerSessionMock).stopProgramListUpdates();
}
+ @Test
+ public void isConfigFlagSupported_withUnsupportedFlag_returnsFalse() throws Exception {
+ openAidlClients(/* numClients= */ 1);
+ int flag = UNSUPPORTED_CONFIG_FLAG;
+
+ boolean isSupported = mTunerSessions[0].isConfigFlagSupported(flag);
+
+ verify(mHalTunerSessionMock).isConfigFlagSet(eq(flag), any());
+ assertWithMessage("Config flag %s is supported", flag).that(isSupported).isFalse();
+ }
+
+ @Test
+ public void isConfigFlagSupported_withSupportedFlag_returnsTrue() throws Exception {
+ openAidlClients(/* numClients= */ 1);
+ int flag = UNSUPPORTED_CONFIG_FLAG + 1;
+
+ boolean isSupported = mTunerSessions[0].isConfigFlagSupported(flag);
+
+ verify(mHalTunerSessionMock).isConfigFlagSet(eq(flag), any());
+ assertWithMessage("Config flag %s is supported", flag).that(isSupported).isTrue();
+ }
+
+ @Test
+ public void setConfigFlag_withUnsupportedFlag_throwsRuntimeException() throws Exception {
+ openAidlClients(/* numClients= */ 1);
+ int flag = UNSUPPORTED_CONFIG_FLAG;
+
+ RuntimeException thrown = assertThrows(RuntimeException.class, () -> {
+ mTunerSessions[0].setConfigFlag(flag, /* value= */ true);
+ });
+
+ assertWithMessage("Exception for setting unsupported flag %s", flag)
+ .that(thrown).hasMessageThat().contains("setConfigFlag: NOT_SUPPORTED");
+ }
+
+ @Test
+ public void setConfigFlag_withFlagSetToTrue() throws Exception {
+ openAidlClients(/* numClients= */ 1);
+ int flag = UNSUPPORTED_CONFIG_FLAG + 1;
+
+ mTunerSessions[0].setConfigFlag(flag, /* value= */ true);
+
+ verify(mHalTunerSessionMock).setConfigFlag(flag, /* value= */ true);
+ }
+
+ @Test
+ public void setConfigFlag_withFlagSetToFalse() throws Exception {
+ openAidlClients(/* numClients= */ 1);
+ int flag = UNSUPPORTED_CONFIG_FLAG + 1;
+
+ mTunerSessions[0].setConfigFlag(flag, /* value= */ false);
+
+ verify(mHalTunerSessionMock).setConfigFlag(flag, /* value= */ false);
+ }
+
+ @Test
+ public void isConfigFlagSet_withUnsupportedFlag_throwsRuntimeException()
+ throws Exception {
+ openAidlClients(/* numClients= */ 1);
+ int flag = UNSUPPORTED_CONFIG_FLAG;
+
+ RuntimeException thrown = assertThrows(RuntimeException.class, () -> {
+ mTunerSessions[0].isConfigFlagSet(flag);
+ });
+
+ assertWithMessage("Exception for check if unsupported flag %s is set", flag)
+ .that(thrown).hasMessageThat().contains("isConfigFlagSet: NOT_SUPPORTED");
+ }
+
+ @Test
+ public void isConfigFlagSet_withSupportedFlag() throws Exception {
+ openAidlClients(/* numClients= */ 1);
+ int flag = UNSUPPORTED_CONFIG_FLAG + 1;
+ boolean expectedConfigFlagValue = true;
+ mTunerSessions[0].setConfigFlag(flag, /* value= */ expectedConfigFlagValue);
+
+ boolean isSet = mTunerSessions[0].isConfigFlagSet(flag);
+
+ assertWithMessage("Config flag %s is set", flag)
+ .that(isSet).isEqualTo(expectedConfigFlagValue);
+ }
+
+ @Test
+ public void setParameters_withMockParameters() throws Exception {
+ openAidlClients(/* numClients= */ 1);
+ Map<String, String> parametersSet = Map.of("mockParam1", "mockValue1",
+ "mockParam2", "mockValue2");
+
+ mTunerSessions[0].setParameters(parametersSet);
+
+ verify(mHalTunerSessionMock).setParameters(Convert.vendorInfoToHal(parametersSet));
+ }
+
+ @Test
+ public void getParameters_withMockKeys() throws Exception {
+ openAidlClients(/* numClients= */ 1);
+ ArrayList<String> parameterKeys = new ArrayList<>(Arrays.asList("mockKey1", "mockKey2"));
+
+ mTunerSessions[0].getParameters(parameterKeys);
+
+ verify(mHalTunerSessionMock).getParameters(parameterKeys);
+ }
+
+ @Test
+ public void onConfigFlagUpdated_forTunerCallback() throws Exception {
+ int numSessions = 3;
+ openAidlClients(numSessions);
+
+ mHalTunerCallback.onAntennaStateChange(/* connected= */ false);
+
+ for (int index = 0; index < numSessions; index++) {
+ verify(mAidlTunerCallbackMocks[index], CALLBACK_TIMEOUT)
+ .onAntennaState(/* connected= */ false);
+ }
+ }
+
+ @Test
+ public void onParametersUpdated_forTunerCallback() throws Exception {
+ int numSessions = 3;
+ openAidlClients(numSessions);
+ ArrayList<VendorKeyValue> parametersUpdates = new ArrayList<VendorKeyValue>(Arrays.asList(
+ TestUtils.makeVendorKeyValue("com.vendor.parameter1", "value1")));
+ Map<String, String> parametersExpected = Map.of("com.vendor.parameter1", "value1");
+
+ mHalTunerCallback.onParametersUpdated(parametersUpdates);
+
+ for (int index = 0; index < numSessions; index++) {
+ verify(mAidlTunerCallbackMocks[index], CALLBACK_TIMEOUT)
+ .onParametersUpdated(parametersExpected);
+ }
+ }
+
private void openAidlClients(int numClients) throws Exception {
mAidlTunerCallbackMocks = new android.hardware.radio.ITunerCallback[numClients];
mTunerSessions = new TunerSession[numClients];
diff --git a/core/tests/coretests/src/android/os/PerformanceHintManagerTest.java b/core/tests/coretests/src/android/os/PerformanceHintManagerTest.java
index 69eb13f..d1d14f6 100644
--- a/core/tests/coretests/src/android/os/PerformanceHintManagerTest.java
+++ b/core/tests/coretests/src/android/os/PerformanceHintManagerTest.java
@@ -114,6 +114,23 @@
}
@Test
+ public void testSendHint() {
+ Session s = createSession();
+ assumeNotNull(s);
+ s.sendHint(Session.CPU_LOAD_UP);
+ s.sendHint(Session.CPU_LOAD_RESET);
+ }
+
+ @Test
+ public void testSendHintWithNegativeHint() {
+ Session s = createSession();
+ assumeNotNull(s);
+ assertThrows(IllegalArgumentException.class, () -> {
+ s.sendHint(-1);
+ });
+ }
+
+ @Test
public void testCloseHintSession() {
Session s = createSession();
assumeNotNull(s);
diff --git a/core/tests/coretests/src/android/os/VibratorTest.java b/core/tests/coretests/src/android/os/VibratorTest.java
index 7ebebc9..c59a3f5 100644
--- a/core/tests/coretests/src/android/os/VibratorTest.java
+++ b/core/tests/coretests/src/android/os/VibratorTest.java
@@ -246,10 +246,12 @@
@Test
public void getQFactorAndResonantFrequency_differentValues_returnsNaN() {
VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
+ .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
.setQFactor(1f)
.setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 1, null))
.build();
VibratorInfo secondVibrator = new VibratorInfo.Builder(/* id= */ 2)
+ .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
.setQFactor(2f)
.setFrequencyProfile(new VibratorInfo.FrequencyProfile(2, 2, 2, null))
.build();
@@ -258,6 +260,7 @@
assertTrue(Float.isNaN(info.getQFactor()));
assertTrue(Float.isNaN(info.getResonantFrequencyHz()));
+ assertEmptyFrequencyProfileAndControl(info);
// One vibrator with values undefined.
VibratorInfo thirdVibrator = new VibratorInfo.Builder(/* id= */ 3).build();
@@ -266,16 +269,19 @@
assertTrue(Float.isNaN(info.getQFactor()));
assertTrue(Float.isNaN(info.getResonantFrequencyHz()));
+ assertEmptyFrequencyProfileAndControl(info);
}
@Test
public void getQFactorAndResonantFrequency_sameValues_returnsValue() {
VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
+ .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
.setQFactor(10f)
.setFrequencyProfile(new VibratorInfo.FrequencyProfile(
/* resonantFrequencyHz= */ 11, 10, 0.5f, null))
.build();
VibratorInfo secondVibrator = new VibratorInfo.Builder(/* id= */ 2)
+ .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
.setQFactor(10f)
.setFrequencyProfile(new VibratorInfo.FrequencyProfile(
/* resonantFrequencyHz= */ 11, 5, 1, null))
@@ -285,113 +291,131 @@
assertEquals(10f, info.getQFactor(), TEST_TOLERANCE);
assertEquals(11f, info.getResonantFrequencyHz(), TEST_TOLERANCE);
+
+ // No frequency range defined.
+ assertTrue(info.getFrequencyProfile().isEmpty());
+ assertEquals(false, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL));
}
@Test
public void getFrequencyProfile_noVibrator_returnsEmpty() {
VibratorInfo info = new SystemVibrator.NoVibratorInfo();
- assertTrue(info.getFrequencyProfile().isEmpty());
+ assertEmptyFrequencyProfileAndControl(info);
}
@Test
public void getFrequencyProfile_differentResonantFrequencyOrResolutionValues_returnsEmpty() {
VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
+ .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
.setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 1,
new float[] { 0, 1 }))
.build();
VibratorInfo differentResonantFrequency = new VibratorInfo.Builder(/* id= */ 2)
+ .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
.setFrequencyProfile(new VibratorInfo.FrequencyProfile(2, 1, 1,
new float[] { 0, 1 }))
.build();
VibratorInfo info = new SystemVibrator.MultiVibratorInfo(
new VibratorInfo[]{firstVibrator, differentResonantFrequency});
- assertTrue(info.getFrequencyProfile().isEmpty());
+ assertEmptyFrequencyProfileAndControl(info);
VibratorInfo differentFrequencyResolution = new VibratorInfo.Builder(/* id= */ 2)
+ .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
.setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 2,
new float[] { 0, 1 }))
.build();
info = new SystemVibrator.MultiVibratorInfo(
new VibratorInfo[]{firstVibrator, differentFrequencyResolution});
- assertTrue(info.getFrequencyProfile().isEmpty());
+ assertEmptyFrequencyProfileAndControl(info);
}
@Test
public void getFrequencyProfile_missingValues_returnsEmpty() {
VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
+ .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
.setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 1,
new float[] { 0, 1 }))
.build();
VibratorInfo missingResonantFrequency = new VibratorInfo.Builder(/* id= */ 2)
+ .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
.setFrequencyProfile(new VibratorInfo.FrequencyProfile(Float.NaN, 1, 1,
new float[] { 0, 1 }))
.build();
VibratorInfo info = new SystemVibrator.MultiVibratorInfo(
new VibratorInfo[]{firstVibrator, missingResonantFrequency});
- assertTrue(info.getFrequencyProfile().isEmpty());
+ assertEmptyFrequencyProfileAndControl(info);
VibratorInfo missingMinFrequency = new VibratorInfo.Builder(/* id= */ 2)
+ .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
.setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, Float.NaN, 1,
new float[] { 0, 1 }))
.build();
info = new SystemVibrator.MultiVibratorInfo(
new VibratorInfo[]{firstVibrator, missingMinFrequency});
- assertTrue(info.getFrequencyProfile().isEmpty());
+ assertEmptyFrequencyProfileAndControl(info);
VibratorInfo missingFrequencyResolution = new VibratorInfo.Builder(/* id= */ 2)
+ .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
.setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, Float.NaN,
new float[] { 0, 1 }))
.build();
info = new SystemVibrator.MultiVibratorInfo(
new VibratorInfo[]{firstVibrator, missingFrequencyResolution});
- assertTrue(info.getFrequencyProfile().isEmpty());
+ assertEmptyFrequencyProfileAndControl(info);
VibratorInfo missingMaxAmplitudes = new VibratorInfo.Builder(/* id= */ 2)
+ .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
.setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 1, null))
.build();
info = new SystemVibrator.MultiVibratorInfo(
new VibratorInfo[]{firstVibrator, missingMaxAmplitudes});
- assertTrue(info.getFrequencyProfile().isEmpty());
+ assertEmptyFrequencyProfileAndControl(info);
}
@Test
public void getFrequencyProfile_unalignedMaxAmplitudes_returnsEmpty() {
VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
+ .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
.setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10, 0.5f,
new float[] { 0, 1, 1, 0 }))
.build();
VibratorInfo unalignedMinFrequency = new VibratorInfo.Builder(/* id= */ 2)
+ .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
.setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.1f, 0.5f,
new float[] { 0, 1, 1, 0 }))
.build();
VibratorInfo thirdVibrator = new VibratorInfo.Builder(/* id= */ 2)
+ .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
.setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f,
new float[] { 0, 1, 1, 0 }))
.build();
VibratorInfo info = new SystemVibrator.MultiVibratorInfo(
new VibratorInfo[]{firstVibrator, unalignedMinFrequency, thirdVibrator});
- assertTrue(info.getFrequencyProfile().isEmpty());
+ assertEmptyFrequencyProfileAndControl(info);
}
@Test
public void getFrequencyProfile_alignedProfiles_returnsIntersection() {
VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
+ .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
.setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10, 0.5f,
new float[] { 0.5f, 1, 1, 0.5f }))
.build();
VibratorInfo secondVibrator = new VibratorInfo.Builder(/* id= */ 2)
+ .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
.setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f,
new float[] { 1, 1, 1 }))
.build();
VibratorInfo thirdVibrator = new VibratorInfo.Builder(/* id= */ 3)
+ .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
.setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f,
new float[] { 0.8f, 1, 0.8f, 0.5f }))
.build();
@@ -401,6 +425,20 @@
assertEquals(
new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f, new float[] { 0.8f, 1, 0.5f }),
info.getFrequencyProfile());
+ assertEquals(true, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL));
+
+ // Third vibrator without frequency control capability.
+ thirdVibrator = new VibratorInfo.Builder(/* id= */ 3)
+ .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f,
+ new float[] { 0.8f, 1, 0.8f, 0.5f }))
+ .build();
+ info = new SystemVibrator.MultiVibratorInfo(
+ new VibratorInfo[]{firstVibrator, secondVibrator, thirdVibrator});
+
+ assertEquals(
+ new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f, new float[] { 0.8f, 1, 0.5f }),
+ info.getFrequencyProfile());
+ assertEquals(false, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL));
}
@Test
@@ -547,4 +585,12 @@
VibrationAttributes vibrationAttributes = captor.getValue();
assertEquals(new VibrationAttributes.Builder().build(), vibrationAttributes);
}
+
+ /**
+ * Asserts that the frequency profile is empty, and therefore frequency control isn't supported.
+ */
+ void assertEmptyFrequencyProfileAndControl(VibratorInfo info) {
+ assertTrue(info.getFrequencyProfile().isEmpty());
+ assertEquals(false, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL));
+ }
}
diff --git a/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java b/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java
index ddcb175..0bf133f 100644
--- a/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java
+++ b/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java
@@ -24,6 +24,7 @@
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@@ -98,12 +99,12 @@
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
// test if setVisibility can show IME
mImeConsumer.onWindowFocusGained(true);
- mController.show(WindowInsets.Type.ime(), true /* fromIme */);
+ mController.show(WindowInsets.Type.ime(), true /* fromIme */, null /* statsToken */);
mController.cancelExistingAnimations();
assertTrue((mController.getRequestedVisibleTypes() & WindowInsets.Type.ime()) != 0);
// test if setVisibility can hide IME
- mController.hide(WindowInsets.Type.ime(), true /* fromIme */);
+ mController.hide(WindowInsets.Type.ime(), true /* fromIme */, null /* statsToken */);
mController.cancelExistingAnimations();
assertFalse((mController.getRequestedVisibleTypes() & WindowInsets.Type.ime()) != 0);
});
@@ -117,7 +118,7 @@
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
// Request IME visible before control is available.
mImeConsumer.onWindowFocusGained(true);
- mController.show(WindowInsets.Type.ime(), true /* fromIme */);
+ mController.show(WindowInsets.Type.ime(), true /* fromIme */, null /* statsToken */);
// set control and verify visibility is applied.
InsetsSourceControl control =
@@ -125,9 +126,11 @@
mController.onControlsChanged(new InsetsSourceControl[] { control });
// IME show animation should be triggered when control becomes available.
verify(mController).applyAnimation(
- eq(WindowInsets.Type.ime()), eq(true) /* show */, eq(true) /* fromIme */);
+ eq(WindowInsets.Type.ime()), eq(true) /* show */, eq(true) /* fromIme */,
+ any() /* statsToken */);
verify(mController, never()).applyAnimation(
- eq(WindowInsets.Type.ime()), eq(false) /* show */, eq(true) /* fromIme */);
+ eq(WindowInsets.Type.ime()), eq(false) /* show */, eq(true) /* fromIme */,
+ any() /* statsToken */);
});
}
@@ -153,7 +156,8 @@
mImeConsumer.onWindowFocusGained(hasWindowFocus);
final boolean imeVisible = hasWindowFocus && hasViewFocus;
if (imeVisible) {
- mController.show(WindowInsets.Type.ime(), true /* fromIme */);
+ mController.show(WindowInsets.Type.ime(), true /* fromIme */,
+ null /* statsToken */);
}
// set control and verify visibility is applied.
@@ -169,20 +173,21 @@
verify(control).getAndClearSkipAnimationOnce();
verify(mController).applyAnimation(eq(WindowInsets.Type.ime()),
eq(true) /* show */, eq(false) /* fromIme */,
- eq(expectSkipAnim) /* skipAnim */);
+ eq(expectSkipAnim) /* skipAnim */, null /* statsToken */);
}
// If previously hasViewFocus is false, verify when requesting the IME visible next
// time will not skip animation.
if (!hasViewFocus) {
- mController.show(WindowInsets.Type.ime(), true);
+ mController.show(WindowInsets.Type.ime(), true /* fromIme */,
+ null /* statsToken */);
mController.onControlsChanged(new InsetsSourceControl[]{ control });
// Verify IME show animation should be triggered when control becomes available and
// the animation will be skipped by getAndClearSkipAnimationOnce invoked.
verify(control).getAndClearSkipAnimationOnce();
verify(mController).applyAnimation(eq(WindowInsets.Type.ime()),
eq(true) /* show */, eq(true) /* fromIme */,
- eq(false) /* skipAnim */);
+ eq(false) /* skipAnim */, null /* statsToken */);
}
});
}
diff --git a/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java b/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java
index e9cd8ad..c88255e 100644
--- a/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java
+++ b/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java
@@ -109,7 +109,8 @@
mController = new InsetsAnimationControlImpl(controls,
new Rect(0, 0, 500, 500), mInsetsState, mMockListener, systemBars(),
mMockController, 10 /* durationMs */, new LinearInterpolator(),
- 0 /* animationType */, 0 /* layoutInsetsDuringAnimation */, null /* translator */);
+ 0 /* animationType */, 0 /* layoutInsetsDuringAnimation */, null /* translator */,
+ null /* statsToken */);
mController.setReadyDispatched(true);
}
diff --git a/core/tests/coretests/src/android/view/InsetsControllerTest.java b/core/tests/coretests/src/android/view/InsetsControllerTest.java
index 409bae8..c6fa778 100644
--- a/core/tests/coretests/src/android/view/InsetsControllerTest.java
+++ b/core/tests/coretests/src/android/view/InsetsControllerTest.java
@@ -248,7 +248,7 @@
mController.setSystemDrivenInsetsAnimationLoggingListener(loggingListener);
mController.getSourceConsumer(ITYPE_IME).onWindowFocusGained(true);
// since there is no focused view, forcefully make IME visible.
- mController.show(ime(), true /* fromIme */);
+ mController.show(WindowInsets.Type.ime(), true /* fromIme */, null /* statsToken */);
verify(loggingListener).onReady(notNull(), anyInt());
});
}
@@ -260,14 +260,14 @@
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
mController.getSourceConsumer(ITYPE_IME).onWindowFocusGained(true);
// since there is no focused view, forcefully make IME visible.
- mController.show(ime(), true /* fromIme */);
+ mController.show(WindowInsets.Type.ime(), true /* fromIme */, null /* statsToken */);
mController.show(all());
// quickly jump to final state by cancelling it.
mController.cancelExistingAnimations();
final @InsetsType int types = navigationBars() | statusBars() | ime();
assertEquals(types, mController.getRequestedVisibleTypes() & types);
- mController.hide(ime(), true /* fromIme */);
+ mController.hide(ime(), true /* fromIme */, null /* statsToken */);
mController.hide(all());
mController.cancelExistingAnimations();
assertEquals(0, mController.getRequestedVisibleTypes() & types);
@@ -282,10 +282,10 @@
mController.onControlsChanged(new InsetsSourceControl[] { ime });
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
mController.getSourceConsumer(ITYPE_IME).onWindowFocusGained(true);
- mController.show(ime(), true /* fromIme */);
+ mController.show(WindowInsets.Type.ime(), true /* fromIme */, null /* statsToken */);
mController.cancelExistingAnimations();
assertTrue(isRequestedVisible(mController, ime()));
- mController.hide(ime(), true /* fromIme */);
+ mController.hide(ime(), true /* fromIme */, null /* statsToken */);
mController.cancelExistingAnimations();
assertFalse(isRequestedVisible(mController, ime()));
mController.getSourceConsumer(ITYPE_IME).onWindowFocusLost();
@@ -452,7 +452,7 @@
assertFalse(mController.getState().getSource(ITYPE_IME).isVisible());
// Pretend IME is calling
- mController.show(ime(), true /* fromIme */);
+ mController.show(ime(), true /* fromIme */, null /* statsToken */);
// Gaining control shortly after
mController.onControlsChanged(createSingletonControl(ITYPE_IME));
@@ -476,7 +476,7 @@
mController.onControlsChanged(createSingletonControl(ITYPE_IME));
// Pretend IME is calling
- mController.show(ime(), true /* fromIme */);
+ mController.show(ime(), true /* fromIme */, null /* statsToken */);
assertEquals(ANIMATION_TYPE_SHOW, mController.getAnimationType(ime()));
mController.cancelExistingAnimations();
@@ -554,7 +554,7 @@
verify(listener, never()).onReady(any(), anyInt());
// Pretend that IME is calling.
- mController.show(ime(), true);
+ mController.show(ime(), true /* fromIme */, null /* statsToken */);
// Ready gets deferred until next predraw
mViewRoot.getView().getViewTreeObserver().dispatchOnPreDraw();
@@ -638,7 +638,7 @@
mController.onControlsChanged(createSingletonControl(ITYPE_IME));
// Pretend IME is calling
- mController.show(ime(), true /* fromIme */);
+ mController.show(ime(), true /* fromIme */, null /* statsToken */);
InsetsState copy = new InsetsState(mController.getState(), true /* copySources */);
copy.getSource(ITYPE_IME).setFrame(0, 1, 2, 3);
@@ -845,7 +845,7 @@
// Showing invisible ime should only causes insets change once.
clearInvocations(mTestHost);
- mController.show(ime(), true /* fromIme */);
+ mController.show(ime(), true /* fromIme */, null /* statsToken */);
verify(mTestHost, times(1)).notifyInsetsChanged();
// Sending the same insets state should not cause insets change.
diff --git a/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java b/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java
index 32085c1..cc02bbb 100644
--- a/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java
+++ b/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java
@@ -46,7 +46,7 @@
// The number of fields tested in the corresponding CTS AccessibilityNodeInfoTest:
// See fullyPopulateAccessibilityNodeInfo, assertEqualsAccessibilityNodeInfo,
// and assertAccessibilityNodeInfoCleared in that class.
- private static final int NUM_MARSHALLED_PROPERTIES = 41;
+ private static final int NUM_MARSHALLED_PROPERTIES = 42;
/**
* The number of properties that are purposely not marshalled
diff --git a/graphics/java/android/graphics/fonts/FontStyle.java b/graphics/java/android/graphics/fonts/FontStyle.java
index 09799fd..48969aa 100644
--- a/graphics/java/android/graphics/fonts/FontStyle.java
+++ b/graphics/java/android/graphics/fonts/FontStyle.java
@@ -48,6 +48,10 @@
private static final String TAG = "FontStyle";
/**
+ * A default value when font weight is unspecified
+ */
+ public static final int FONT_WEIGHT_UNSPECIFIED = -1;
+ /**
* A minimum weight value for the font
*/
public static final int FONT_WEIGHT_MIN = 1;
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index 16760e26..a7fa2d9 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -96,7 +96,7 @@
ActivityEmbeddingComponent {
static final String TAG = "SplitController";
static final boolean ENABLE_SHELL_TRANSITIONS =
- SystemProperties.getBoolean("persist.wm.debug.shell_transit", false);
+ SystemProperties.getBoolean("persist.wm.debug.shell_transit", true);
@VisibleForTesting
@GuardedBy("mLock")
diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml
index df5f921..c6197c8 100644
--- a/libs/WindowManager/Shell/res/values/config.xml
+++ b/libs/WindowManager/Shell/res/values/config.xml
@@ -111,4 +111,8 @@
<!-- Whether to dim a split-screen task when the other is the IME target -->
<bool name="config_dimNonImeAttachedSide">true</bool>
+
+ <!-- Components support to launch multiple instances into split-screen -->
+ <string-array name="config_componentsSupportMultiInstancesSplit">
+ </string-array>
</resources>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index b6327e5..d9eaeee 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -38,7 +38,6 @@
import android.provider.Settings.Global;
import android.util.Log;
import android.util.SparseArray;
-import android.view.IRemoteAnimationFinishedCallback;
import android.view.IRemoteAnimationRunner;
import android.view.IWindowFocusObserver;
import android.view.InputDevice;
@@ -69,7 +68,7 @@
* Controls the window animation run when a user initiates a back gesture.
*/
public class BackAnimationController implements RemoteCallable<BackAnimationController> {
- private static final String TAG = "BackAnimationController";
+ private static final String TAG = "ShellBackPreview";
private static final int SETTING_VALUE_OFF = 0;
private static final int SETTING_VALUE_ON = 1;
public static final boolean IS_ENABLED =
@@ -82,16 +81,15 @@
/** Predictive back animation developer option */
private final AtomicBoolean mEnableAnimations = new AtomicBoolean(false);
/**
- * Max duration to wait for a transition to finish before accepting another gesture start
- * request.
+ * Max duration to wait for an animation to finish before triggering the real back.
*/
- private static final long MAX_TRANSITION_DURATION = 2000;
+ private static final long MAX_ANIMATION_DURATION = 2000;
/** True when a back gesture is ongoing */
private boolean mBackGestureStarted = false;
- /** Tracks if an uninterruptible transition is in progress */
- private boolean mTransitionInProgress = false;
+ /** Tracks if an uninterruptible animation is in progress */
+ private boolean mPostCommitAnimationInProgress = false;
/** Tracks if we should start the back gesture on the next motion move event */
private boolean mShouldStartOnNextMoveEvent = false;
/** @see #setTriggerBack(boolean) */
@@ -105,9 +103,9 @@
private final ShellController mShellController;
private final ShellExecutor mShellExecutor;
private final Handler mBgHandler;
- private final Runnable mResetTransitionRunnable = () -> {
- ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Transition didn't finish in %d ms. Resetting...",
- MAX_TRANSITION_DURATION);
+ private final Runnable mAnimationTimeoutRunnable = () -> {
+ ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Animation didn't finish in %d ms. Resetting...",
+ MAX_ANIMATION_DURATION);
onBackAnimationFinished();
};
@@ -119,6 +117,8 @@
private final SparseArray<BackAnimationRunner> mAnimationDefinition = new SparseArray<>();
+ private IOnBackInvokedCallback mActiveCallback;
+
@VisibleForTesting
final IWindowFocusObserver mFocusObserver = new IWindowFocusObserver.Stub() {
@Override
@@ -126,9 +126,9 @@
@Override
public void focusLost(IBinder inputToken) {
mShellExecutor.execute(() -> {
- if (!mBackGestureStarted || mTransitionInProgress) {
- // If an uninterruptible transition is already in progress, we should ignore
- // this due to the transition may cause focus lost. (alpha = 0)
+ if (!mBackGestureStarted || mPostCommitAnimationInProgress) {
+ // If an uninterruptible animation is already in progress, we should ignore
+ // this due to it may cause focus lost. (alpha = 0)
return;
}
ProtoLog.i(WM_SHELL_BACK_PREVIEW, "Target window lost focus.");
@@ -180,26 +180,11 @@
}
private void initBackAnimationRunners() {
- final IOnBackInvokedCallback dummyCallback = new IOnBackInvokedCallback.Default();
- final IRemoteAnimationRunner dummyRunner = new IRemoteAnimationRunner.Default() {
- @Override
- public void onAnimationStart(int transit, RemoteAnimationTarget[] apps,
- RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps,
- IRemoteAnimationFinishedCallback finishedCallback) throws RemoteException {
- // Animation missing. Simply finish animation.
- finishedCallback.onAnimationFinished();
- }
- };
-
- final BackAnimationRunner dummyBackRunner =
- new BackAnimationRunner(dummyCallback, dummyRunner);
final CrossTaskBackAnimation crossTaskAnimation = new CrossTaskBackAnimation(mContext);
mAnimationDefinition.set(BackNavigationInfo.TYPE_CROSS_TASK,
new BackAnimationRunner(crossTaskAnimation.mCallback, crossTaskAnimation.mRunner));
// TODO (238474994): register cross activity animation when it's completed.
- mAnimationDefinition.set(BackNavigationInfo.TYPE_CROSS_ACTIVITY, dummyBackRunner);
// TODO (236760237): register dialog close animation when it's completed.
- mAnimationDefinition.set(BackNavigationInfo.TYPE_DIALOG_CLOSE, dummyBackRunner);
}
private void setupAnimationDeveloperSettingsObserver(
@@ -222,10 +207,9 @@
private void updateEnableAnimationFromSetting() {
int settingValue = Global.getInt(mContext.getContentResolver(),
Global.ENABLE_BACK_ANIMATION, SETTING_VALUE_OFF);
- boolean isEnabled = settingValue == SETTING_VALUE_ON;
+ boolean isEnabled = settingValue == SETTING_VALUE_ON && IS_U_ANIMATION_ENABLED;
mEnableAnimations.set(isEnabled);
- ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Back animation enabled=%s",
- isEnabled);
+ ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Back animation enabled=%s", isEnabled);
}
public BackAnimation getBackAnimationImpl() {
@@ -279,7 +263,9 @@
public void setBackToLauncherCallback(IOnBackInvokedCallback callback,
IRemoteAnimationRunner runner) {
executeRemoteCallWithTaskPermission(mController, "setBackToLauncherCallback",
- (controller) -> controller.setBackToLauncherCallback(callback, runner));
+ (controller) -> controller.registerAnimation(
+ BackNavigationInfo.TYPE_RETURN_TO_HOME,
+ new BackAnimationRunner(callback, runner)));
}
@Override
@@ -294,44 +280,22 @@
}
}
- @VisibleForTesting
- void setBackToLauncherCallback(IOnBackInvokedCallback callback, IRemoteAnimationRunner runner) {
- mAnimationDefinition.set(BackNavigationInfo.TYPE_RETURN_TO_HOME,
- new BackAnimationRunner(callback, runner));
+ void registerAnimation(@BackNavigationInfo.BackTargetType int type,
+ @NonNull BackAnimationRunner runner) {
+ mAnimationDefinition.set(type, runner);
}
private void clearBackToLauncherCallback() {
mAnimationDefinition.remove(BackNavigationInfo.TYPE_RETURN_TO_HOME);
}
- @VisibleForTesting
- void onBackAnimationFinished() {
- if (!mTransitionInProgress) {
- return;
- }
-
- ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: onBackAnimationFinished()");
-
- // Trigger real back.
- if (mBackNavigationInfo != null) {
- IOnBackInvokedCallback callback = mBackNavigationInfo.getOnBackInvokedCallback();
- if (mTriggerBack) {
- dispatchOnBackInvoked(callback);
- } else {
- dispatchOnBackCancelled(callback);
- }
- }
-
- finishBackNavigation();
- }
-
/**
* Called when a new motion event needs to be transferred to this
* {@link BackAnimationController}
*/
public void onMotionEvent(float touchX, float touchY, int keyAction,
@BackEvent.SwipeEdge int swipeEdge) {
- if (mTransitionInProgress) {
+ if (mPostCommitAnimationInProgress) {
return;
}
@@ -348,7 +312,7 @@
onGestureStarted(touchX, touchY, swipeEdge);
mShouldStartOnNextMoveEvent = false;
}
- onMove(touchX, touchY, swipeEdge);
+ onMove();
} else if (keyAction == MotionEvent.ACTION_UP || keyAction == MotionEvent.ACTION_CANCEL) {
ProtoLog.d(WM_SHELL_BACK_PREVIEW,
"Finishing gesture with event action: %d", keyAction);
@@ -386,30 +350,22 @@
return;
}
final int backType = backNavigationInfo.getType();
- final IOnBackInvokedCallback targetCallback;
final boolean shouldDispatchToAnimator = shouldDispatchToAnimator(backType);
if (shouldDispatchToAnimator) {
+ mActiveCallback = mAnimationDefinition.get(backType).getCallback();
mAnimationDefinition.get(backType).startGesture();
} else {
- targetCallback = mBackNavigationInfo.getOnBackInvokedCallback();
- dispatchOnBackStarted(targetCallback, mTouchTracker.createStartEvent(null));
+ mActiveCallback = mBackNavigationInfo.getOnBackInvokedCallback();
+ dispatchOnBackStarted(mActiveCallback, mTouchTracker.createStartEvent(null));
}
}
- private void onMove(float touchX, float touchY, @BackEvent.SwipeEdge int swipeEdge) {
+ private void onMove() {
if (!mBackGestureStarted || mBackNavigationInfo == null || !mEnableAnimations.get()) {
return;
}
final BackEvent backEvent = mTouchTracker.createProgressEvent();
-
- int backType = mBackNavigationInfo.getType();
- IOnBackInvokedCallback targetCallback;
- if (shouldDispatchToAnimator(backType)) {
- targetCallback = mAnimationDefinition.get(backType).getCallback();
- } else {
- targetCallback = mBackNavigationInfo.getOnBackInvokedCallback();
- }
- dispatchOnBackProgressed(targetCallback, backEvent);
+ dispatchOnBackProgressed(mActiveCallback, backEvent);
}
private void injectBackKey() {
@@ -431,57 +387,6 @@
}
}
- private void onGestureFinished(boolean fromTouch) {
- ProtoLog.d(WM_SHELL_BACK_PREVIEW, "onGestureFinished() mTriggerBack == %s", mTriggerBack);
- if (!mBackGestureStarted) {
- finishBackNavigation();
- return;
- }
-
- if (fromTouch) {
- // Let touch reset the flag otherwise it will start a new back navigation and refresh
- // the info when received a new move event.
- mBackGestureStarted = false;
- }
-
- if (mTransitionInProgress) {
- return;
- }
-
- if (mBackNavigationInfo == null) {
- // No focus window found or core are running recents animation, inject back key as
- // legacy behavior.
- if (mTriggerBack) {
- injectBackKey();
- }
- finishBackNavigation();
- return;
- }
-
- int backType = mBackNavigationInfo.getType();
- boolean shouldDispatchToAnimator = shouldDispatchToAnimator(backType);
- final BackAnimationRunner runner = mAnimationDefinition.get(backType);
- IOnBackInvokedCallback targetCallback = shouldDispatchToAnimator
- ? runner.getCallback() : mBackNavigationInfo.getOnBackInvokedCallback();
-
- if (shouldDispatchToAnimator) {
- if (runner.onGestureFinished(mTriggerBack)) {
- Log.w(TAG, "Gesture released, but animation didn't ready.");
- return;
- }
- startTransition();
- }
- if (mTriggerBack) {
- dispatchOnBackInvoked(targetCallback);
- } else {
- dispatchOnBackCancelled(targetCallback);
- }
- if (!shouldDispatchToAnimator) {
- // Animation callback missing. Simply finish animation.
- finishBackNavigation();
- }
- }
-
private boolean shouldDispatchToAnimator(int backType) {
return mEnableAnimations.get()
&& mBackNavigationInfo != null
@@ -495,7 +400,7 @@
return;
}
try {
- if (shouldDispatchAnimation(callback)) {
+ if (mEnableAnimations.get()) {
callback.onBackStarted(backEvent);
}
} catch (RemoteException e) {
@@ -519,7 +424,7 @@
return;
}
try {
- if (shouldDispatchAnimation(callback)) {
+ if (mEnableAnimations.get()) {
callback.onBackCancelled();
}
} catch (RemoteException e) {
@@ -533,7 +438,7 @@
return;
}
try {
- if (shouldDispatchAnimation(callback)) {
+ if (mEnableAnimations.get()) {
callback.onBackProgressed(backEvent);
}
} catch (RemoteException e) {
@@ -541,17 +446,11 @@
}
}
- private boolean shouldDispatchAnimation(IOnBackInvokedCallback callback) {
- return (IS_U_ANIMATION_ENABLED || callback == mAnimationDefinition.get(
- BackNavigationInfo.TYPE_RETURN_TO_HOME).getCallback())
- && mEnableAnimations.get();
- }
-
/**
* Sets to true when the back gesture has passed the triggering threshold, false otherwise.
*/
public void setTriggerBack(boolean triggerBack) {
- if (mTransitionInProgress) {
+ if (mPostCommitAnimationInProgress) {
return;
}
mTriggerBack = triggerBack;
@@ -562,6 +461,109 @@
mTouchTracker.setProgressThreshold(progressThreshold);
}
+ /**
+ * Called when the gesture is released, then it could start the post commit animation.
+ */
+ private void onGestureFinished(boolean fromTouch) {
+ ProtoLog.d(WM_SHELL_BACK_PREVIEW, "onGestureFinished() mTriggerBack == %s", mTriggerBack);
+ if (!mBackGestureStarted) {
+ finishBackNavigation();
+ return;
+ }
+
+ if (fromTouch) {
+ // Let touch reset the flag otherwise it will start a new back navigation and refresh
+ // the info when received a new move event.
+ mBackGestureStarted = false;
+ }
+
+ if (mPostCommitAnimationInProgress) {
+ ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Animation is still running");
+ return;
+ }
+
+ if (mBackNavigationInfo == null) {
+ // No focus window found or core are running recents animation, inject back key as
+ // legacy behavior.
+ if (mTriggerBack) {
+ injectBackKey();
+ }
+ finishBackNavigation();
+ return;
+ }
+
+ final int backType = mBackNavigationInfo.getType();
+ // Directly finish back navigation if no animator defined.
+ if (!shouldDispatchToAnimator(backType)) {
+ if (mTriggerBack) {
+ dispatchOnBackInvoked(mActiveCallback);
+ } else {
+ dispatchOnBackCancelled(mActiveCallback);
+ }
+ // Animation missing. Simply finish back navigation.
+ finishBackNavigation();
+ return;
+ }
+
+ final BackAnimationRunner runner = mAnimationDefinition.get(backType);
+ if (runner.isWaitingAnimation()) {
+ ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Gesture released, but animation didn't ready.");
+ return;
+ }
+ startPostCommitAnimation();
+ }
+
+ /**
+ * Start the phase 2 animation when gesture is released.
+ * Callback to {@link #onBackAnimationFinished} when it is finished or timeout.
+ */
+ private void startPostCommitAnimation() {
+ if (mPostCommitAnimationInProgress) {
+ return;
+ }
+ ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: startPostCommitAnimation()");
+ mPostCommitAnimationInProgress = true;
+ mShellExecutor.executeDelayed(mAnimationTimeoutRunnable, MAX_ANIMATION_DURATION);
+
+ // The next callback should be {@link #onBackAnimationFinished}.
+ if (mTriggerBack) {
+ dispatchOnBackInvoked(mActiveCallback);
+ } else {
+ dispatchOnBackCancelled(mActiveCallback);
+ }
+ }
+
+ /**
+ * Called when the post commit animation is completed or timeout.
+ * This will trigger the real {@link IOnBackInvokedCallback} behavior.
+ */
+ @VisibleForTesting
+ void onBackAnimationFinished() {
+ if (!mPostCommitAnimationInProgress) {
+ return;
+ }
+ // Stop timeout runner.
+ mShellExecutor.removeCallbacks(mAnimationTimeoutRunnable);
+ mPostCommitAnimationInProgress = false;
+
+ ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: onBackAnimationFinished()");
+
+ // Trigger the real back.
+ if (mBackNavigationInfo != null) {
+ IOnBackInvokedCallback callback = mBackNavigationInfo.getOnBackInvokedCallback();
+ if (mTriggerBack) {
+ dispatchOnBackInvoked(callback);
+ } else {
+ dispatchOnBackCancelled(callback);
+ }
+ }
+
+ finishBackNavigation();
+ }
+
+ /**
+ * This should be called after the whole back navigation is completed.
+ */
@VisibleForTesting
void finishBackNavigation() {
ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: finishBackNavigation()");
@@ -571,10 +573,10 @@
mTriggerBack = false;
mShouldStartOnNextMoveEvent = false;
mTouchTracker.reset();
+ mActiveCallback = null;
if (backNavigationInfo == null) {
return;
}
- stopTransition();
if (mBackAnimationFinishedCallback != null) {
try {
mBackAnimationFinishedCallback.onAnimationFinished(triggerBack);
@@ -586,19 +588,6 @@
backNavigationInfo.onBackNavigationFinished(triggerBack);
}
- private void startTransition() {
- if (mTransitionInProgress) {
- return;
- }
- mTransitionInProgress = true;
- mShellExecutor.executeDelayed(mResetTransitionRunnable, MAX_TRANSITION_DURATION);
- }
-
- void stopTransition() {
- mShellExecutor.removeCallbacks(mResetTransitionRunnable);
- mTransitionInProgress = false;
- }
-
private void createAdapter() {
IBackAnimationRunner runner = new IBackAnimationRunner.Stub() {
@Override
@@ -624,20 +613,18 @@
ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: startAnimation()");
runner.startAnimation(apps, wallpapers, nonApps,
BackAnimationController.this::onBackAnimationFinished);
+
if (apps.length >= 1) {
- final int backType = mBackNavigationInfo.getType();
- IOnBackInvokedCallback targetCallback = mAnimationDefinition.get(backType)
- .getCallback();
dispatchOnBackStarted(
- targetCallback, mTouchTracker.createStartEvent(apps[0]));
+ mActiveCallback, mTouchTracker.createStartEvent(apps[0]));
}
if (!mBackGestureStarted) {
// if the down -> up gesture happened before animation start, we have to
// trigger the uninterruptible transition to finish the back animation.
- final BackEvent backFinish = mTouchTracker.createProgressEvent(1);
- startTransition();
- runner.consumeIfGestureFinished(backFinish);
+ final BackEvent backFinish = mTouchTracker.createProgressEvent();
+ dispatchOnBackProgressed(mActiveCallback, backFinish);
+ startPostCommitAnimation();
}
});
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java
index c53fcfc..d70b8f5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java
@@ -18,12 +18,12 @@
import static android.view.WindowManager.TRANSIT_OLD_UNSET;
+import android.annotation.NonNull;
import android.os.RemoteException;
import android.util.Log;
import android.view.IRemoteAnimationFinishedCallback;
import android.view.IRemoteAnimationRunner;
import android.view.RemoteAnimationTarget;
-import android.window.BackEvent;
import android.window.IBackAnimationRunner;
import android.window.IOnBackInvokedCallback;
@@ -38,11 +38,11 @@
private final IOnBackInvokedCallback mCallback;
private final IRemoteAnimationRunner mRunner;
- private boolean mTriggerBack;
// Whether we are waiting to receive onAnimationStart
private boolean mWaitingAnimation;
- BackAnimationRunner(IOnBackInvokedCallback callback, IRemoteAnimationRunner runner) {
+ BackAnimationRunner(@NonNull IOnBackInvokedCallback callback,
+ @NonNull IRemoteAnimationRunner runner) {
mCallback = callback;
mRunner = runner;
}
@@ -83,25 +83,7 @@
mWaitingAnimation = true;
}
- boolean onGestureFinished(boolean triggerBack) {
- if (mWaitingAnimation) {
- mTriggerBack = triggerBack;
- return true;
- }
- return false;
- }
-
- void consumeIfGestureFinished(final BackEvent backFinish) {
- Log.d(TAG, "Start transition due to gesture is finished");
- try {
- mCallback.onBackProgressed(backFinish);
- if (mTriggerBack) {
- mCallback.onBackInvoked();
- } else {
- mCallback.onBackCancelled();
- }
- } catch (RemoteException e) {
- Log.e(TAG, "dispatch error: ", e);
- }
+ boolean isWaitingAnimation() {
+ return mWaitingAnimation;
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
index bb7c4134..d9b4f47 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
@@ -20,6 +20,7 @@
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.IntDef;
+import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.res.Configuration;
import android.graphics.Point;
@@ -38,6 +39,7 @@
import android.view.WindowInsets.Type.InsetsType;
import android.view.animation.Interpolator;
import android.view.animation.PathInterpolator;
+import android.view.inputmethod.ImeTracker;
import android.view.inputmethod.InputMethodManagerGlobal;
import androidx.annotation.VisibleForTesting;
@@ -112,7 +114,7 @@
}
if (mDisplayController.getDisplayLayout(displayId).rotation()
!= pd.mRotation && isImeShowing(displayId)) {
- pd.startAnimation(true, false /* forceRestart */);
+ pd.startAnimation(true, false /* forceRestart */, null /* statsToken */);
}
}
@@ -244,7 +246,7 @@
mInsetsState.set(insetsState, true /* copySources */);
if (mImeShowing && !newFrame.equals(oldFrame) && newSource.isVisible()) {
if (DEBUG) Slog.d(TAG, "insetsChanged when IME showing, restart animation");
- startAnimation(mImeShowing, true /* forceRestart */);
+ startAnimation(mImeShowing, true /* forceRestart */, null /* statsToken */);
}
}
@@ -280,7 +282,7 @@
!haveSameLeash(mImeSourceControl, imeSourceControl);
if (mAnimation != null) {
if (positionChanged) {
- startAnimation(mImeShowing, true /* forceRestart */);
+ startAnimation(mImeShowing, true /* forceRestart */, null /* statsToken */);
}
} else {
if (leashChanged) {
@@ -312,21 +314,23 @@
}
@Override
- public void showInsets(int types, boolean fromIme) {
+ public void showInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
if ((types & WindowInsets.Type.ime()) == 0) {
return;
}
if (DEBUG) Slog.d(TAG, "Got showInsets for ime");
- startAnimation(true /* show */, false /* forceRestart */);
+ startAnimation(true /* show */, false /* forceRestart */, statsToken);
}
@Override
- public void hideInsets(int types, boolean fromIme) {
+ public void hideInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
if ((types & WindowInsets.Type.ime()) == 0) {
return;
}
if (DEBUG) Slog.d(TAG, "Got hideInsets for ime");
- startAnimation(false /* show */, false /* forceRestart */);
+ startAnimation(false /* show */, false /* forceRestart */, statsToken);
}
@Override
@@ -367,9 +371,11 @@
.navBarFrameHeight();
}
- private void startAnimation(final boolean show, final boolean forceRestart) {
+ private void startAnimation(final boolean show, final boolean forceRestart,
+ @Nullable ImeTracker.Token statsToken) {
final InsetsSource imeSource = mInsetsState.getSource(InsetsState.ITYPE_IME);
if (imeSource == null || mImeSourceControl == null) {
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_WM_ANIMATION_CREATE);
return;
}
final Rect newFrame = imeSource.getFrame();
@@ -390,8 +396,9 @@
+ (mAnimationDirection == DIRECTION_SHOW ? "SHOW"
: (mAnimationDirection == DIRECTION_HIDE ? "HIDE" : "NONE")));
}
- if (!forceRestart && (mAnimationDirection == DIRECTION_SHOW && show)
+ if ((!forceRestart && (mAnimationDirection == DIRECTION_SHOW && show))
|| (mAnimationDirection == DIRECTION_HIDE && !show)) {
+ ImeTracker.get().onCancelled(statsToken, ImeTracker.PHASE_WM_ANIMATION_CREATE);
return;
}
boolean seek = false;
@@ -435,8 +442,11 @@
mTransactionPool.release(t);
});
mAnimation.setInterpolator(INTERPOLATOR);
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_WM_ANIMATION_CREATE);
mAnimation.addListener(new AnimatorListenerAdapter() {
private boolean mCancelled = false;
+ @Nullable
+ private final ImeTracker.Token mStatsToken = statsToken;
@Override
public void onAnimationStart(Animator animation) {
@@ -455,6 +465,8 @@
: 1.f;
t.setAlpha(mImeSourceControl.getLeash(), alpha);
if (mAnimationDirection == DIRECTION_SHOW) {
+ ImeTracker.get().onProgress(mStatsToken,
+ ImeTracker.PHASE_WM_ANIMATION_RUNNING);
t.show(mImeSourceControl.getLeash());
}
t.apply();
@@ -476,8 +488,16 @@
}
dispatchEndPositioning(mDisplayId, mCancelled, t);
if (mAnimationDirection == DIRECTION_HIDE && !mCancelled) {
+ ImeTracker.get().onProgress(mStatsToken,
+ ImeTracker.PHASE_WM_ANIMATION_RUNNING);
t.hide(mImeSourceControl.getLeash());
removeImeSurface();
+ ImeTracker.get().onHidden(mStatsToken);
+ } else if (mAnimationDirection == DIRECTION_SHOW && !mCancelled) {
+ ImeTracker.get().onShown(mStatsToken);
+ } else if (mCancelled) {
+ ImeTracker.get().onCancelled(mStatsToken,
+ ImeTracker.PHASE_WM_ANIMATION_RUNNING);
}
t.apply();
mTransactionPool.release(t);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java
index 8d4a09d..8759301 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java
@@ -16,6 +16,7 @@
package com.android.wm.shell.common;
+import android.annotation.Nullable;
import android.content.ComponentName;
import android.os.RemoteException;
import android.util.Slog;
@@ -25,6 +26,7 @@
import android.view.InsetsSourceControl;
import android.view.InsetsState;
import android.view.WindowInsets.Type.InsetsType;
+import android.view.inputmethod.ImeTracker;
import androidx.annotation.BinderThread;
@@ -156,23 +158,29 @@
}
}
- private void showInsets(int types, boolean fromIme) {
+ private void showInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId);
if (listeners == null) {
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_WM_REMOTE_INSETS_CONTROLLER);
return;
}
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_WM_REMOTE_INSETS_CONTROLLER);
for (OnInsetsChangedListener listener : listeners) {
- listener.showInsets(types, fromIme);
+ listener.showInsets(types, fromIme, statsToken);
}
}
- private void hideInsets(int types, boolean fromIme) {
+ private void hideInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId);
if (listeners == null) {
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_WM_REMOTE_INSETS_CONTROLLER);
return;
}
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_WM_REMOTE_INSETS_CONTROLLER);
for (OnInsetsChangedListener listener : listeners) {
- listener.hideInsets(types, fromIme);
+ listener.hideInsets(types, fromIme, statsToken);
}
}
@@ -214,16 +222,18 @@
}
@Override
- public void showInsets(int types, boolean fromIme) throws RemoteException {
+ public void showInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) throws RemoteException {
mMainExecutor.execute(() -> {
- PerDisplay.this.showInsets(types, fromIme);
+ PerDisplay.this.showInsets(types, fromIme, statsToken);
});
}
@Override
- public void hideInsets(int types, boolean fromIme) throws RemoteException {
+ public void hideInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) throws RemoteException {
mMainExecutor.execute(() -> {
- PerDisplay.this.hideInsets(types, fromIme);
+ PerDisplay.this.hideInsets(types, fromIme, statsToken);
});
}
}
@@ -263,15 +273,21 @@
*
* @param types {@link InsetsType} to show
* @param fromIme true if this request originated from IME (InputMethodService).
+ * @param statsToken the token tracking the current IME show request
+ * or {@code null} otherwise.
*/
- default void showInsets(@InsetsType int types, boolean fromIme) {}
+ default void showInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {}
/**
* Called when a set of insets source window should be hidden by policy.
*
* @param types {@link InsetsType} to hide
* @param fromIme true if this request originated from IME (InputMethodService).
+ * @param statsToken the token tracking the current IME hide request
+ * or {@code null} otherwise.
*/
- default void hideInsets(@InsetsType int types, boolean fromIme) {}
+ default void hideInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {}
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
index e270edb..af13bf5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
@@ -19,6 +19,7 @@
import static android.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Region;
@@ -46,6 +47,7 @@
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.WindowlessWindowManager;
+import android.view.inputmethod.ImeTracker;
import android.window.ClientWindowFrames;
import com.android.internal.os.IResultReceiver;
@@ -351,10 +353,10 @@
InsetsSourceControl[] activeControls) {}
@Override
- public void showInsets(int types, boolean fromIme) {}
+ public void showInsets(int types, boolean fromIme, @Nullable ImeTracker.Token statsToken) {}
@Override
- public void hideInsets(int types, boolean fromIme) {}
+ public void hideInsets(int types, boolean fromIme, @Nullable ImeTracker.Token statsToken) {}
@Override
public void moved(int newX, int newY) {}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
index 839edc8..3de1045 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
@@ -601,7 +601,7 @@
animator.start();
}
- /** Swich both surface position with animation. */
+ /** Switch both surface position with animation. */
public void splitSwitching(SurfaceControl.Transaction t, SurfaceControl leash1,
SurfaceControl leash2, Consumer<Rect> finishCallback) {
final boolean isLandscape = isLandscape();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl
index eb08d0e..5533ad5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl
@@ -86,8 +86,8 @@
/**
* Starts a pair of intent and task in one transition.
*/
- oneway void startIntentAndTask(in PendingIntent pendingIntent, in Intent fillInIntent,
- in Bundle options1, int taskId, in Bundle options2, int sidePosition, float splitRatio,
+ oneway void startIntentAndTask(in PendingIntent pendingIntent, in Bundle options1, int taskId,
+ in Bundle options2, int sidePosition, float splitRatio,
in RemoteTransition remoteTransition, in InstanceId instanceId) = 16;
/**
@@ -108,9 +108,8 @@
* Starts a pair of intent and task using legacy transition system.
*/
oneway void startIntentAndTaskWithLegacyTransition(in PendingIntent pendingIntent,
- in Intent fillInIntent, in Bundle options1, int taskId, in Bundle options2,
- int splitPosition, float splitRatio, in RemoteAnimationAdapter adapter,
- in InstanceId instanceId) = 12;
+ in Bundle options1, int taskId, in Bundle options2, int splitPosition, float splitRatio,
+ in RemoteAnimationAdapter adapter, in InstanceId instanceId) = 12;
/**
* Starts a pair of shortcut and task using legacy transition system.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index c6a2b83..cdc8cdd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -18,6 +18,7 @@
import static android.app.ActivityManager.START_SUCCESS;
import static android.app.ActivityManager.START_TASK_TO_FRONT;
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION;
import static android.view.Display.DEFAULT_DISPLAY;
@@ -32,6 +33,8 @@
import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_SPLIT_SCREEN;
import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.app.ActivityTaskManager;
@@ -60,13 +63,12 @@
import androidx.annotation.BinderThread;
import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.InstanceId;
import com.android.internal.protolog.common.ProtoLog;
import com.android.launcher3.icons.IconProvider;
+import com.android.wm.shell.R;
import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.DisplayController;
@@ -166,8 +168,11 @@
private final IconProvider mIconProvider;
private final Optional<RecentTasksController> mRecentTasksOptional;
private final SplitScreenShellCommandHandler mSplitScreenShellCommandHandler;
+ private final String[] mMultiInstancesComponents;
- private StageCoordinator mStageCoordinator;
+ @VisibleForTesting
+ StageCoordinator mStageCoordinator;
+
// Only used for the legacy recents animation from splitscreen to allow the tasks to be animated
// outside the bounds of the roots by being reparented into a higher level fullscreen container
private SurfaceControl mGoingToRecentsTasksLayer;
@@ -210,6 +215,51 @@
if (ActivityTaskManager.supportsSplitScreenMultiWindow(context)) {
shellInit.addInitCallback(this::onInit, this);
}
+
+ // TODO(255224696): Remove the config once having a way for client apps to opt-in
+ // multi-instances split.
+ mMultiInstancesComponents = mContext.getResources()
+ .getStringArray(R.array.config_componentsSupportMultiInstancesSplit);
+ }
+
+ @VisibleForTesting
+ SplitScreenController(Context context,
+ ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler,
+ ShellController shellController,
+ ShellTaskOrganizer shellTaskOrganizer,
+ SyncTransactionQueue syncQueue,
+ RootTaskDisplayAreaOrganizer rootTDAOrganizer,
+ DisplayController displayController,
+ DisplayImeController displayImeController,
+ DisplayInsetsController displayInsetsController,
+ DragAndDropController dragAndDropController,
+ Transitions transitions,
+ TransactionPool transactionPool,
+ IconProvider iconProvider,
+ RecentTasksController recentTasks,
+ ShellExecutor mainExecutor,
+ StageCoordinator stageCoordinator) {
+ mShellCommandHandler = shellCommandHandler;
+ mShellController = shellController;
+ mTaskOrganizer = shellTaskOrganizer;
+ mSyncQueue = syncQueue;
+ mContext = context;
+ mRootTDAOrganizer = rootTDAOrganizer;
+ mMainExecutor = mainExecutor;
+ mDisplayController = displayController;
+ mDisplayImeController = displayImeController;
+ mDisplayInsetsController = displayInsetsController;
+ mDragAndDropController = dragAndDropController;
+ mTransitions = transitions;
+ mTransactionPool = transactionPool;
+ mIconProvider = iconProvider;
+ mRecentTasksOptional = Optional.of(recentTasks);
+ mStageCoordinator = stageCoordinator;
+ mSplitScreenShellCommandHandler = new SplitScreenShellCommandHandler(this);
+ shellInit.addInitCallback(this::onInit, this);
+ mMultiInstancesComponents = mContext.getResources()
+ .getStringArray(R.array.config_componentsSupportMultiInstancesSplit);
}
public SplitScreen asSplitScreen() {
@@ -471,72 +521,116 @@
startIntent(intent, fillInIntent, position, options);
}
+ private void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent,
+ @Nullable Bundle options1, int taskId, @Nullable Bundle options2,
+ @SplitPosition int splitPosition, float splitRatio, RemoteAnimationAdapter adapter,
+ InstanceId instanceId) {
+ Intent fillInIntent = null;
+ if (launchSameComponentAdjacently(pendingIntent, splitPosition, taskId)
+ && supportMultiInstancesSplit(pendingIntent.getIntent().getComponent())) {
+ fillInIntent = new Intent();
+ fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+ }
+ mStageCoordinator.startIntentAndTaskWithLegacyTransition(pendingIntent, fillInIntent,
+ options1, taskId, options2, splitPosition, splitRatio, adapter, instanceId);
+ }
+
+ private void startIntentAndTask(PendingIntent pendingIntent, @Nullable Bundle options1,
+ int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition,
+ float splitRatio, @Nullable RemoteTransition remoteTransition, InstanceId instanceId) {
+ Intent fillInIntent = null;
+ if (launchSameComponentAdjacently(pendingIntent, splitPosition, taskId)
+ && supportMultiInstancesSplit(pendingIntent.getIntent().getComponent())) {
+ fillInIntent = new Intent();
+ fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+ }
+ mStageCoordinator.startIntentAndTask(pendingIntent, fillInIntent, options1, taskId,
+ options2, splitPosition, splitRatio, remoteTransition, instanceId);
+ }
+
@Override
public void startIntent(PendingIntent intent, @Nullable Intent fillInIntent,
@SplitPosition int position, @Nullable Bundle options) {
- if (fillInIntent == null) {
- fillInIntent = new Intent();
- }
- // Flag this as a no-user-action launch to prevent sending user leaving event to the
- // current top activity since it's going to be put into another side of the split. This
- // prevents the current top activity from going into pip mode due to user leaving event.
+ // Flag this as a no-user-action launch to prevent sending user leaving event to the current
+ // top activity since it's going to be put into another side of the split. This prevents the
+ // current top activity from going into pip mode due to user leaving event.
+ if (fillInIntent == null) fillInIntent = new Intent();
fillInIntent.addFlags(FLAG_ACTIVITY_NO_USER_ACTION);
- // Flag with MULTIPLE_TASK if this is launching the same activity into both sides of the
- // split and there is no reusable background task.
- if (shouldAddMultipleTaskFlag(intent.getIntent(), position)) {
- final ActivityManager.RecentTaskInfo taskInfo = mRecentTasksOptional.isPresent()
- ? mRecentTasksOptional.get().findTaskInBackground(
- intent.getIntent().getComponent())
- : null;
- if (taskInfo != null) {
- startTask(taskInfo.taskId, position, options);
+ if (launchSameComponentAdjacently(intent, position, INVALID_TASK_ID)) {
+ final ComponentName launching = intent.getIntent().getComponent();
+ if (supportMultiInstancesSplit(launching)) {
+ // To prevent accumulating large number of instances in the background, reuse task
+ // in the background with priority.
+ final ActivityManager.RecentTaskInfo taskInfo = mRecentTasksOptional
+ .map(recentTasks -> recentTasks.findTaskInBackground(launching))
+ .orElse(null);
+ if (taskInfo != null) {
+ startTask(taskInfo.taskId, position, options);
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
+ "Start task in background");
+ return;
+ }
+
+ // Flag with MULTIPLE_TASK if this is launching the same activity into both sides of
+ // the split and there is no reusable background task.
+ fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
+ } else if (isSplitScreenVisible()) {
+ mStageCoordinator.switchSplitPosition("startIntent");
return;
}
- fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
}
- if (!ENABLE_SHELL_TRANSITIONS) {
- mStageCoordinator.startIntentLegacy(intent, fillInIntent, position, options);
- return;
- }
mStageCoordinator.startIntent(intent, fillInIntent, position, options);
}
/** Returns {@code true} if it's launching the same component on both sides of the split. */
- @VisibleForTesting
- boolean shouldAddMultipleTaskFlag(@Nullable Intent startIntent, @SplitPosition int position) {
- if (startIntent == null) {
- return false;
- }
+ private boolean launchSameComponentAdjacently(@Nullable PendingIntent pendingIntent,
+ @SplitPosition int position, int taskId) {
+ if (pendingIntent == null || pendingIntent.getIntent() == null) return false;
- final ComponentName launchingActivity = startIntent.getComponent();
- if (launchingActivity == null) {
- return false;
- }
+ final ComponentName launchingActivity = pendingIntent.getIntent().getComponent();
+ if (launchingActivity == null) return false;
- if (isSplitScreenVisible()) {
- // To prevent users from constantly dropping the same app to the same side resulting in
- // a large number of instances in the background.
- final ActivityManager.RunningTaskInfo targetTaskInfo = getTaskInfo(position);
- final ComponentName targetActivity = targetTaskInfo != null
- ? targetTaskInfo.baseIntent.getComponent() : null;
- if (Objects.equals(launchingActivity, targetActivity)) {
- return false;
+ if (taskId != INVALID_TASK_ID) {
+ final ActivityManager.RunningTaskInfo taskInfo =
+ mTaskOrganizer.getRunningTaskInfo(taskId);
+ if (taskInfo != null) {
+ return Objects.equals(taskInfo.baseIntent.getComponent(), launchingActivity);
}
-
- // Allow users to start a new instance the same to adjacent side.
- final ActivityManager.RunningTaskInfo pairedTaskInfo =
- getTaskInfo(SplitLayout.reversePosition(position));
- final ComponentName pairedActivity = pairedTaskInfo != null
- ? pairedTaskInfo.baseIntent.getComponent() : null;
- return Objects.equals(launchingActivity, pairedActivity);
+ return false;
}
- final ActivityManager.RunningTaskInfo taskInfo = getFocusingTaskInfo();
- if (taskInfo != null && isValidToEnterSplitScreen(taskInfo)) {
- return Objects.equals(taskInfo.baseIntent.getComponent(), launchingActivity);
+ if (!isSplitScreenVisible()) {
+ // Split screen is not yet activated, check if the current top running task is valid to
+ // split together.
+ final ActivityManager.RunningTaskInfo taskInfo = getFocusingTaskInfo();
+ if (taskInfo != null && isValidToEnterSplitScreen(taskInfo)) {
+ return Objects.equals(taskInfo.baseIntent.getComponent(), launchingActivity);
+ }
+ return false;
+ }
+
+ // Compare to the adjacent side of the split to determine if this is launching the same
+ // component adjacently.
+ final ActivityManager.RunningTaskInfo pairedTaskInfo =
+ getTaskInfo(SplitLayout.reversePosition(position));
+ final ComponentName pairedActivity = pairedTaskInfo != null
+ ? pairedTaskInfo.baseIntent.getComponent() : null;
+ return Objects.equals(launchingActivity, pairedActivity);
+ }
+
+ @VisibleForTesting
+ /** Returns {@code true} if the component supports multi-instances split. */
+ boolean supportMultiInstancesSplit(@Nullable ComponentName launching) {
+ if (launching == null) return false;
+
+ final String componentName = launching.flattenToString();
+ for (int i = 0; i < mMultiInstancesComponents.length; i++) {
+ if (mMultiInstancesComponents[i].equals(componentName)) {
+ return true;
+ }
}
return false;
@@ -839,14 +933,13 @@
@Override
public void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent,
- Intent fillInIntent, Bundle options1, int taskId, Bundle options2,
- int splitPosition, float splitRatio, RemoteAnimationAdapter adapter,
- InstanceId instanceId) {
+ Bundle options1, int taskId, Bundle options2, int splitPosition, float splitRatio,
+ RemoteAnimationAdapter adapter, InstanceId instanceId) {
executeRemoteCallWithTaskPermission(mController,
"startIntentAndTaskWithLegacyTransition", (controller) ->
- controller.mStageCoordinator.startIntentAndTaskWithLegacyTransition(
- pendingIntent, fillInIntent, options1, taskId, options2,
- splitPosition, splitRatio, adapter, instanceId));
+ controller.startIntentAndTaskWithLegacyTransition(pendingIntent,
+ options1, taskId, options2, splitPosition, splitRatio, adapter,
+ instanceId));
}
@Override
@@ -872,14 +965,13 @@
}
@Override
- public void startIntentAndTask(PendingIntent pendingIntent, Intent fillInIntent,
- @Nullable Bundle options1, int taskId, @Nullable Bundle options2,
- @SplitPosition int splitPosition, float splitRatio,
- @Nullable RemoteTransition remoteTransition, InstanceId instanceId) {
+ public void startIntentAndTask(PendingIntent pendingIntent, @Nullable Bundle options1,
+ int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition,
+ float splitRatio, @Nullable RemoteTransition remoteTransition,
+ InstanceId instanceId) {
executeRemoteCallWithTaskPermission(mController, "startIntentAndTask",
- (controller) -> controller.mStageCoordinator.startIntentAndTask(pendingIntent,
- fillInIntent, options1, taskId, options2, splitPosition, splitRatio,
- remoteTransition, instanceId));
+ (controller) -> controller.startIntentAndTask(pendingIntent, options1, taskId,
+ options2, splitPosition, splitRatio, remoteTransition, instanceId));
}
@Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index e888c6f..c2ab7ef 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -24,7 +24,6 @@
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
-import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
import static android.content.res.Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.RemoteAnimationTarget.MODE_OPENING;
@@ -428,6 +427,11 @@
/** Launches an activity into split. */
void startIntent(PendingIntent intent, Intent fillInIntent, @SplitPosition int position,
@Nullable Bundle options) {
+ if (!ENABLE_SHELL_TRANSITIONS) {
+ startIntentLegacy(intent, fillInIntent, position, options);
+ return;
+ }
+
final WindowContainerTransaction wct = new WindowContainerTransaction();
final WindowContainerTransaction evictWct = new WindowContainerTransaction();
prepareEvictChildTasks(position, evictWct);
@@ -441,13 +445,7 @@
prepareEnterSplitScreen(wct, null /* taskInfo */, position);
mSplitTransitions.startEnterTransition(transitType, wct, null, this,
- aborted -> {
- // Switch the split position if launching as MULTIPLE_TASK failed.
- if (aborted && (fillInIntent.getFlags() & FLAG_ACTIVITY_MULTIPLE_TASK) != 0) {
- setSideStagePositionAnimated(
- SplitLayout.reversePosition(mSideStagePosition));
- }
- } /* consumedCallback */,
+ null /* consumedCallback */,
(finishWct, finishT) -> {
if (!evictWct.isEmpty()) {
finishWct.merge(evictWct, true);
@@ -457,7 +455,7 @@
/** Launches an activity into split by legacy transition. */
void startIntentLegacy(PendingIntent intent, Intent fillInIntent,
- @SplitPosition int position, @androidx.annotation.Nullable Bundle options) {
+ @SplitPosition int position, @Nullable Bundle options) {
final WindowContainerTransaction evictWct = new WindowContainerTransaction();
prepareEvictChildTasks(position, evictWct);
@@ -473,12 +471,6 @@
exitSplitScreen(mMainStage.getChildCount() == 0
? mSideStage : mMainStage, EXIT_REASON_UNKNOWN));
mSplitUnsupportedToast.show();
- } else {
- // Switch the split position if launching as MULTIPLE_TASK failed.
- if ((fillInIntent.getFlags() & FLAG_ACTIVITY_MULTIPLE_TASK) != 0) {
- setSideStagePosition(SplitLayout.reversePosition(
- getSideStagePosition()), null);
- }
}
// Do nothing when the animation was cancelled.
@@ -771,9 +763,8 @@
mSideStage.evictInvisibleChildren(wct);
}
- Bundle resolveStartStage(@StageType int stage,
- @SplitPosition int position, @androidx.annotation.Nullable Bundle options,
- @androidx.annotation.Nullable WindowContainerTransaction wct) {
+ Bundle resolveStartStage(@StageType int stage, @SplitPosition int position,
+ @Nullable Bundle options, @Nullable WindowContainerTransaction wct) {
switch (stage) {
case STAGE_TYPE_UNDEFINED: {
if (position != SPLIT_POSITION_UNDEFINED) {
@@ -844,9 +835,8 @@
: mMainStage.getTopVisibleChildTaskId();
}
- void setSideStagePositionAnimated(@SplitPosition int sideStagePosition) {
- if (mSideStagePosition == sideStagePosition) return;
- SurfaceControl.Transaction t = mTransactionPool.acquire();
+ void switchSplitPosition(String reason) {
+ final SurfaceControl.Transaction t = mTransactionPool.acquire();
mTempRect1.setEmpty();
final StageTaskListener topLeftStage =
mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage;
@@ -886,6 +876,11 @@
va.start();
});
});
+
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Switch split position: %s", reason);
+ mLogger.logSwap(getMainStagePosition(), mMainStage.getTopChildTaskUid(),
+ getSideStagePosition(), mSideStage.getTopChildTaskUid(),
+ mSplitLayout.isLandscape());
}
void setSideStagePosition(@SplitPosition int sideStagePosition,
@@ -1617,10 +1612,7 @@
@Override
public void onDoubleTappedDivider() {
- setSideStagePositionAnimated(SplitLayout.reversePosition(mSideStagePosition));
- mLogger.logSwap(getMainStagePosition(), mMainStage.getTopChildTaskUid(),
- getSideStagePosition(), mSideStage.getTopChildTaskUid(),
- mSplitLayout.isLandscape());
+ switchSplitPosition("double tap");
}
@Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index 56d51bd..c6f31c2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -80,7 +80,7 @@
/** Set to {@code true} to enable shell transitions. */
public static final boolean ENABLE_SHELL_TRANSITIONS =
- SystemProperties.getBoolean("persist.wm.debug.shell_transit", false);
+ SystemProperties.getBoolean("persist.wm.debug.shell_transit", true);
public static final boolean SHELL_TRANSITIONS_ROTATION = ENABLE_SHELL_TRANSITIONS
&& SystemProperties.getBoolean("persist.wm.debug.shell_transit_rotate", false);
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenUtils.kt
index 4a3284e..ead451f 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenUtils.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenUtils.kt
@@ -41,7 +41,6 @@
import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
import com.android.wm.shell.flicker.LAUNCHER_UI_PACKAGE_NAME
import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME
-import org.junit.Assert.assertNotNull
import java.util.Collections
internal object SplitScreenUtils {
@@ -51,7 +50,7 @@
private const val DIVIDER_BAR = "docked_divider_handle"
private const val OVERVIEW_SNAPSHOT = "snapshot"
private const val GESTURE_STEP_MS = 16L
- private val LONG_PRESS_TIME_MS = ViewConfiguration.getLongPressTimeout() * 2L
+ private const val LONG_PRESS_TIME_MS = 100L
private val SPLIT_DECOR_MANAGER = ComponentNameMatcher("", "SplitDecorManager#")
private val notificationScrollerSelector: BySelector
@@ -276,6 +275,13 @@
}
}
+ fun longPress(instrumentation: Instrumentation, point: Point) {
+ val downTime = SystemClock.uptimeMillis()
+ touch(instrumentation, MotionEvent.ACTION_DOWN, downTime, downTime, TIMEOUT_MS, point)
+ SystemClock.sleep(LONG_PRESS_TIME_MS)
+ touch(instrumentation, MotionEvent.ACTION_UP, downTime, downTime, TIMEOUT_MS, point)
+ }
+
fun createShortcutOnHotseatIfNotExist(tapl: LauncherInstrumentation, appName: String) {
tapl.workspace.deleteAppIcon(tapl.workspace.getHotseatAppIcon(0))
val allApps = tapl.workspace.switchToAllApps()
@@ -347,11 +353,9 @@
Until.findObject(By.res(sourceApp.packageName, "SplitScreenTest")),
TIMEOUT_MS
)
- assertNotNull("Unable to find the TextView", textView)
- textView.click(LONG_PRESS_TIME_MS)
+ longPress(instrumentation, textView.visibleCenter)
val copyBtn = device.wait(Until.findObject(By.text("Copy")), TIMEOUT_MS)
- assertNotNull("Unable to find the copy button", copyBtn)
copyBtn.click()
// Paste text to destinationApp
@@ -360,11 +364,9 @@
Until.findObject(By.res(destinationApp.packageName, "plain_text_input")),
TIMEOUT_MS
)
- assertNotNull("Unable to find the EditText", editText)
- editText.click(LONG_PRESS_TIME_MS)
+ longPress(instrumentation, editText.visibleCenter)
val pasteBtn = device.wait(Until.findObject(By.text("Paste")), TIMEOUT_MS)
- assertNotNull("Unable to find the paste button", pasteBtn)
pasteBtn.click()
// Verify text
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
index 7eccbf4..b603e03 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
@@ -23,6 +23,7 @@
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -93,7 +94,10 @@
private IActivityTaskManager mActivityTaskManager;
@Mock
- private IOnBackInvokedCallback mIOnBackInvokedCallback;
+ private IOnBackInvokedCallback mAppCallback;
+
+ @Mock
+ private IOnBackInvokedCallback mAnimatorCallback;
@Mock
private IBackAnimationFinishedCallback mBackAnimationFinishedCallback;
@@ -105,8 +109,6 @@
private ShellController mShellController;
private BackAnimationController mController;
-
- private int mEventTime = 0;
private TestableContentResolver mContentResolver;
private TestableLooper mTestableLooper;
@@ -126,16 +128,15 @@
mContentResolver);
mController.setEnableUAnimation(true);
mShellInit.init();
- mEventTime = 0;
mShellExecutor.flushAll();
}
- private void createNavigationInfo(int backType, IOnBackInvokedCallback onBackInvokedCallback) {
+ private void createNavigationInfo(int backType, boolean enableAnimation) {
BackNavigationInfo.Builder builder = new BackNavigationInfo.Builder()
.setType(backType)
.setOnBackNavigationDone(new RemoteCallback((bundle) -> {}))
- .setOnBackInvokedCallback(onBackInvokedCallback)
- .setPrepareRemoteAnimation(true);
+ .setOnBackInvokedCallback(mAppCallback)
+ .setPrepareRemoteAnimation(enableAnimation);
createNavigationInfo(builder);
}
@@ -176,26 +177,47 @@
}
@Test
- public void verifyAnimationFinishes() {
- RemoteAnimationTarget animationTarget = createAnimationTarget();
- boolean[] backNavigationDone = new boolean[]{false};
- boolean[] triggerBack = new boolean[]{false};
- createNavigationInfo(new BackNavigationInfo.Builder()
- .setType(BackNavigationInfo.TYPE_CROSS_ACTIVITY)
- .setOnBackNavigationDone(
- new RemoteCallback(result -> {
- backNavigationDone[0] = true;
- triggerBack[0] = result.getBoolean(KEY_TRIGGER_BACK);
- })));
- triggerBackGesture();
- assertTrue("Navigation Done callback not called", backNavigationDone[0]);
- assertTrue("TriggerBack should have been true", triggerBack[0]);
+ public void verifyNavigationFinishes() throws RemoteException {
+ final int[] testTypes = new int[] {BackNavigationInfo.TYPE_RETURN_TO_HOME,
+ BackNavigationInfo.TYPE_CROSS_TASK,
+ BackNavigationInfo.TYPE_CROSS_ACTIVITY,
+ BackNavigationInfo.TYPE_DIALOG_CLOSE,
+ BackNavigationInfo.TYPE_CALLBACK };
+
+ for (int type: testTypes) {
+ registerAnimation(type);
+ }
+
+ for (int type: testTypes) {
+ boolean[] backNavigationDone = new boolean[]{false};
+ boolean[] triggerBack = new boolean[]{false};
+
+ createNavigationInfo(new BackNavigationInfo.Builder()
+ .setType(type)
+ .setOnBackInvokedCallback(mAppCallback)
+ .setPrepareRemoteAnimation(true)
+ .setOnBackNavigationDone(
+ new RemoteCallback(result -> {
+ backNavigationDone[0] = true;
+ triggerBack[0] = result.getBoolean(KEY_TRIGGER_BACK);
+ })));
+ triggerBackGesture();
+ simulateRemoteAnimationStart(type);
+ simulateRemoteAnimationFinished();
+ mShellExecutor.flushAll();
+
+ assertTrue("Navigation Done callback not called for "
+ + BackNavigationInfo.typeToString(type), backNavigationDone[0]);
+ assertTrue("TriggerBack should have been true", triggerBack[0]);
+ }
}
+
+
@Test
public void backToHome_dispatchesEvents() throws RemoteException {
- mController.setBackToLauncherCallback(mIOnBackInvokedCallback, mBackAnimationRunner);
- createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, mIOnBackInvokedCallback);
+ registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME);
+ createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, true);
doMotionEvent(MotionEvent.ACTION_DOWN, 0);
@@ -203,14 +225,16 @@
doMotionEvent(MotionEvent.ACTION_MOVE, 100);
simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME);
- verify(mIOnBackInvokedCallback).onBackStarted(any(BackEvent.class));
+
+ verify(mAnimatorCallback).onBackStarted(any(BackEvent.class));
verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any());
- verify(mIOnBackInvokedCallback, atLeastOnce()).onBackProgressed(any(BackEvent.class));
+ ArgumentCaptor<BackEvent> backEventCaptor = ArgumentCaptor.forClass(BackEvent.class);
+ verify(mAnimatorCallback, atLeastOnce()).onBackProgressed(backEventCaptor.capture());
// Check that back invocation is dispatched.
mController.setTriggerBack(true); // Fake trigger back
doMotionEvent(MotionEvent.ACTION_UP, 0);
- verify(mIOnBackInvokedCallback).onBackInvoked();
+ verify(mAnimatorCallback).onBackInvoked();
}
@Test
@@ -223,97 +247,94 @@
mActivityTaskManager, mContext,
mContentResolver);
shellInit.init();
- mController.setBackToLauncherCallback(mIOnBackInvokedCallback, mBackAnimationRunner);
+ registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME);
- IOnBackInvokedCallback appCallback = mock(IOnBackInvokedCallback.class);
ArgumentCaptor<BackEvent> backEventCaptor = ArgumentCaptor.forClass(BackEvent.class);
- createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, appCallback);
+ createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, false);
triggerBackGesture();
- verify(appCallback, never()).onBackStarted(any(BackEvent.class));
- verify(appCallback, never()).onBackProgressed(backEventCaptor.capture());
- verify(appCallback, times(1)).onBackInvoked();
+ verify(mAppCallback, never()).onBackStarted(any());
+ verify(mAppCallback, never()).onBackProgressed(backEventCaptor.capture());
+ verify(mAppCallback, times(1)).onBackInvoked();
- verify(mIOnBackInvokedCallback, never()).onBackStarted(any(BackEvent.class));
- verify(mIOnBackInvokedCallback, never()).onBackProgressed(backEventCaptor.capture());
- verify(mIOnBackInvokedCallback, never()).onBackInvoked();
+ verify(mAnimatorCallback, never()).onBackStarted(any());
+ verify(mAnimatorCallback, never()).onBackProgressed(backEventCaptor.capture());
+ verify(mAnimatorCallback, never()).onBackInvoked();
verify(mBackAnimationRunner, never()).onAnimationStart(
anyInt(), any(), any(), any(), any());
}
@Test
public void ignoresGesture_transitionInProgress() throws RemoteException {
- mController.setBackToLauncherCallback(mIOnBackInvokedCallback, mBackAnimationRunner);
- createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, null);
+ registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME);
+ createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, true);
triggerBackGesture();
simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME);
// Check that back invocation is dispatched.
- verify(mIOnBackInvokedCallback).onBackInvoked();
+ verify(mAnimatorCallback).onBackInvoked();
verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any());
- reset(mIOnBackInvokedCallback);
+ reset(mAnimatorCallback);
reset(mBackAnimationRunner);
// Verify that we prevent animation from restarting if another gestures happens before
// the previous transition is finished.
doMotionEvent(MotionEvent.ACTION_DOWN, 0);
- verifyNoMoreInteractions(mIOnBackInvokedCallback);
- mController.onBackAnimationFinished();
- // Pretend the transition handler called finishAnimation.
- mController.finishBackNavigation();
+ verifyNoMoreInteractions(mAnimatorCallback);
+
+ // Finish back navigation.
+ simulateRemoteAnimationFinished();
// Verify that more events from a rejected swipe cannot start animation.
doMotionEvent(MotionEvent.ACTION_MOVE, 100);
doMotionEvent(MotionEvent.ACTION_UP, 0);
- verifyNoMoreInteractions(mIOnBackInvokedCallback);
+ verifyNoMoreInteractions(mAnimatorCallback);
// Verify that we start accepting gestures again once transition finishes.
doMotionEvent(MotionEvent.ACTION_DOWN, 0);
doMotionEvent(MotionEvent.ACTION_MOVE, 100);
simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME);
- verify(mIOnBackInvokedCallback).onBackStarted(any(BackEvent.class));
+ verify(mAnimatorCallback).onBackStarted(any());
verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any());
}
@Test
public void acceptsGesture_transitionTimeout() throws RemoteException {
- mController.setBackToLauncherCallback(mIOnBackInvokedCallback, mBackAnimationRunner);
- createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, null);
+ registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME);
+ createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, true);
+
+ // In case it is still running in animation.
+ doNothing().when(mAnimatorCallback).onBackInvoked();
triggerBackGesture();
simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME);
- reset(mIOnBackInvokedCallback);
-
// Simulate transition timeout.
mShellExecutor.flushAll();
- mController.onBackAnimationFinished();
- // Pretend the transition handler called finishAnimation.
- mController.finishBackNavigation();
+ reset(mAnimatorCallback);
doMotionEvent(MotionEvent.ACTION_DOWN, 0);
doMotionEvent(MotionEvent.ACTION_MOVE, 100);
simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME);
- verify(mIOnBackInvokedCallback).onBackStarted(any(BackEvent.class));
+ verify(mAnimatorCallback).onBackStarted(any());
}
-
@Test
public void cancelBackInvokeWhenLostFocus() throws RemoteException {
- mController.setBackToLauncherCallback(mIOnBackInvokedCallback, mBackAnimationRunner);
+ registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME);
- createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, null);
+ createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, true);
doMotionEvent(MotionEvent.ACTION_DOWN, 0);
// Check that back start and progress is dispatched when first move.
doMotionEvent(MotionEvent.ACTION_MOVE, 100);
simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME);
- verify(mIOnBackInvokedCallback).onBackStarted(any(BackEvent.class));
+ verify(mAnimatorCallback).onBackStarted(any());
verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any());
// Check that back invocation is dispatched.
@@ -323,11 +344,11 @@
IBinder token = mock(IBinder.class);
mController.mFocusObserver.focusLost(token);
mShellExecutor.flushAll();
- verify(mIOnBackInvokedCallback).onBackCancelled();
+ verify(mAnimatorCallback).onBackCancelled();
// No more back invoke.
doMotionEvent(MotionEvent.ACTION_UP, 0);
- verify(mIOnBackInvokedCallback, never()).onBackInvoked();
+ verify(mAnimatorCallback, never()).onBackInvoked();
}
private void doMotionEvent(int actionDown, int coordinate) {
@@ -335,7 +356,6 @@
coordinate, coordinate,
actionDown,
BackEvent.EDGE_LEFT);
- mEventTime += 10;
}
private void simulateRemoteAnimationStart(int type) throws RemoteException {
@@ -347,4 +367,14 @@
mShellExecutor.flushAll();
}
}
+
+ private void simulateRemoteAnimationFinished() {
+ mController.onBackAnimationFinished();
+ mController.finishBackNavigation();
+ }
+
+ private void registerAnimation(int type) {
+ mController.registerAnimation(type,
+ new BackAnimationRunner(mAnimatorCallback, mBackAnimationRunner));
+ }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
index a6f19e7..40f2e88 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
@@ -97,13 +97,13 @@
@Test
public void showInsets_schedulesNoWorkOnExecutor() {
- mPerDisplay.showInsets(ime(), true);
+ mPerDisplay.showInsets(ime(), true /* fromIme */, null /* statsToken */);
verifyZeroInteractions(mExecutor);
}
@Test
public void hideInsets_schedulesNoWorkOnExecutor() {
- mPerDisplay.hideInsets(ime(), true);
+ mPerDisplay.hideInsets(ime(), true /* fromIme */, null /* statsToken */);
verifyZeroInteractions(mExecutor);
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java
index 39db328..956f1cd 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java
@@ -25,6 +25,7 @@
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import android.annotation.Nullable;
import android.content.ComponentName;
import android.os.RemoteException;
import android.util.SparseArray;
@@ -33,6 +34,7 @@
import android.view.InsetsSourceControl;
import android.view.InsetsState;
import android.view.WindowInsets;
+import android.view.inputmethod.ImeTracker;
import androidx.test.filters.SmallTest;
@@ -111,8 +113,10 @@
WindowInsets.Type.defaultVisible());
mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).insetsChanged(null);
mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).insetsControlChanged(null, null);
- mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).showInsets(0, false);
- mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).hideInsets(0, false);
+ mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).showInsets(0, false,
+ null /* statsToken */);
+ mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).hideInsets(0, false,
+ null /* statsToken */);
mExecutor.flushAll();
assertTrue(defaultListener.topFocusedWindowChangedCount == 1);
@@ -131,8 +135,10 @@
WindowInsets.Type.defaultVisible());
mInsetsControllersByDisplayId.get(SECOND_DISPLAY).insetsChanged(null);
mInsetsControllersByDisplayId.get(SECOND_DISPLAY).insetsControlChanged(null, null);
- mInsetsControllersByDisplayId.get(SECOND_DISPLAY).showInsets(0, false);
- mInsetsControllersByDisplayId.get(SECOND_DISPLAY).hideInsets(0, false);
+ mInsetsControllersByDisplayId.get(SECOND_DISPLAY).showInsets(0, false,
+ null /* statsToken */);
+ mInsetsControllersByDisplayId.get(SECOND_DISPLAY).hideInsets(0, false,
+ null /* statsToken */);
mExecutor.flushAll();
assertTrue(defaultListener.topFocusedWindowChangedCount == 1);
@@ -191,12 +197,12 @@
}
@Override
- public void showInsets(int types, boolean fromIme) {
+ public void showInsets(int types, boolean fromIme, @Nullable ImeTracker.Token statsToken) {
showInsetsCount++;
}
@Override
- public void hideInsets(int types, boolean fromIme) {
+ public void hideInsets(int types, boolean fromIme, @Nullable ImeTracker.Token statsToken) {
hideInsetsCount++;
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
index d01f3d3..38b75f8 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
@@ -16,18 +16,24 @@
package com.android.wm.shell.splitscreen;
+import static android.app.PendingIntent.FLAG_IMMUTABLE;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
+import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
@@ -35,6 +41,8 @@
import static org.mockito.Mockito.when;
import android.app.ActivityManager;
+import android.app.ActivityTaskManager;
+import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ActivityInfo;
@@ -65,11 +73,11 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-import java.util.Optional;
-
/**
* Tests for {@link SplitScreenController}
*/
@@ -91,18 +99,21 @@
@Mock Transitions mTransitions;
@Mock TransactionPool mTransactionPool;
@Mock IconProvider mIconProvider;
- @Mock Optional<RecentTasksController> mRecentTasks;
+ @Mock StageCoordinator mStageCoordinator;
+ @Mock RecentTasksController mRecentTasks;
+ @Captor ArgumentCaptor<Intent> mIntentCaptor;
private SplitScreenController mSplitScreenController;
@Before
public void setup() {
+ assumeTrue(ActivityTaskManager.supportsSplitScreenMultiWindow(mContext));
MockitoAnnotations.initMocks(this);
mSplitScreenController = spy(new SplitScreenController(mContext, mShellInit,
mShellCommandHandler, mShellController, mTaskOrganizer, mSyncQueue,
mRootTDAOrganizer, mDisplayController, mDisplayImeController,
mDisplayInsetsController, mDragAndDropController, mTransitions, mTransactionPool,
- mIconProvider, mRecentTasks, mMainExecutor));
+ mIconProvider, mRecentTasks, mMainExecutor, mStageCoordinator));
}
@Test
@@ -148,58 +159,100 @@
}
@Test
- public void testShouldAddMultipleTaskFlag_notInSplitScreen() {
- doReturn(false).when(mSplitScreenController).isSplitScreenVisible();
- doReturn(true).when(mSplitScreenController).isValidToEnterSplitScreen(any());
-
- // Verify launching the same activity returns true.
+ public void testStartIntent_appendsNoUserActionFlag() {
Intent startIntent = createStartIntent("startActivity");
- ActivityManager.RunningTaskInfo focusTaskInfo =
- createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, startIntent);
- doReturn(focusTaskInfo).when(mSplitScreenController).getFocusingTaskInfo();
- assertTrue(mSplitScreenController.shouldAddMultipleTaskFlag(
- startIntent, SPLIT_POSITION_TOP_OR_LEFT));
+ PendingIntent pendingIntent =
+ PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE);
- // Verify launching different activity returns false.
- Intent diffIntent = createStartIntent("diffActivity");
- focusTaskInfo =
- createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, diffIntent);
- doReturn(focusTaskInfo).when(mSplitScreenController).getFocusingTaskInfo();
- assertFalse(mSplitScreenController.shouldAddMultipleTaskFlag(
- startIntent, SPLIT_POSITION_TOP_OR_LEFT));
+ mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null);
+
+ verify(mStageCoordinator).startIntent(eq(pendingIntent), mIntentCaptor.capture(),
+ eq(SPLIT_POSITION_TOP_OR_LEFT), isNull());
+ assertEquals(FLAG_ACTIVITY_NO_USER_ACTION,
+ mIntentCaptor.getValue().getFlags() & FLAG_ACTIVITY_NO_USER_ACTION);
}
@Test
- public void testShouldAddMultipleTaskFlag_inSplitScreen() {
- doReturn(true).when(mSplitScreenController).isSplitScreenVisible();
+ public void startIntent_multiInstancesSupported_appendsMultipleTaskFag() {
+ doReturn(true).when(mSplitScreenController).supportMultiInstancesSplit(any());
Intent startIntent = createStartIntent("startActivity");
+ PendingIntent pendingIntent =
+ PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE);
+ // Put the same component into focus task
+ ActivityManager.RunningTaskInfo focusTaskInfo =
+ createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, startIntent);
+ doReturn(focusTaskInfo).when(mStageCoordinator).getFocusingTaskInfo();
+ doReturn(true).when(mStageCoordinator).isValidToEnterSplitScreen(any());
+
+ mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null);
+
+ verify(mStageCoordinator).startIntent(eq(pendingIntent), mIntentCaptor.capture(),
+ eq(SPLIT_POSITION_TOP_OR_LEFT), isNull());
+ assertEquals(FLAG_ACTIVITY_MULTIPLE_TASK,
+ mIntentCaptor.getValue().getFlags() & FLAG_ACTIVITY_MULTIPLE_TASK);
+ }
+
+ @Test
+ public void startIntent_multiInstancesSupported_startTaskInBackgroundBeforeSplitActivated() {
+ doReturn(true).when(mSplitScreenController).supportMultiInstancesSplit(any());
+ doNothing().when(mSplitScreenController).startTask(anyInt(), anyInt(), any());
+ Intent startIntent = createStartIntent("startActivity");
+ PendingIntent pendingIntent =
+ PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE);
+ // Put the same component into focus task
+ ActivityManager.RunningTaskInfo focusTaskInfo =
+ createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, startIntent);
+ doReturn(focusTaskInfo).when(mStageCoordinator).getFocusingTaskInfo();
+ doReturn(true).when(mStageCoordinator).isValidToEnterSplitScreen(any());
+ // Put the same component into a task in the background
+ ActivityManager.RecentTaskInfo sameTaskInfo = new ActivityManager.RecentTaskInfo();
+ doReturn(sameTaskInfo).when(mRecentTasks).findTaskInBackground(any());
+
+ mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null);
+
+ verify(mSplitScreenController).startTask(anyInt(), eq(SPLIT_POSITION_TOP_OR_LEFT),
+ isNull());
+ }
+
+ @Test
+ public void startIntent_multiInstancesSupported_startTaskInBackgroundAfterSplitActivated() {
+ doReturn(true).when(mSplitScreenController).supportMultiInstancesSplit(any());
+ doNothing().when(mSplitScreenController).startTask(anyInt(), anyInt(), any());
+ Intent startIntent = createStartIntent("startActivity");
+ PendingIntent pendingIntent =
+ PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE);
+ // Put the same component into another side of the split
+ doReturn(true).when(mSplitScreenController).isSplitScreenVisible();
ActivityManager.RunningTaskInfo sameTaskInfo =
createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, startIntent);
- Intent diffIntent = createStartIntent("diffActivity");
- ActivityManager.RunningTaskInfo differentTaskInfo =
- createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, diffIntent);
+ doReturn(sameTaskInfo).when(mSplitScreenController).getTaskInfo(
+ SPLIT_POSITION_BOTTOM_OR_RIGHT);
+ // Put the same component into a task in the background
+ doReturn(new ActivityManager.RecentTaskInfo()).when(mRecentTasks)
+ .findTaskInBackground(any());
- // Verify launching the same activity return false.
- doReturn(sameTaskInfo).when(mSplitScreenController)
- .getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT);
- assertFalse(mSplitScreenController.shouldAddMultipleTaskFlag(
- startIntent, SPLIT_POSITION_TOP_OR_LEFT));
+ mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null);
- // Verify launching the same activity as adjacent returns true.
- doReturn(differentTaskInfo).when(mSplitScreenController)
- .getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT);
- doReturn(sameTaskInfo).when(mSplitScreenController)
- .getTaskInfo(SPLIT_POSITION_BOTTOM_OR_RIGHT);
- assertTrue(mSplitScreenController.shouldAddMultipleTaskFlag(
- startIntent, SPLIT_POSITION_TOP_OR_LEFT));
+ verify(mSplitScreenController).startTask(anyInt(), eq(SPLIT_POSITION_TOP_OR_LEFT),
+ isNull());
+ }
- // Verify launching different activity from adjacent returns false.
- doReturn(differentTaskInfo).when(mSplitScreenController)
- .getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT);
- doReturn(differentTaskInfo).when(mSplitScreenController)
- .getTaskInfo(SPLIT_POSITION_BOTTOM_OR_RIGHT);
- assertFalse(mSplitScreenController.shouldAddMultipleTaskFlag(
- startIntent, SPLIT_POSITION_TOP_OR_LEFT));
+ @Test
+ public void startIntent_multiInstancesNotSupported_switchesPositionAfterSplitActivated() {
+ doReturn(false).when(mSplitScreenController).supportMultiInstancesSplit(any());
+ Intent startIntent = createStartIntent("startActivity");
+ PendingIntent pendingIntent =
+ PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE);
+ // Put the same component into another side of the split
+ doReturn(true).when(mSplitScreenController).isSplitScreenVisible();
+ ActivityManager.RunningTaskInfo sameTaskInfo =
+ createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, startIntent);
+ doReturn(sameTaskInfo).when(mSplitScreenController).getTaskInfo(
+ SPLIT_POSITION_BOTTOM_OR_RIGHT);
+
+ mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null);
+
+ verify(mStageCoordinator).switchSplitPosition(anyString());
}
private Intent createStartIntent(String activityName) {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java
index 5ee8bf3..1a1bebd 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java
@@ -61,7 +61,7 @@
@RunWith(AndroidJUnit4.class)
public final class StageTaskListenerTests extends ShellTestCase {
private static final boolean ENABLE_SHELL_TRANSITIONS =
- SystemProperties.getBoolean("persist.wm.debug.shell_transit", false);
+ SystemProperties.getBoolean("persist.wm.debug.shell_transit", true);
@Mock
private ShellTaskOrganizer mTaskOrganizer;
diff --git a/libs/input/PointerController.cpp b/libs/input/PointerController.cpp
index 54f893e..099efd3 100644
--- a/libs/input/PointerController.cpp
+++ b/libs/input/PointerController.cpp
@@ -22,10 +22,18 @@
#include <SkBlendMode.h>
#include <SkCanvas.h>
#include <SkColor.h>
+#include <android-base/stringprintf.h>
#include <android-base/thread_annotations.h>
+#include <ftl/enum.h>
+
+#include <mutex>
#include "PointerControllerContext.h"
+#define INDENT " "
+#define INDENT2 " "
+#define INDENT3 " "
+
namespace android {
namespace {
@@ -223,7 +231,7 @@
}
void PointerController::clearSpotsLocked() {
- for (auto& [displayID, spotController] : mLocked.spotControllers) {
+ for (auto& [displayId, spotController] : mLocked.spotControllers) {
spotController.clearSpots();
}
}
@@ -235,7 +243,7 @@
void PointerController::reloadPointerResources() {
std::scoped_lock lock(getLock());
- for (auto& [displayID, spotController] : mLocked.spotControllers) {
+ for (auto& [displayId, spotController] : mLocked.spotControllers) {
spotController.reloadSpotResources();
}
@@ -286,13 +294,13 @@
std::scoped_lock lock(getLock());
for (auto it = mLocked.spotControllers.begin(); it != mLocked.spotControllers.end();) {
- int32_t displayID = it->first;
- if (!displayIdSet.count(displayID)) {
+ int32_t displayId = it->first;
+ if (!displayIdSet.count(displayId)) {
/*
* Ensures that an in-progress animation won't dereference
* a null pointer to TouchSpotController.
*/
- mContext.removeAnimationCallback(displayID);
+ mContext.removeAnimationCallback(displayId);
it = mLocked.spotControllers.erase(it);
} else {
++it;
@@ -313,4 +321,20 @@
return it != di.end() ? it->transform : kIdentityTransform;
}
+void PointerController::dump(std::string& dump) {
+ dump += INDENT "PointerController:\n";
+ std::scoped_lock lock(getLock());
+ dump += StringPrintf(INDENT2 "Presentation: %s\n",
+ ftl::enum_string(mLocked.presentation).c_str());
+ dump += StringPrintf(INDENT2 "Pointer Display ID: %" PRIu32 "\n", mLocked.pointerDisplayId);
+ dump += StringPrintf(INDENT2 "Viewports:\n");
+ for (const auto& info : mLocked.mDisplayInfos) {
+ info.dump(dump, INDENT3);
+ }
+ dump += INDENT2 "Spot Controllers:\n";
+ for (const auto& [_, spotController] : mLocked.spotControllers) {
+ spotController.dump(dump, INDENT3);
+ }
+}
+
} // namespace android
diff --git a/libs/input/PointerController.h b/libs/input/PointerController.h
index 33480e8..48d5a57 100644
--- a/libs/input/PointerController.h
+++ b/libs/input/PointerController.h
@@ -27,6 +27,7 @@
#include <map>
#include <memory>
+#include <string>
#include <vector>
#include "MouseCursorController.h"
@@ -75,6 +76,8 @@
void onDisplayInfosChangedLocked(const std::vector<gui::DisplayInfo>& displayInfos)
REQUIRES(getLock());
+ void dump(std::string& dump);
+
protected:
using WindowListenerConsumer =
std::function<void(const sp<android::gui::WindowInfosListener>&)>;
diff --git a/libs/input/TouchSpotController.cpp b/libs/input/TouchSpotController.cpp
index 4ac66c4..d9fe599 100644
--- a/libs/input/TouchSpotController.cpp
+++ b/libs/input/TouchSpotController.cpp
@@ -21,8 +21,15 @@
#include "TouchSpotController.h"
+#include <android-base/stringprintf.h>
+#include <input/PrintTools.h>
#include <log/log.h>
+#include <mutex>
+
+#define INDENT " "
+#define INDENT2 " "
+
namespace {
// Time to spend fading out the spot completely.
const nsecs_t SPOT_FADE_DURATION = 200 * 1000000LL; // 200 ms
@@ -53,6 +60,12 @@
}
}
+void TouchSpotController::Spot::dump(std::string& out, const char* prefix) const {
+ out += prefix;
+ base::StringAppendF(&out, "Spot{id=%" PRIx32 ", alpha=%f, scale=%f, pos=[%f, %f]}\n", id, alpha,
+ scale, x, y);
+}
+
// --- TouchSpotController ---
TouchSpotController::TouchSpotController(int32_t displayId, PointerControllerContext& context)
@@ -255,4 +268,22 @@
mContext.addAnimationCallback(mDisplayId, func);
}
+void TouchSpotController::dump(std::string& out, const char* prefix) const {
+ using base::StringAppendF;
+ out += prefix;
+ out += "SpotController:\n";
+ out += prefix;
+ StringAppendF(&out, INDENT "DisplayId: %" PRId32 "\n", mDisplayId);
+ std::scoped_lock lock(mLock);
+ out += prefix;
+ StringAppendF(&out, INDENT "Animating: %s\n", toString(mLocked.animating));
+ out += prefix;
+ out += INDENT "Spots:\n";
+ std::string spotPrefix = prefix;
+ spotPrefix += INDENT2;
+ for (const auto& spot : mLocked.displaySpots) {
+ spot->dump(out, spotPrefix.c_str());
+ }
+}
+
} // namespace android
diff --git a/libs/input/TouchSpotController.h b/libs/input/TouchSpotController.h
index 703de36..5bbc75d 100644
--- a/libs/input/TouchSpotController.h
+++ b/libs/input/TouchSpotController.h
@@ -38,6 +38,8 @@
void reloadSpotResources();
bool doAnimations(nsecs_t timestamp);
+ void dump(std::string& out, const char* prefix = "") const;
+
private:
struct Spot {
static const uint32_t INVALID_ID = 0xffffffff;
@@ -58,6 +60,7 @@
mLastIcon(nullptr) {}
void updateSprite(const SpriteIcon* icon, float x, float y, int32_t displayId);
+ void dump(std::string& out, const char* prefix = "") const;
private:
const SpriteIcon* mLastIcon;
diff --git a/media/java/android/media/session/MediaSession.java b/media/java/android/media/session/MediaSession.java
index 1bd12af..7e1bbe3 100644
--- a/media/java/android/media/session/MediaSession.java
+++ b/media/java/android/media/session/MediaSession.java
@@ -244,12 +244,9 @@
mCallback = null;
return;
}
- if (handler == null) {
- handler = new Handler();
- }
+ Looper looper = handler != null ? handler.getLooper() : Looper.myLooper();
callback.mSession = this;
- CallbackMessageHandler msgHandler = new CallbackMessageHandler(handler.getLooper(),
- callback);
+ CallbackMessageHandler msgHandler = new CallbackMessageHandler(looper, callback);
mCallback = msgHandler;
}
}
diff --git a/native/android/libandroid.map.txt b/native/android/libandroid.map.txt
index cb0f22f..584d0ba2 100644
--- a/native/android/libandroid.map.txt
+++ b/native/android/libandroid.map.txt
@@ -330,6 +330,7 @@
APerformanceHint_updateTargetWorkDuration; # introduced=Tiramisu
APerformanceHint_reportActualWorkDuration; # introduced=Tiramisu
APerformanceHint_closeSession; # introduced=Tiramisu
+ APerformanceHint_sendHint; # introduced=UpsideDownCake
local:
*;
};
diff --git a/native/android/performance_hint.cpp b/native/android/performance_hint.cpp
index d627984..7863a7d 100644
--- a/native/android/performance_hint.cpp
+++ b/native/android/performance_hint.cpp
@@ -61,6 +61,7 @@
int updateTargetWorkDuration(int64_t targetDurationNanos);
int reportActualWorkDuration(int64_t actualDurationNanos);
+ int sendHint(int32_t hint);
private:
friend struct APerformanceHintManager;
@@ -159,7 +160,7 @@
}
binder::Status ret = mHintSession->updateTargetWorkDuration(targetDurationNanos);
if (!ret.isOk()) {
- ALOGE("%s: HintSessionn updateTargetWorkDuration failed: %s", __FUNCTION__,
+ ALOGE("%s: HintSession updateTargetWorkDuration failed: %s", __FUNCTION__,
ret.exceptionMessage().c_str());
return EPIPE;
}
@@ -205,6 +206,21 @@
return 0;
}
+int APerformanceHintSession::sendHint(int32_t hint) {
+ if (hint < 0) {
+ ALOGE("%s: session hint value must be greater than zero", __FUNCTION__);
+ return EINVAL;
+ }
+
+ binder::Status ret = mHintSession->sendHint(hint);
+
+ if (!ret.isOk()) {
+ ALOGE("%s: HintSession sendHint failed: %s", __FUNCTION__, ret.exceptionMessage().c_str());
+ return EPIPE;
+ }
+ return 0;
+}
+
// ===================================== C API
APerformanceHintManager* APerformanceHint_getManager() {
return APerformanceHintManager::getInstance();
@@ -230,6 +246,10 @@
return session->reportActualWorkDuration(actualDurationNanos);
}
+int APerformanceHint_sendHint(APerformanceHintSession* session, int32_t hint) {
+ return session->sendHint(hint);
+}
+
void APerformanceHint_closeSession(APerformanceHintSession* session) {
delete session;
}
diff --git a/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp b/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
index b17850e..1881e60 100644
--- a/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
+++ b/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
@@ -51,6 +51,7 @@
(const ::std::vector<int64_t>& actualDurationNanos,
const ::std::vector<int64_t>& timeStampNanos),
(override));
+ MOCK_METHOD(Status, sendHint, (int32_t hints), (override));
MOCK_METHOD(Status, close, (), (override));
MOCK_METHOD(IBinder*, onAsBinder, (), (override));
};
@@ -121,6 +122,15 @@
result = APerformanceHint_reportActualWorkDuration(session, -1L);
EXPECT_EQ(EINVAL, result);
+ // Send both valid and invalid session hints
+ int hintId = 2;
+ EXPECT_CALL(*iSession, sendHint(Eq(2))).Times(Exactly(1));
+ result = APerformanceHint_sendHint(session, hintId);
+ EXPECT_EQ(0, result);
+
+ result = APerformanceHint_sendHint(session, -1);
+ EXPECT_EQ(EINVAL, result);
+
EXPECT_CALL(*iSession, close()).Times(Exactly(1));
APerformanceHint_closeSession(session);
}
diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java
index e67ea7e..5d1e164 100644
--- a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java
+++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java
@@ -105,7 +105,7 @@
loge("Unable to start the slice purchase application on the non-default data "
+ "subscription: " + mSubId);
SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponse(
- intent, SlicePurchaseController.EXTRA_INTENT_NOT_DEFAULT_DATA_SUB);
+ intent, SlicePurchaseController.EXTRA_INTENT_NOT_DEFAULT_DATA_SUBSCRIPTION);
finishAndRemoveTask();
return;
}
diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiver.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiver.java
index 5761b3c..c2ad2c560 100644
--- a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiver.java
+++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiver.java
@@ -173,7 +173,7 @@
&& isPendingIntentValid(intent, SlicePurchaseController.EXTRA_INTENT_CARRIER_ERROR)
&& isPendingIntentValid(intent, SlicePurchaseController.EXTRA_INTENT_REQUEST_FAILED)
&& isPendingIntentValid(intent,
- SlicePurchaseController.EXTRA_INTENT_NOT_DEFAULT_DATA_SUB)
+ SlicePurchaseController.EXTRA_INTENT_NOT_DEFAULT_DATA_SUBSCRIPTION)
&& isPendingIntentValid(intent, SlicePurchaseController.EXTRA_INTENT_SUCCESS);
}
@@ -204,8 +204,8 @@
case SlicePurchaseController.EXTRA_INTENT_CANCELED: return "canceled";
case SlicePurchaseController.EXTRA_INTENT_CARRIER_ERROR: return "carrier error";
case SlicePurchaseController.EXTRA_INTENT_REQUEST_FAILED: return "request failed";
- case SlicePurchaseController.EXTRA_INTENT_NOT_DEFAULT_DATA_SUB:
- return "not default data sub";
+ case SlicePurchaseController.EXTRA_INTENT_NOT_DEFAULT_DATA_SUBSCRIPTION:
+ return "not default data subscription";
case SlicePurchaseController.EXTRA_INTENT_SUCCESS: return "success";
default: {
loge("Unknown pending intent extra: " + extra);
diff --git a/packages/CredentialManager/res/drawable/ic_face.xml b/packages/CredentialManager/res/drawable/ic_face.xml
new file mode 100644
index 0000000..16fe144
--- /dev/null
+++ b/packages/CredentialManager/res/drawable/ic_face.xml
@@ -0,0 +1,30 @@
+<!--
+ ~ 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.
+ -->
+
+<!--TODO: Testing only icon. Remove later. -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:ignore="VectorPath"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="#808080"
+ android:pathData="M9.025,14.275Q8.5,14.275 8.125,13.9Q7.75,13.525 7.75,13Q7.75,12.475 8.125,12.1Q8.5,11.725 9.025,11.725Q9.575,11.725 9.938,12.1Q10.3,12.475 10.3,13Q10.3,13.525 9.938,13.9Q9.575,14.275 9.025,14.275ZM14.975,14.275Q14.425,14.275 14.062,13.9Q13.7,13.525 13.7,13Q13.7,12.475 14.062,12.1Q14.425,11.725 14.975,11.725Q15.5,11.725 15.875,12.1Q16.25,12.475 16.25,13Q16.25,13.525 15.875,13.9Q15.5,14.275 14.975,14.275ZM12,19.925Q15.325,19.925 17.625,17.625Q19.925,15.325 19.925,12Q19.925,11.4 19.85,10.85Q19.775,10.3 19.575,9.775Q19.05,9.9 18.538,9.962Q18.025,10.025 17.45,10.025Q15.2,10.025 13.188,9.062Q11.175,8.1 9.775,6.375Q8.975,8.3 7.5,9.712Q6.025,11.125 4.075,11.85Q4.075,11.9 4.075,11.925Q4.075,11.95 4.075,12Q4.075,15.325 6.375,17.625Q8.675,19.925 12,19.925ZM12,22.2Q9.9,22.2 8.038,21.4Q6.175,20.6 4.788,19.225Q3.4,17.85 2.6,15.988Q1.8,14.125 1.8,12Q1.8,9.875 2.6,8.012Q3.4,6.15 4.788,4.775Q6.175,3.4 8.038,2.6Q9.9,1.8 12,1.8Q14.125,1.8 15.988,2.6Q17.85,3.4 19.225,4.775Q20.6,6.15 21.4,8.012Q22.2,9.875 22.2,12Q22.2,14.125 21.4,15.988Q20.6,17.85 19.225,19.225Q17.85,20.6 15.988,21.4Q14.125,22.2 12,22.2Z"/>
+</vector>
\ No newline at end of file
diff --git a/packages/CredentialManager/res/drawable/ic_manage_accounts.xml b/packages/CredentialManager/res/drawable/ic_manage_accounts.xml
new file mode 100644
index 0000000..adad2f1
--- /dev/null
+++ b/packages/CredentialManager/res/drawable/ic_manage_accounts.xml
@@ -0,0 +1,30 @@
+<!--
+ ~ 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.
+ -->
+
+<!--TODO: Testing only icon. Remove later. -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:ignore="VectorPath"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="#808080"
+ android:pathData="M16.1,21.2 L15.775,19.675Q15.5,19.55 15.25,19.425Q15,19.3 14.75,19.1L13.275,19.575L12.2,17.75L13.375,16.725Q13.325,16.4 13.325,16.112Q13.325,15.825 13.375,15.5L12.2,14.475L13.275,12.65L14.75,13.1Q15,12.925 15.25,12.787Q15.5,12.65 15.775,12.55L16.1,11.025H18.25L18.55,12.55Q18.825,12.65 19.075,12.8Q19.325,12.95 19.575,13.15L21.05,12.65L22.125,14.525L20.95,15.55Q21.025,15.825 21.013,16.137Q21,16.45 20.95,16.725L22.125,17.75L21.05,19.575L19.575,19.1Q19.325,19.3 19.075,19.425Q18.825,19.55 18.55,19.675L18.25,21.2ZM1.8,20.3V17.3Q1.8,16.375 2.275,15.613Q2.75,14.85 3.5,14.475Q4.775,13.825 6.425,13.362Q8.075,12.9 10,12.9Q10.2,12.9 10.4,12.9Q10.6,12.9 10.775,12.95Q9.925,14.85 10.062,16.738Q10.2,18.625 11.4,20.3ZM17.175,18.075Q17.975,18.075 18.55,17.487Q19.125,16.9 19.125,16.1Q19.125,15.3 18.55,14.725Q17.975,14.15 17.175,14.15Q16.375,14.15 15.788,14.725Q15.2,15.3 15.2,16.1Q15.2,16.9 15.788,17.487Q16.375,18.075 17.175,18.075ZM10,11.9Q8.25,11.9 7.025,10.662Q5.8,9.425 5.8,7.7Q5.8,5.95 7.025,4.725Q8.25,3.5 10,3.5Q11.75,3.5 12.975,4.725Q14.2,5.95 14.2,7.7Q14.2,9.425 12.975,10.662Q11.75,11.9 10,11.9Z"/>
+</vector>
\ No newline at end of file
diff --git a/packages/CredentialManager/res/drawable/ic_other_devices.xml b/packages/CredentialManager/res/drawable/ic_other_devices.xml
new file mode 100644
index 0000000..754648c
--- /dev/null
+++ b/packages/CredentialManager/res/drawable/ic_other_devices.xml
@@ -0,0 +1,15 @@
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:ignore="VectorPath"
+ android:name="vector"
+ android:width="20dp"
+ android:height="20dp"
+ android:viewportWidth="20"
+ android:viewportHeight="20">
+ <path
+ android:name="path"
+ android:pathData="M 7.6 4.72 L 7.6 7.6 L 4.72 7.6 L 4.72 4.72 L 7.6 4.72 Z M 9.04 3.28 L 3.28 3.28 L 3.28 9.04 L 9.04 9.04 L 9.04 3.28 Z M 7.6 12.4 L 7.6 15.28 L 4.72 15.28 L 4.72 12.4 L 7.6 12.4 Z M 9.04 10.96 L 3.28 10.96 L 3.28 16.72 L 9.04 16.72 L 9.04 10.96 Z M 15.28 4.72 L 15.28 7.6 L 12.4 7.6 L 12.4 4.72 L 15.28 4.72 Z M 16.72 3.28 L 10.96 3.28 L 10.96 9.04 L 16.72 9.04 L 16.72 3.28 Z M 10.96 10.96 L 12.4 10.96 L 12.4 12.4 L 10.96 12.4 L 10.96 10.96 Z M 12.4 12.4 L 13.84 12.4 L 13.84 13.84 L 12.4 13.84 L 12.4 12.4 Z M 13.84 10.96 L 15.28 10.96 L 15.28 12.4 L 13.84 12.4 L 13.84 10.96 Z M 10.96 13.84 L 12.4 13.84 L 12.4 15.28 L 10.96 15.28 L 10.96 13.84 Z M 12.4 15.28 L 13.84 15.28 L 13.84 16.72 L 12.4 16.72 L 12.4 15.28 Z M 13.84 13.84 L 15.28 13.84 L 15.28 15.28 L 13.84 15.28 L 13.84 13.84 Z M 15.28 12.4 L 16.72 12.4 L 16.72 13.84 L 15.28 13.84 L 15.28 12.4 Z M 15.28 15.28 L 16.72 15.28 L 16.72 16.72 L 15.28 16.72 L 15.28 15.28 Z M 19.6 5.2 L 17.68 5.2 L 17.68 2.32 L 14.8 2.32 L 14.8 0.4 L 19.6 0.4 L 19.6 5.2 Z M 19.6 19.6 L 19.6 14.8 L 17.68 14.8 L 17.68 17.68 L 14.8 17.68 L 14.8 19.6 L 19.6 19.6 Z M 0.4 19.6 L 5.2 19.6 L 5.2 17.68 L 2.32 17.68 L 2.32 14.8 L 0.4 14.8 L 0.4 19.6 Z M 0.4 0.4 L 0.4 5.2 L 2.32 5.2 L 2.32 2.32 L 5.2 2.32 L 5.2 0.4 L 0.4 0.4 Z"
+ android:fillColor="#000000"
+ android:strokeWidth="1"/>
+</vector>
\ No newline at end of file
diff --git a/packages/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml
index cdd1a89..2f6d1b4 100644
--- a/packages/CredentialManager/res/values/strings.xml
+++ b/packages/CredentialManager/res/values/strings.xml
@@ -28,7 +28,10 @@
<string name="passkey">passkey</string>
<string name="password">password</string>
<string name="sign_ins">sign-ins</string>
+ <string name="another_device">Another device</string>
<string name="other_password_manager">Other password manager</string>
+ <!-- TODO: Check the wording here. -->
+ <string name="confirm_default_or_use_once_description">This password manager will store your passwords and passkeys to help you easily sign in.</string>
<!-- Spoken content description of an element which will close the sheet when clicked. -->
<string name="close_sheet">"Close sheet"</string>
<!-- Spoken content description of the back arrow button. -->
@@ -51,6 +54,12 @@
<string name="get_dialog_sign_in_type_username_separator" translatable="false">" - "</string>
<!-- Modal bottom sheet title for displaying all the available sign-in options. [CHAR LIMIT=80] -->
<string name="get_dialog_title_sign_in_options">Sign-in options</string>
- <!-- Column heading for displaying sign-ins for a specific username. [CHAR LIMIT=20] -->
+ <!-- Column heading for displaying sign-ins for a specific username. [CHAR LIMIT=80] -->
<string name="get_dialog_heading_for_username">For <xliff:g id="username" example="becket@gmail.com">%1$s</xliff:g></string>
+ <!-- Column heading for displaying locked (that is, the user needs to first authenticate via pin, fingerprint, faceId, etc.) sign-ins. [CHAR LIMIT=80] -->
+ <string name="get_dialog_heading_locked_password_managers">Locked password managers</string>
+ <!-- Explanatory sub/body text for an option entry to use a locked (that is, the user needs to first authenticate via pin, fingerprint, faceId, etc.) sign-in. [CHAR LIMIT=120] -->
+ <string name="locked_credential_entry_label_subtext">Tap to unlock</string>
+ <!-- Column heading for displaying action chips for managing sign-ins from each credential provider. [CHAR LIMIT=80] -->
+ <string name="get_dialog_heading_manage_sign_ins">Manage sign-ins</string>
</resources>
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
index b71af2b..8bd7cf0 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
@@ -181,6 +181,9 @@
20, 7, 27, 11000),
)
)
+ .setRemoteEntry(
+ newRemoteEntry("key1", "subkey-1")
+ )
.setIsDefaultProvider(true)
.build(),
CreateCredentialProviderData
@@ -206,7 +209,7 @@
private fun testGetCredentialProviderList(): List<GetCredentialProviderData> {
return listOf(
- GetCredentialProviderData.Builder("com.google/com.google.CredentialManagerService")
+ GetCredentialProviderData.Builder("io.enpass.app")
.setCredentialEntries(
listOf<Entry>(
newGetEntry(
@@ -222,8 +225,23 @@
"elisa.family@outlook.com", null, 100L
),
)
+ ).setAuthenticationEntry(
+ newAuthenticationEntry("key2", "subkey-1", TYPE_PASSWORD_CREDENTIAL)
+ ).setActionChips(
+ listOf(
+ newActionEntry(
+ "key3", "subkey-1", TYPE_PASSWORD_CREDENTIAL,
+ Icon.createWithResource(context, R.drawable.ic_manage_accounts),
+ "Open Google Password Manager", "elisa.beckett@gmail.com"
+ ),
+ newActionEntry(
+ "key3", "subkey-2", TYPE_PASSWORD_CREDENTIAL,
+ Icon.createWithResource(context, R.drawable.ic_manage_accounts),
+ "Open Google Password Manager", "beckett-family@gmail.com"
+ ),
+ )
).build(),
- GetCredentialProviderData.Builder("com.dashlane/com.dashlane.CredentialManagerService")
+ GetCredentialProviderData.Builder("com.dashlane")
.setCredentialEntries(
listOf<Entry>(
newGetEntry(
@@ -235,10 +253,58 @@
"elisa.family@outlook.com", null, 100L
),
)
+ ).setAuthenticationEntry(
+ newAuthenticationEntry("key2", "subkey-1", TYPE_PASSWORD_CREDENTIAL)
+ ).setActionChips(
+ listOf(
+ newActionEntry(
+ "key3", "subkey-1", TYPE_PASSWORD_CREDENTIAL,
+ Icon.createWithResource(context, R.drawable.ic_face),
+ "Open Enpass"
+ ),
+ )
).build(),
)
}
+ private fun newActionEntry(
+ key: String,
+ subkey: String,
+ credentialType: String,
+ icon: Icon,
+ text: String,
+ subtext: String? = null,
+ ): Entry {
+ val slice = Slice.Builder(
+ Entry.CREDENTIAL_MANAGER_ENTRY_URI, SliceSpec(credentialType, 1)
+ ).addText(
+ text, null, listOf(Entry.HINT_ACTION_TITLE)
+ ).addIcon(icon, null, listOf(Entry.HINT_ACTION_ICON))
+ if (subtext != null) {
+ slice.addText(subtext, null, listOf(Entry.HINT_ACTION_SUBTEXT))
+ }
+ return Entry(
+ key,
+ subkey,
+ slice.build()
+ )
+ }
+
+ private fun newAuthenticationEntry(
+ key: String,
+ subkey: String,
+ credentialType: String,
+ ): Entry {
+ val slice = Slice.Builder(
+ Entry.CREDENTIAL_MANAGER_ENTRY_URI, SliceSpec(credentialType, 1)
+ )
+ return Entry(
+ key,
+ subkey,
+ slice.build()
+ )
+ }
+
private fun newGetEntry(
key: String,
subkey: String,
@@ -308,6 +374,19 @@
)
}
+ private fun newRemoteEntry(
+ key: String,
+ subkey: String,
+ ): Entry {
+ return Entry(
+ key,
+ subkey,
+ Slice.Builder(
+ Entry.CREDENTIAL_MANAGER_ENTRY_URI, SliceSpec(Entry.VERSION, 1)
+ ).build()
+ )
+ }
+
private fun testCreateRequestInfo(): RequestInfo {
val data = Bundle()
return RequestInfo.newCreateRequestInfo(
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
index 53e817e..33fb154 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
@@ -22,11 +22,14 @@
import android.credentials.ui.GetCredentialProviderData
import android.credentials.ui.CreateCredentialProviderData
import android.credentials.ui.DisabledProviderData
+import android.graphics.drawable.Drawable
import com.android.credentialmanager.createflow.CreateOptionInfo
+import com.android.credentialmanager.createflow.RemoteInfo
import com.android.credentialmanager.getflow.ActionEntryInfo
import com.android.credentialmanager.getflow.AuthenticationEntryInfo
import com.android.credentialmanager.getflow.CredentialEntryInfo
import com.android.credentialmanager.getflow.ProviderInfo
+import com.android.credentialmanager.jetpack.provider.ActionUi
import com.android.credentialmanager.jetpack.provider.CredentialEntryUi
import com.android.credentialmanager.jetpack.provider.SaveEntryUi
@@ -38,17 +41,27 @@
providerDataList: List<GetCredentialProviderData>,
context: Context,
): List<ProviderInfo> {
+ val packageManager = context.packageManager
return providerDataList.map {
+ // TODO: get from the actual service info
+ val pkgInfo = packageManager
+ .getPackageInfo(it.providerFlattenedComponentName,
+ PackageManager.PackageInfoFlags.of(0))
+ val providerDisplayName = pkgInfo.applicationInfo.loadLabel(packageManager).toString()
+ // TODO: decide what to do when failed to load a provider icon
+ val providerIcon = pkgInfo.applicationInfo.loadIcon(packageManager)!!
ProviderInfo(
id = it.providerFlattenedComponentName,
- // TODO: replace to extract from the service data structure when available
- icon = context.getDrawable(R.drawable.ic_passkey)!!,
- // TODO: get the service display name and icon from the component name.
- displayName = it.providerFlattenedComponentName,
+ // TODO: decide what to do when failed to load a provider icon
+ icon = providerIcon,
+ displayName = providerDisplayName,
credentialEntryList = getCredentialOptionInfoList(
it.providerFlattenedComponentName, it.credentialEntries, context),
authenticationEntry = getAuthenticationEntry(
- it.providerFlattenedComponentName, it.authenticationEntry, context),
+ it.providerFlattenedComponentName,
+ providerDisplayName,
+ providerIcon,
+ it.authenticationEntry),
actionEntryList = getActionEntryList(
it.providerFlattenedComponentName, it.actionChips, context),
)
@@ -84,11 +97,22 @@
private fun getAuthenticationEntry(
providerId: String,
+ providerDisplayName: String,
+ providerIcon: Drawable,
authEntry: Entry?,
- context: Context,
): AuthenticationEntryInfo? {
- // TODO: implement
- return null
+ // TODO: should also call fromSlice after getting the official jetpack code.
+
+ if (authEntry == null) {
+ return null
+ }
+ return AuthenticationEntryInfo(
+ providerId = providerId,
+ entryKey = authEntry.key,
+ entrySubkey = authEntry.subkey,
+ title = providerDisplayName,
+ icon = providerIcon,
+ )
}
private fun getActionEntryList(
@@ -96,8 +120,19 @@
actionEntries: List<Entry>,
context: Context,
): List<ActionEntryInfo> {
- // TODO: implement
- return emptyList()
+ return actionEntries.map {
+ val actionEntryUi = ActionUi.fromSlice(it.slice)
+
+ return@map ActionEntryInfo(
+ providerId = providerId,
+ entryKey = it.key,
+ entrySubkey = it.subkey,
+ title = actionEntryUi.text.toString(),
+ // TODO: gracefully fail
+ icon = actionEntryUi.icon.loadDrawable(context)!!,
+ subTitle = actionEntryUi.subtext?.toString(),
+ )
+ }
}
}
}
@@ -109,17 +144,20 @@
providerDataList: List<CreateCredentialProviderData>,
context: Context,
): List<com.android.credentialmanager.createflow.EnabledProviderInfo> {
+ // TODO: get from the actual service info
val packageManager = context.packageManager
return providerDataList.map {
val pkgInfo = packageManager
.getPackageInfo(it.providerFlattenedComponentName,
PackageManager.PackageInfoFlags.of(0))
com.android.credentialmanager.createflow.EnabledProviderInfo(
+ // TODO: decide what to do when failed to load a provider icon
icon = pkgInfo.applicationInfo.loadIcon(packageManager)!!,
name = it.providerFlattenedComponentName,
displayName = pkgInfo.applicationInfo.loadLabel(packageManager).toString(),
createOptions = toCreationOptionInfoList(it.saveEntries, context),
isDefault = it.isDefaultProvider,
+ remoteEntry = toRemoteInfo(it.remoteEntry),
)
}
}
@@ -128,6 +166,7 @@
providerDataList: List<DisabledProviderData>,
context: Context,
): List<com.android.credentialmanager.createflow.DisabledProviderInfo> {
+ // TODO: get from the actual service info
val packageManager = context.packageManager
return providerDataList.map {
val pkgInfo = packageManager
@@ -164,5 +203,17 @@
)
}
}
+
+ private fun toRemoteInfo(
+ remoteEntry: Entry?,
+ ): RemoteInfo? {
+ // TODO: should also call fromSlice after getting the official jetpack code.
+ return if (remoteEntry != null) {
+ RemoteInfo(
+ entryKey = remoteEntry.key,
+ entrySubkey = remoteEntry.subkey,
+ )
+ } else null
+ }
}
}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
index 0c3447f..123c3d4 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
@@ -30,6 +30,7 @@
displayName: String,
var createOptions: List<CreateOptionInfo>,
val isDefault: Boolean,
+ var remoteEntry: RemoteInfo?,
) : ProviderInfo(icon, name, displayName)
class DisabledProviderInfo(
@@ -55,6 +56,11 @@
val lastUsedTimeMillis: Long?,
) : EntryInfo(entryKey, entrySubkey)
+class RemoteInfo(
+ entryKey: String,
+ entrySubkey: String,
+) : EntryInfo(entryKey, entrySubkey)
+
data class RequestDisplayInfo(
val userName: String,
val displayName: String,
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt
index 4769a26..67b704f 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt
@@ -21,6 +21,7 @@
import androidx.compose.material3.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.material.icons.outlined.NewReleases
import androidx.compose.material.icons.filled.Add
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -80,7 +81,8 @@
disabledProviderList = uiState.disabledProviders,
onBackButtonSelected = viewModel::onBackButtonSelected,
onOptionSelected = viewModel::onMoreOptionsRowSelected,
- onDisabledPasswordManagerSelected = viewModel::onDisabledPasswordManagerSelected
+ onDisabledPasswordManagerSelected = viewModel::onDisabledPasswordManagerSelected,
+ onRemoteEntrySelected = viewModel::onRemoteEntrySelected
)
CreateScreenState.MORE_OPTIONS_ROW_INTRO -> MoreOptionsRowIntroCard(
providerInfo = uiState.activeEntry?.activeProvider!!,
@@ -216,10 +218,11 @@
fun MoreOptionsSelectionCard(
requestDisplayInfo: RequestDisplayInfo,
enabledProviderList: List<EnabledProviderInfo>,
- disabledProviderList: List<DisabledProviderInfo>,
+ disabledProviderList: List<DisabledProviderInfo>?,
onBackButtonSelected: () -> Unit,
onOptionSelected: (ActiveEntry) -> Unit,
onDisabledPasswordManagerSelected: () -> Unit,
+ onRemoteEntrySelected: () -> Unit,
) {
Card() {
Column() {
@@ -267,11 +270,26 @@
}
}
}
- item {
- MoreOptionsDisabledProvidersRow(
- disabledProviders = disabledProviderList,
- onDisabledPasswordManagerSelected = onDisabledPasswordManagerSelected,
- )
+ if (disabledProviderList != null) {
+ item {
+ MoreOptionsDisabledProvidersRow(
+ disabledProviders = disabledProviderList,
+ onDisabledPasswordManagerSelected = onDisabledPasswordManagerSelected,
+ )
+ }
+ }
+ var hasRemoteInfo = false
+ enabledProviderList.forEach {
+ if (it.remoteEntry != null) {
+ hasRemoteInfo = true
+ }
+ }
+ if (hasRemoteInfo) {
+ item {
+ RemoteEntryRow(
+ onRemoteEntrySelected = onRemoteEntrySelected,
+ )
+ }
}
}
}
@@ -292,9 +310,21 @@
) {
Card() {
Column() {
+ Icon(
+ Icons.Outlined.NewReleases,
+ contentDescription = null,
+ modifier = Modifier.align(alignment = Alignment.CenterHorizontally).padding(all = 24.dp)
+ )
Text(
text = stringResource(R.string.use_provider_for_all_title, providerInfo.displayName),
style = MaterialTheme.typography.titleMedium,
+ modifier = Modifier.padding(horizontal = 24.dp)
+ .align(alignment = Alignment.CenterHorizontally),
+ textAlign = TextAlign.Center,
+ )
+ Text(
+ text = stringResource(R.string.confirm_default_or_use_once_description),
+ style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(all = 24.dp).align(alignment = Alignment.CenterHorizontally)
)
Row(
@@ -498,7 +528,7 @@
modifier = Modifier.fillMaxWidth(),
onClick = onOptionSelected,
icon = {
- Image(modifier = Modifier.size(32.dp, 32.dp).padding(start = 10.dp),
+ Image(modifier = Modifier.size(32.dp, 32.dp).padding(start = 16.dp),
bitmap = providerInfo.icon.toBitmap().asImageBitmap(),
contentDescription = null)
},
@@ -508,12 +538,14 @@
Text(
text = providerInfo.displayName,
style = MaterialTheme.typography.titleLarge,
- modifier = Modifier.padding(top = 16.dp)
+ modifier = Modifier.padding(top = 16.dp, start = 16.dp)
)
if (createOptionInfo.userProviderDisplayName != null) {
Text(
text = createOptionInfo.userProviderDisplayName,
- style = MaterialTheme.typography.bodyMedium)
+ style = MaterialTheme.typography.bodyMedium,
+ modifier = Modifier.padding(start = 16.dp)
+ )
}
if (createOptionInfo.passwordCount != null && createOptionInfo.passkeyCount != null) {
Text(
@@ -524,7 +556,7 @@
createOptionInfo.passkeyCount
),
style = MaterialTheme.typography.bodyMedium,
- modifier = Modifier.padding(bottom = 16.dp)
+ modifier = Modifier.padding(bottom = 16.dp, start = 16.dp)
)
} else if (createOptionInfo.passwordCount != null) {
Text(
@@ -534,7 +566,7 @@
createOptionInfo.passwordCount
),
style = MaterialTheme.typography.bodyMedium,
- modifier = Modifier.padding(bottom = 16.dp)
+ modifier = Modifier.padding(bottom = 16.dp, start = 16.dp)
)
} else if (createOptionInfo.passkeyCount != null) {
Text(
@@ -544,7 +576,7 @@
createOptionInfo.passkeyCount
),
style = MaterialTheme.typography.bodyMedium,
- modifier = Modifier.padding(bottom = 16.dp)
+ modifier = Modifier.padding(bottom = 16.dp, start = 16.dp)
)
} else if (createOptionInfo.totalCredentialCount != null) {
// TODO: Handle the case when there is total count
@@ -567,7 +599,8 @@
icon = {
Icon(
Icons.Filled.Add,
- contentDescription = null
+ contentDescription = null,
+ modifier = Modifier.padding(start = 16.dp)
)
},
shape = MaterialTheme.shapes.large,
@@ -576,12 +609,42 @@
Text(
text = stringResource(R.string.other_password_manager),
style = MaterialTheme.typography.titleLarge,
- modifier = Modifier.padding(top = 16.dp)
+ modifier = Modifier.padding(top = 16.dp, start = 16.dp)
)
Text(
text = disabledProviders.joinToString(separator = ", "){ it.displayName },
style = MaterialTheme.typography.bodyMedium,
- modifier = Modifier.padding(bottom = 16.dp)
+ modifier = Modifier.padding(bottom = 16.dp, start = 16.dp)
+ )
+ }
+ }
+ )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun RemoteEntryRow(
+ onRemoteEntrySelected: () -> Unit,
+) {
+ SuggestionChip(
+ modifier = Modifier.fillMaxWidth(),
+ onClick = onRemoteEntrySelected,
+ icon = {
+ Icon(
+ painter = painterResource(R.drawable.ic_other_devices),
+ contentDescription = null,
+ tint = Color.Unspecified,
+ modifier = Modifier.padding(start = 18.dp)
+ )
+ },
+ shape = MaterialTheme.shapes.large,
+ label = {
+ Column() {
+ Text(
+ text = stringResource(R.string.another_device),
+ style = MaterialTheme.typography.titleLarge,
+ modifier = Modifier.padding(start = 16.dp, top = 18.dp, bottom = 18.dp)
+ .align(alignment = Alignment.CenterHorizontally)
)
}
}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt
index 8b94201..af74b8e 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt
@@ -29,7 +29,7 @@
data class CreatePasskeyUiState(
val enabledProviders: List<EnabledProviderInfo>,
- val disabledProviders: List<DisabledProviderInfo>,
+ val disabledProviders: List<DisabledProviderInfo>? = null,
val currentScreenState: CreateScreenState,
val requestDisplayInfo: RequestDisplayInfo,
val activeEntry: ActiveEntry? = null,
@@ -103,6 +103,10 @@
// TODO: Complete this function
}
+ fun onRemoteEntrySelected() {
+ // TODO: Complete this function
+ }
+
fun onCancel() {
CredentialManagerRepo.getInstance().onCancel()
dialogResult.value = DialogResult(ResultState.CANCELED)
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
index 23592c3..dcdd71a 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
@@ -55,8 +55,6 @@
import com.android.credentialmanager.common.ui.CancelButton
import com.android.credentialmanager.jetpack.developer.PublicKeyCredential
-
-@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun GetCredentialScreen(
viewModel: GetCredentialViewModel,
@@ -72,13 +70,14 @@
when (uiState.currentScreenState) {
GetScreenState.PRIMARY_SELECTION -> PrimarySelectionCard(
requestDisplayInfo = uiState.requestDisplayInfo,
- sortedUserNameToCredentialEntryList = uiState.sortedUserNameToCredentialEntryList,
+ providerDisplayInfo = uiState.providerDisplayInfo,
onEntrySelected = viewModel::onEntrySelected,
onCancel = viewModel::onCancel,
onMoreOptionSelected = viewModel::onMoreOptionSelected,
)
GetScreenState.ALL_SIGN_IN_OPTIONS -> AllSignInOptionCard(
- sortedUserNameToCredentialEntryList = uiState.sortedUserNameToCredentialEntryList,
+ providerInfoList = uiState.providerInfoList,
+ providerDisplayInfo = uiState.providerDisplayInfo,
onEntrySelected = viewModel::onEntrySelected,
onBackButtonClicked = viewModel::onBackToPrimarySelectionScreen,
)
@@ -95,15 +94,16 @@
}
/** Draws the primary credential selection page. */
-@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PrimarySelectionCard(
requestDisplayInfo: RequestDisplayInfo,
- sortedUserNameToCredentialEntryList: List<PerUserNameCredentialEntryList>,
+ providerDisplayInfo: ProviderDisplayInfo,
onEntrySelected: (EntryInfo) -> Unit,
onCancel: () -> Unit,
onMoreOptionSelected: () -> Unit,
) {
+ val sortedUserNameToCredentialEntryList = providerDisplayInfo.sortedUserNameToCredentialEntryList
+ val authenticationEntryList = providerDisplayInfo.authenticationEntryList
Card() {
Column() {
Text(
@@ -133,7 +133,13 @@
items(sortedUserNameToCredentialEntryList) {
CredentialEntryRow(
credentialEntryInfo = it.sortedCredentialEntryList.first(),
- onEntrySelected = onEntrySelected
+ onEntrySelected = onEntrySelected,
+ )
+ }
+ items(authenticationEntryList) {
+ AuthenticationEntryRow(
+ authenticationEntryInfo = it,
+ onEntrySelected = onEntrySelected,
)
}
item {
@@ -164,10 +170,13 @@
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AllSignInOptionCard(
- sortedUserNameToCredentialEntryList: List<PerUserNameCredentialEntryList>,
+ providerInfoList: List<ProviderInfo>,
+ providerDisplayInfo: ProviderDisplayInfo,
onEntrySelected: (EntryInfo) -> Unit,
onBackButtonClicked: () -> Unit,
) {
+ val sortedUserNameToCredentialEntryList = providerDisplayInfo.sortedUserNameToCredentialEntryList
+ val authenticationEntryList = providerDisplayInfo.authenticationEntryList
Card() {
Column() {
TopAppBar(
@@ -199,19 +208,74 @@
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
+ // For username
items(sortedUserNameToCredentialEntryList) { item ->
PerUserNameCredentials(
perUserNameCredentialEntryList = item,
- onEntrySelected = onEntrySelected
+ onEntrySelected = onEntrySelected,
)
}
+ // Locked password manager
+ item {
+ if (!authenticationEntryList.isEmpty()) {
+ LockedCredentials(
+ authenticationEntryList = authenticationEntryList,
+ onEntrySelected = onEntrySelected,
+ )
+ }
+ }
+ // TODO: Remote action
+ // Manage sign-ins
+ item {
+ ActionChips(providerInfoList = providerInfoList, onEntrySelected = onEntrySelected)
+ }
}
}
}
}
}
-@OptIn(ExperimentalMaterial3Api::class)
+// TODO: create separate rows for primary and secondary pages.
+// TODO: reuse rows and columns across types.
+
+@Composable
+fun ActionChips(
+ providerInfoList: List<ProviderInfo>,
+ onEntrySelected: (EntryInfo) -> Unit,
+) {
+ val actionChips = providerInfoList.flatMap { it.actionEntryList }
+ if (actionChips.isEmpty()) {
+ return
+ }
+
+ Text(
+ text = stringResource(R.string.get_dialog_heading_manage_sign_ins),
+ style = MaterialTheme.typography.labelLarge,
+ modifier = Modifier.padding(vertical = 8.dp)
+ )
+ // TODO: tweak padding.
+ Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
+ actionChips.forEach {
+ ActionEntryRow(it, onEntrySelected)
+ }
+ }
+}
+
+@Composable
+fun LockedCredentials(
+ authenticationEntryList: List<AuthenticationEntryInfo>,
+ onEntrySelected: (EntryInfo) -> Unit,
+) {
+ Text(
+ text = stringResource(R.string.get_dialog_heading_locked_password_managers),
+ style = MaterialTheme.typography.labelLarge,
+ modifier = Modifier.padding(vertical = 8.dp)
+ )
+ authenticationEntryList.forEach {
+ AuthenticationEntryRow(it, onEntrySelected)
+ }
+}
+
@Composable
fun PerUserNameCredentials(
perUserNameCredentialEntryList: PerUserNameCredentialEntryList,
@@ -270,6 +334,73 @@
@OptIn(ExperimentalMaterial3Api::class)
@Composable
+fun AuthenticationEntryRow(
+ authenticationEntryInfo: AuthenticationEntryInfo,
+ onEntrySelected: (EntryInfo) -> Unit,
+) {
+ SuggestionChip(
+ modifier = Modifier.fillMaxWidth(),
+ onClick = {onEntrySelected(authenticationEntryInfo)},
+ icon = {
+ Image(modifier = Modifier.size(24.dp, 24.dp).padding(start = 10.dp),
+ bitmap = authenticationEntryInfo.icon.toBitmap().asImageBitmap(),
+ // TODO: add description.
+ contentDescription = "")
+ },
+ shape = MaterialTheme.shapes.large,
+ label = {
+ Column() {
+ // TODO: fix the text values.
+ Text(
+ text = authenticationEntryInfo.title,
+ style = MaterialTheme.typography.titleLarge,
+ modifier = Modifier.padding(top = 16.dp)
+ )
+ Text(
+ text = stringResource(R.string.locked_credential_entry_label_subtext),
+ style = MaterialTheme.typography.bodyMedium,
+ modifier = Modifier.padding(bottom = 16.dp)
+ )
+ }
+ }
+ )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun ActionEntryRow(
+ actionEntryInfo: ActionEntryInfo,
+ onEntrySelected: (EntryInfo) -> Unit,
+) {
+ SuggestionChip(
+ modifier = Modifier.fillMaxWidth(),
+ onClick = { onEntrySelected(actionEntryInfo) },
+ icon = {
+ Image(modifier = Modifier.size(24.dp, 24.dp).padding(start = 10.dp),
+ bitmap = actionEntryInfo.icon.toBitmap().asImageBitmap(),
+ // TODO: add description.
+ contentDescription = "")
+ },
+ shape = MaterialTheme.shapes.large,
+ label = {
+ Column() {
+ Text(
+ text = actionEntryInfo.title,
+ style = MaterialTheme.typography.titleLarge,
+ )
+ if (actionEntryInfo.subTitle != null) {
+ Text(
+ text = actionEntryInfo.subTitle,
+ style = MaterialTheme.typography.bodyMedium,
+ )
+ }
+ }
+ }
+ )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
fun SignInAnotherWayRow(onSelect: () -> Unit) {
SuggestionChip(
modifier = Modifier.fillMaxWidth(),
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt
index f449274..f78456a 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt
@@ -32,12 +32,7 @@
val providerInfoList: List<ProviderInfo>,
val currentScreenState: GetScreenState,
val requestDisplayInfo: RequestDisplayInfo,
- /**
- * The credential entries grouped by userName, derived from all entries of the [providerInfoList].
- * Note that the list order matters to the display order.
- */
- val sortedUserNameToCredentialEntryList: List<PerUserNameCredentialEntryList> =
- createSortedUserNameToCredentialEntryList(providerInfoList),
+ val providerDisplayInfo: ProviderDisplayInfo = toProviderDisplayInfo(providerInfoList),
)
class GetCredentialViewModel(
@@ -85,14 +80,19 @@
}
}
-internal fun createSortedUserNameToCredentialEntryList(
+private fun toProviderDisplayInfo(
providerInfoList: List<ProviderInfo>
-): List<PerUserNameCredentialEntryList> {
- // Group by username
- val userNameToEntryMap = mutableMapOf<String, MutableList<CredentialEntryInfo>>()
+): ProviderDisplayInfo {
+
+ val userNameToCredentialEntryMap = mutableMapOf<String, MutableList<CredentialEntryInfo>>()
+ val authenticationEntryList = mutableListOf<AuthenticationEntryInfo>()
providerInfoList.forEach { providerInfo ->
+ if (providerInfo.authenticationEntry != null) {
+ authenticationEntryList.add(providerInfo.authenticationEntry)
+ }
+
providerInfo.credentialEntryList.forEach {
- userNameToEntryMap.compute(
+ userNameToCredentialEntryMap.compute(
it.userName
) {
_, v ->
@@ -105,17 +105,24 @@
}
}
}
+
+ // Compose sortedUserNameToCredentialEntryList
val comparator = CredentialEntryInfoComparator()
// Sort per username
- userNameToEntryMap.values.forEach {
+ userNameToCredentialEntryMap.values.forEach {
it.sortWith(comparator)
}
// Transform to list of PerUserNameCredentialEntryLists and then sort across usernames
- return userNameToEntryMap.map {
+ val sortedUserNameToCredentialEntryList = userNameToCredentialEntryMap.map {
PerUserNameCredentialEntryList(it.key, it.value)
}.sortedWith(
compareBy(comparator) { it.sortedCredentialEntryList.first() }
)
+
+ return ProviderDisplayInfo(
+ sortedUserNameToCredentialEntryList = sortedUserNameToCredentialEntryList,
+ authenticationEntryList = authenticationEntryList,
+ )
}
internal class CredentialEntryInfoComparator : Comparator<CredentialEntryInfo> {
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
index 84009b1..c1d9ea9 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
@@ -32,6 +32,17 @@
// TODO: add remote entry
)
+/** Display-centric data structure derived from the [ProviderInfo]. This abstraction is not grouping
+ * by the provider id but instead focuses on structures convenient for display purposes. */
+data class ProviderDisplayInfo(
+ /**
+ * The credential entries grouped by userName, derived from all entries of the [providerInfoList].
+ * Note that the list order matters to the display order.
+ */
+ val sortedUserNameToCredentialEntryList: List<PerUserNameCredentialEntryList>,
+ val authenticationEntryList: List<AuthenticationEntryInfo>,
+)
+
abstract class EntryInfo (
/** Unique id combination of this entry. Not for display purpose. */
val providerId: String,
@@ -59,6 +70,8 @@
providerId: String,
entryKey: String,
entrySubkey: String,
+ val title: String,
+ val icon: Drawable,
) : EntryInfo(providerId, entryKey, entrySubkey)
class ActionEntryInfo(
@@ -66,6 +79,7 @@
entryKey: String,
entrySubkey: String,
val title: String,
+ val icon: Drawable,
val subTitle: String?,
) : EntryInfo(providerId, entryKey, entrySubkey)
diff --git a/packages/SettingsLib/Spa/build.gradle b/packages/SettingsLib/Spa/build.gradle
index 4fb77d7..b8fd579 100644
--- a/packages/SettingsLib/Spa/build.gradle
+++ b/packages/SettingsLib/Spa/build.gradle
@@ -16,9 +16,10 @@
buildscript {
ext {
- spa_min_sdk = 21
- spa_target_sdk = 33
- jetpack_compose_version = '1.3.0'
+ BUILD_TOOLS_VERSION = "30.0.3"
+ MIN_SDK = 21
+ TARGET_SDK = 33
+ jetpack_compose_version = '1.4.0-alpha01'
jetpack_compose_compiler_version = '1.3.2'
}
}
diff --git a/packages/SettingsLib/Spa/gallery/build.gradle b/packages/SettingsLib/Spa/gallery/build.gradle
index e04a9be..7868aff 100644
--- a/packages/SettingsLib/Spa/gallery/build.gradle
+++ b/packages/SettingsLib/Spa/gallery/build.gradle
@@ -21,12 +21,13 @@
android {
namespace 'com.android.settingslib.spa.gallery'
- compileSdk spa_target_sdk
+ compileSdk TARGET_SDK
+ buildToolsVersion = BUILD_TOOLS_VERSION
defaultConfig {
applicationId "com.android.settingslib.spa.gallery"
- minSdk spa_min_sdk
- targetSdk spa_target_sdk
+ minSdk MIN_SDK
+ targetSdk TARGET_SDK
versionCode 1
versionName "1.0"
}
diff --git a/packages/SettingsLib/Spa/spa/Android.bp b/packages/SettingsLib/Spa/spa/Android.bp
index 037b45e..2eaa73e 100644
--- a/packages/SettingsLib/Spa/spa/Android.bp
+++ b/packages/SettingsLib/Spa/spa/Android.bp
@@ -24,6 +24,9 @@
srcs: ["src/**/*.kt"],
static_libs: [
+ "androidx.slice_slice-builders",
+ "androidx.slice_slice-core",
+ "androidx.slice_slice-view",
"androidx.compose.material3_material3",
"androidx.compose.material_material-icons-extended",
"androidx.compose.runtime_runtime",
diff --git a/packages/SettingsLib/Spa/spa/build.gradle b/packages/SettingsLib/Spa/spa/build.gradle
index 7a20c747..4944784 100644
--- a/packages/SettingsLib/Spa/spa/build.gradle
+++ b/packages/SettingsLib/Spa/spa/build.gradle
@@ -21,11 +21,12 @@
android {
namespace 'com.android.settingslib.spa'
- compileSdk spa_target_sdk
+ compileSdk TARGET_SDK
+ buildToolsVersion = BUILD_TOOLS_VERSION
defaultConfig {
- minSdk spa_min_sdk
- targetSdk spa_target_sdk
+ minSdk MIN_SDK
+ targetSdk TARGET_SDK
}
sourceSets {
@@ -55,6 +56,9 @@
dependencies {
api "androidx.appcompat:appcompat:1.7.0-alpha01"
+ api "androidx.slice:slice-builders:1.1.0-alpha02"
+ api "androidx.slice:slice-core:1.1.0-alpha02"
+ api "androidx.slice:slice-view:1.1.0-alpha02"
api "androidx.compose.material3:material3:1.1.0-alpha01"
api "androidx.compose.material:material-icons-extended:$jetpack_compose_version"
api "androidx.compose.runtime:runtime-livedata:$jetpack_compose_version"
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSearchProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSearchProvider.kt
index 35b9c0f..3689e4e 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSearchProvider.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSearchProvider.kt
@@ -119,7 +119,7 @@
val entryRepository by spaEnvironment.entryRepository
val cursor = MatrixCursor(QueryEnum.SEARCH_IMMUTABLE_STATUS_DATA_QUERY.getColumns())
for (entry in entryRepository.getAllEntries()) {
- if (!entry.isAllowSearch || entry.mutableStatus) continue
+ if (!entry.isAllowSearch || entry.hasMutableStatus) continue
fetchStatusData(entry, cursor)
}
return cursor
@@ -129,7 +129,7 @@
val entryRepository by spaEnvironment.entryRepository
val cursor = MatrixCursor(QueryEnum.SEARCH_MUTABLE_STATUS_DATA_QUERY.getColumns())
for (entry in entryRepository.getAllEntries()) {
- if (!entry.isAllowSearch || !entry.mutableStatus) continue
+ if (!entry.isAllowSearch || !entry.hasMutableStatus) continue
fetchStatusData(entry, cursor)
}
return cursor
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSliceBroadcastReceiver.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSliceBroadcastReceiver.kt
new file mode 100644
index 0000000..8c038c8
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSliceBroadcastReceiver.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.settingslib.spa.framework
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
+
+class SpaSliceBroadcastReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ val sliceRepository = SpaEnvironmentFactory.instance.sliceDataRepository
+ val sliceUri = intent?.data ?: return
+ val sliceData = sliceRepository.getActiveSliceData(sliceUri) ?: return
+ sliceData.doAction()
+ }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSliceProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSliceProvider.kt
new file mode 100644
index 0000000..d800e50
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSliceProvider.kt
@@ -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.settingslib.spa.framework
+
+import android.net.Uri
+import android.util.Log
+import androidx.lifecycle.Observer
+import androidx.slice.Slice
+import androidx.slice.SliceProvider
+import com.android.settingslib.spa.framework.common.EntrySliceData
+import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+
+private const val TAG = "SpaSliceProvider"
+
+class SpaSliceProvider : SliceProvider(), Observer<Slice?> {
+ private fun getOrPutSliceData(sliceUri: Uri): EntrySliceData? {
+ if (!SpaEnvironmentFactory.isReady()) return null
+ return SpaEnvironmentFactory.instance.sliceDataRepository.getOrBuildSliceData(sliceUri)
+ }
+
+ override fun onBindSlice(sliceUri: Uri): Slice? {
+ if (context == null) return null
+ Log.d(TAG, "onBindSlice: $sliceUri")
+ return getOrPutSliceData(sliceUri)?.value
+ }
+
+ override fun onSlicePinned(sliceUri: Uri) {
+ Log.d(TAG, "onSlicePinned: $sliceUri")
+ super.onSlicePinned(sliceUri)
+ val sliceLiveData = getOrPutSliceData(sliceUri) ?: return
+ runBlocking {
+ withContext(Dispatchers.Main) {
+ sliceLiveData.observeForever(this@SpaSliceProvider)
+ }
+ }
+ }
+
+ override fun onSliceUnpinned(sliceUri: Uri) {
+ Log.d(TAG, "onSliceUnpinned: $sliceUri")
+ super.onSliceUnpinned(sliceUri)
+ val sliceLiveData = getOrPutSliceData(sliceUri) ?: return
+ runBlocking {
+ withContext(Dispatchers.Main) {
+ sliceLiveData.removeObserver(this@SpaSliceProvider)
+ }
+ }
+ }
+
+ override fun onChanged(slice: Slice?) {
+ val uri = slice?.uri ?: return
+ Log.d(TAG, "onChanged: $uri")
+ context?.contentResolver?.notifyChange(uri, null)
+ }
+
+ override fun onCreateSliceProvider(): Boolean {
+ Log.d(TAG, "onCreateSliceProvider")
+ return true
+ }
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntrySliceData.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntrySliceData.kt
new file mode 100644
index 0000000..fc551a8
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntrySliceData.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.settingslib.spa.framework.common
+
+import androidx.lifecycle.LiveData
+import androidx.slice.Slice
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+
+open class EntrySliceData : LiveData<Slice?>() {
+ private val asyncRunnerScope = CoroutineScope(Dispatchers.IO)
+ private var asyncRunnerJob: Job? = null
+ private var asyncActionJob: Job? = null
+ private var isActive = false
+
+ open suspend fun asyncRunner() {}
+
+ open suspend fun asyncAction() {}
+
+ override fun onActive() {
+ asyncRunnerJob?.cancel()
+ asyncRunnerJob = asyncRunnerScope.launch { asyncRunner() }
+ isActive = true
+ }
+
+ override fun onInactive() {
+ asyncRunnerJob?.cancel()
+ asyncRunnerJob = null
+ asyncActionJob?.cancel()
+ asyncActionJob = null
+ isActive = false
+ }
+
+ fun isActive(): Boolean {
+ return isActive
+ }
+
+ fun doAction() {
+ asyncActionJob?.cancel()
+ asyncActionJob = asyncRunnerScope.launch { asyncAction() }
+ }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt
index 224fe1d..9ee7f9e 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt
@@ -16,6 +16,7 @@
package com.android.settingslib.spa.framework.common
+import android.net.Uri
import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
@@ -39,6 +40,11 @@
val LocalEntryDataProvider =
compositionLocalOf<EntryData> { object : EntryData {} }
+typealias UiLayerRenderer = @Composable (arguments: Bundle?) -> Unit
+typealias StatusDataGetter = (arguments: Bundle?) -> EntryStatusData?
+typealias SearchDataGetter = (arguments: Bundle?) -> EntrySearchData?
+typealias SliceDataGetter = (sliceUri: Uri, arguments: Bundle?) -> EntrySliceData?
+
/**
* Defines data of a Settings entry.
*/
@@ -71,7 +77,10 @@
// Indicate whether the status of entry is mutable.
// If so, for instance, we'll reindex its status for search.
- val mutableStatus: Boolean = false,
+ val hasMutableStatus: Boolean = false,
+
+ // Indicate whether the entry has SliceProvider support.
+ val hasSliceSupport: Boolean = false,
/**
* ========================================
@@ -83,13 +92,19 @@
* API to get the status data of the entry, such as isDisabled / isSwitchOff.
* Returns null if this entry do NOT have any status.
*/
- private val statusDataImpl: (arguments: Bundle?) -> EntryStatusData? = { null },
+ private val statusDataImpl: StatusDataGetter = { null },
/**
* API to get Search indexing data for this entry, such as title / keyword.
* Returns null if this entry do NOT support search.
*/
- private val searchDataImpl: (arguments: Bundle?) -> EntrySearchData? = { null },
+ private val searchDataImpl: SearchDataGetter = { null },
+
+ /**
+ * API to get Slice data of this entry. The Slice data is implemented as a LiveData,
+ * and is associated with the Slice's lifecycle (pin / unpin) by the framework.
+ */
+ private val sliceDataImpl: SliceDataGetter = { _: Uri, _: Bundle? -> null },
/**
* API to Render UI of this entry directly. For now, we use it in the internal injection, to
@@ -97,7 +112,7 @@
* injected entry. In the long term, we may deprecate the @Composable Page() API in SPP, and
* use each entries' UI rendering function in the page instead.
*/
- private val uiLayoutImpl: (@Composable (arguments: Bundle?) -> Unit) = {},
+ private val uiLayoutImpl: UiLayerRenderer = {},
) {
fun containerPage(): SettingsPage {
// The Container page of the entry, which is the from-page or
@@ -121,6 +136,10 @@
return searchDataImpl(fullArgument(runtimeArguments))
}
+ fun getSliceData(sliceUri: Uri, runtimeArguments: Bundle? = null): EntrySliceData? {
+ return sliceDataImpl(sliceUri, fullArgument(runtimeArguments))
+ }
+
@Composable
fun UiLayout(runtimeArguments: Bundle? = null) {
CompositionLocalProvider(provideLocalEntryData()) {
@@ -152,12 +171,14 @@
// Attributes
private var isAllowSearch: Boolean = false
private var isSearchDataDynamic: Boolean = false
- private var mutableStatus: Boolean = false
+ private var hasMutableStatus: Boolean = false
+ private var hasSliceSupport: Boolean = false
// Functions
- private var statusDataFn: (arguments: Bundle?) -> EntryStatusData? = { null }
- private var searchDataFn: (arguments: Bundle?) -> EntrySearchData? = { null }
- private var uiLayoutFn: (@Composable (arguments: Bundle?) -> Unit) = { }
+ private var uiLayoutFn: UiLayerRenderer = { }
+ private var statusDataFn: StatusDataGetter = { null }
+ private var searchDataFn: SearchDataGetter = { null }
+ private var sliceDataFn: SliceDataGetter = { _: Uri, _: Bundle? -> null }
fun build(): SettingsEntry {
return SettingsEntry(
@@ -173,11 +194,13 @@
// attributes
isAllowSearch = isAllowSearch,
isSearchDataDynamic = isSearchDataDynamic,
- mutableStatus = mutableStatus,
+ hasMutableStatus = hasMutableStatus,
+ hasSliceSupport = hasSliceSupport,
// functions
statusDataImpl = statusDataFn,
searchDataImpl = searchDataFn,
+ sliceDataImpl = sliceDataFn,
uiLayoutImpl = uiLayoutFn,
)
}
@@ -207,7 +230,7 @@
}
fun setHasMutableStatus(hasMutableStatus: Boolean): SettingsEntryBuilder {
- this.mutableStatus = hasMutableStatus
+ this.hasMutableStatus = hasMutableStatus
return this
}
@@ -221,17 +244,23 @@
return this
}
- fun setStatusDataFn(fn: (arguments: Bundle?) -> EntryStatusData?): SettingsEntryBuilder {
+ fun setStatusDataFn(fn: StatusDataGetter): SettingsEntryBuilder {
this.statusDataFn = fn
return this
}
- fun setSearchDataFn(fn: (arguments: Bundle?) -> EntrySearchData?): SettingsEntryBuilder {
+ fun setSearchDataFn(fn: SearchDataGetter): SettingsEntryBuilder {
this.searchDataFn = fn
return this
}
- fun setUiLayoutFn(fn: @Composable (arguments: Bundle?) -> Unit): SettingsEntryBuilder {
+ fun setSliceDataFn(fn: SliceDataGetter): SettingsEntryBuilder {
+ this.sliceDataFn = fn
+ this.hasSliceSupport = true
+ return this
+ }
+
+ fun setUiLayoutFn(fn: UiLayerRenderer): SettingsEntryBuilder {
this.uiLayoutFn = fn
return this
}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt
index bb287d1..a372bbd 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt
@@ -113,6 +113,12 @@
)
}
+ fun createBrowseIntent(entryId: String? = null): Intent? {
+ val context = SpaEnvironmentFactory.instance.appContext
+ val browseActivityClass = SpaEnvironmentFactory.instance.browseActivityClass
+ return createBrowseIntent(context, browseActivityClass, entryId)
+ }
+
fun createBrowseIntent(
context: Context?,
browseActivityClass: Class<out Activity>?,
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt
index 151b50cd..60599d4 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt
@@ -30,14 +30,14 @@
val name: String
/** The display name of this page provider, for better readability. */
- val displayName: String?
- get() = null
+ val displayName: String
+ get() = name
/** The page parameters, default is no parameters. */
val parameter: List<NamedNavArgument>
get() = emptyList()
- fun getTitle(arguments: Bundle?): String = displayName ?: name
+ fun getTitle(arguments: Bundle?): String = displayName
fun buildEntry(arguments: Bundle?): List<SettingsEntry> = emptyList()
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt
index a9cb041..9bb06f0 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt
@@ -17,10 +17,12 @@
package com.android.settingslib.spa.framework.common
import android.app.Activity
+import android.content.BroadcastReceiver
import android.content.Context
import android.util.Log
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
+import com.android.settingslib.spa.slice.SettingsSliceDataRepository
private const val TAG = "SpaEnvironment"
@@ -46,6 +48,10 @@
Log.d(TAG, "resetForPreview")
}
+ fun isReady(): Boolean {
+ return spaEnvironment != null
+ }
+
val instance: SpaEnvironment
get() {
if (spaEnvironment == null)
@@ -59,13 +65,15 @@
val entryRepository = lazy { SettingsEntryRepository(pageProviderRepository.value) }
+ val sliceDataRepository = SettingsSliceDataRepository()
+
// In Robolectric test, applicationContext is not available. Use context as fallback.
val appContext: Context = context.applicationContext ?: context
open val browseActivityClass: Class<out Activity>? = null
-
+ open val sliceBroadcastReceiverClass: Class<out BroadcastReceiver>? = null
open val searchProviderAuthorities: String? = null
-
+ open val sliceProviderAuthorities: String? = null
open val logger: SpaLogger = object : SpaLogger {}
// TODO: add other environment setup here.
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugActivity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugActivity.kt
index 26491d5..760064a 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugActivity.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugActivity.kt
@@ -16,6 +16,7 @@
package com.android.settingslib.spa.framework.debug
+import android.net.Uri
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
@@ -38,6 +39,8 @@
import com.android.settingslib.spa.framework.compose.navigator
import com.android.settingslib.spa.framework.compose.toState
import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.slice.appendSliceParams
+import com.android.settingslib.spa.slice.presenter.SliceDemo
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spa.widget.scaffold.HomeScaffold
@@ -47,6 +50,7 @@
private const val ROUTE_ROOT = "root"
private const val ROUTE_All_PAGES = "pages"
private const val ROUTE_All_ENTRIES = "entries"
+private const val ROUTE_All_SLICES = "slices"
private const val ROUTE_PAGE = "page"
private const val ROUTE_ENTRY = "entry"
private const val PARAM_NAME_PAGE_ID = "pid"
@@ -81,6 +85,7 @@
composable(route = ROUTE_ROOT) { RootPage() }
composable(route = ROUTE_All_PAGES) { AllPages() }
composable(route = ROUTE_All_ENTRIES) { AllEntries() }
+ composable(route = ROUTE_All_SLICES) { AllSlices() }
composable(
route = "$ROUTE_PAGE/{$PARAM_NAME_PAGE_ID}",
arguments = listOf(
@@ -102,6 +107,8 @@
val entryRepository by spaEnvironment.entryRepository
val allPageWithEntry = remember { entryRepository.getAllPageWithEntry() }
val allEntry = remember { entryRepository.getAllEntries() }
+ val allSliceEntry =
+ remember { entryRepository.getAllEntries().filter { it.hasSliceSupport } }
HomeScaffold(title = "Settings Debug") {
Preference(object : PreferenceModel {
override val title = "List All Pages (${allPageWithEntry.size})"
@@ -111,6 +118,10 @@
override val title = "List All Entries (${allEntry.size})"
override val onClick = navigator(route = ROUTE_All_ENTRIES)
})
+ Preference(object : PreferenceModel {
+ override val title = "List All Slices (${allSliceEntry.size})"
+ override val onClick = navigator(route = ROUTE_All_SLICES)
+ })
}
}
@@ -140,6 +151,19 @@
}
@Composable
+ fun AllSlices() {
+ val entryRepository by spaEnvironment.entryRepository
+ val authority = spaEnvironment.sliceProviderAuthorities
+ val allSliceEntry =
+ remember { entryRepository.getAllEntries().filter { it.hasSliceSupport } }
+ RegularScaffold(title = "All Slices (${allSliceEntry.size})") {
+ for (entry in allSliceEntry) {
+ SliceDemo(sliceUri = entry.createSliceUri(authority))
+ }
+ }
+ }
+
+ @Composable
fun OnePage(arguments: Bundle?) {
val context = LocalContext.current
val entryRepository by spaEnvironment.entryRepository
@@ -221,6 +245,18 @@
}
}
+private fun SettingsEntry.createSliceUri(
+ authority: String?,
+ runtimeArguments: Bundle? = null
+): Uri {
+ if (authority == null) return Uri.EMPTY
+ return Uri.Builder().scheme("content").authority(authority).appendSliceParams(
+ route = this.containerPage().buildRoute(),
+ entryId = this.id,
+ runtimeArguments = runtimeArguments,
+ ).build()
+}
+
/**
* A blank activity without any page.
*/
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SettingsSliceDataRepository.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SettingsSliceDataRepository.kt
new file mode 100644
index 0000000..d8d2378
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SettingsSliceDataRepository.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.spa.slice
+
+import android.net.Uri
+import android.util.Log
+import com.android.settingslib.spa.framework.common.EntrySliceData
+import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
+
+private const val TAG = "SliceDataRepository"
+
+class SettingsSliceDataRepository {
+ // The map of slice uri to its EntrySliceData, a.k.a. LiveData<Slice?>
+ private val sliceDataMap: MutableMap<String, EntrySliceData> = mutableMapOf()
+
+ // Note: mark this function synchronized, so that we can get the same livedata during the
+ // whole lifecycle of a Slice.
+ @Synchronized
+ fun getOrBuildSliceData(sliceUri: Uri): EntrySliceData? {
+ val sliceString = sliceUri.getSliceId() ?: return null
+ return sliceDataMap[sliceString] ?: buildLiveDataImpl(sliceUri)?.let {
+ sliceDataMap[sliceString] = it
+ it
+ }
+ }
+
+ fun getActiveSliceData(sliceUri: Uri): EntrySliceData? {
+ val sliceString = sliceUri.getSliceId() ?: return null
+ val sliceData = sliceDataMap[sliceString] ?: return null
+ return if (sliceData.isActive()) sliceData else null
+ }
+
+ private fun buildLiveDataImpl(sliceUri: Uri): EntrySliceData? {
+ Log.d(TAG, "buildLiveData: $sliceUri")
+ if (!SpaEnvironmentFactory.isReady()) return null
+
+ val entryRepository by SpaEnvironmentFactory.instance.entryRepository
+ val entryId = sliceUri.getEntryId() ?: return null
+ val entry = entryRepository.getEntry(entryId) ?: return null
+ if (!entry.hasSliceSupport) return null
+
+ val arguments = sliceUri.getRuntimeArguments()
+ return entry.getSliceData(runtimeArguments = arguments, sliceUri = sliceUri)
+ }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SliceUtil.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SliceUtil.kt
new file mode 100644
index 0000000..ff143ed
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SliceUtil.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.slice
+
+import android.app.Activity
+import android.app.PendingIntent
+import android.content.BroadcastReceiver
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import com.android.settingslib.spa.framework.BrowseActivity.Companion.KEY_DESTINATION
+import com.android.settingslib.spa.framework.BrowseActivity.Companion.KEY_HIGHLIGHT_ENTRY
+import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
+
+// Defines SliceUri, which contains special query parameters:
+// -- KEY_DESTINATION: The route that this slice is navigated to.
+// -- KEY_HIGHLIGHT_ENTRY: The entry id of this slice
+// Other parameters can considered as runtime parameters.
+// Use {entryId, runtimeParams} as the unique Id of this Slice.
+typealias SliceUri = Uri
+
+val RESERVED_KEYS = listOf(
+ KEY_DESTINATION,
+ KEY_HIGHLIGHT_ENTRY
+)
+
+fun SliceUri.getEntryId(): String? {
+ return getQueryParameter(KEY_HIGHLIGHT_ENTRY)
+}
+
+fun SliceUri.getDestination(): String? {
+ return getQueryParameter(KEY_DESTINATION)
+}
+
+fun SliceUri.getRuntimeArguments(): Bundle {
+ val params = Bundle()
+ for (queryName in queryParameterNames) {
+ if (RESERVED_KEYS.contains(queryName)) continue
+ params.putString(queryName, getQueryParameter(queryName))
+ }
+ return params
+}
+
+fun SliceUri.getSliceId(): String? {
+ val entryId = getEntryId() ?: return null
+ val params = getRuntimeArguments()
+ return "${entryId}_$params"
+}
+
+fun Uri.Builder.appendSliceParams(
+ route: String? = null,
+ entryId: String? = null,
+ runtimeArguments: Bundle? = null
+): Uri.Builder {
+ if (route != null) appendQueryParameter(KEY_DESTINATION, route)
+ if (entryId != null) appendQueryParameter(KEY_HIGHLIGHT_ENTRY, entryId)
+ if (runtimeArguments != null) {
+ for (key in runtimeArguments.keySet()) {
+ appendQueryParameter(key, runtimeArguments.getString(key, ""))
+ }
+ }
+ return this
+}
+
+fun SliceUri.createBroadcastPendingIntent(): PendingIntent? {
+ val context = SpaEnvironmentFactory.instance.appContext
+ val sliceBroadcastClass =
+ SpaEnvironmentFactory.instance.sliceBroadcastReceiverClass ?: return null
+ val entryId = getEntryId() ?: return null
+ return createBroadcastPendingIntent(context, sliceBroadcastClass, entryId)
+}
+
+fun SliceUri.createBrowsePendingIntent(): PendingIntent? {
+ val context = SpaEnvironmentFactory.instance.appContext
+ val browseActivityClass = SpaEnvironmentFactory.instance.browseActivityClass ?: return null
+ val destination = getDestination() ?: return null
+ val entryId = getEntryId()
+ return createBrowsePendingIntent(context, browseActivityClass, destination, entryId)
+}
+
+fun Intent.createBrowsePendingIntent(): PendingIntent? {
+ val context = SpaEnvironmentFactory.instance.appContext
+ val browseActivityClass = SpaEnvironmentFactory.instance.browseActivityClass ?: return null
+ val destination = getStringExtra(KEY_DESTINATION) ?: return null
+ val entryId = getStringExtra(KEY_HIGHLIGHT_ENTRY)
+ return createBrowsePendingIntent(context, browseActivityClass, destination, entryId)
+}
+
+private fun createBrowsePendingIntent(
+ context: Context,
+ browseActivityClass: Class<out Activity>,
+ destination: String,
+ entryId: String?
+): PendingIntent {
+ val intent = Intent().setComponent(ComponentName(context, browseActivityClass))
+ .apply {
+ // Set both extra and data (which is a Uri) in Slice Intent:
+ // 1) extra is used in SPA navigation framework
+ // 2) data is used in Slice framework
+ putExtra(KEY_DESTINATION, destination)
+ if (entryId != null) {
+ putExtra(KEY_HIGHLIGHT_ENTRY, entryId)
+ }
+ data = Uri.Builder().appendSliceParams(destination, entryId).build()
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ }
+
+ return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
+}
+
+private fun createBroadcastPendingIntent(
+ context: Context,
+ sliceBroadcastClass: Class<out BroadcastReceiver>,
+ entryId: String
+): PendingIntent {
+ val intent = Intent().setComponent(ComponentName(context, sliceBroadcastClass))
+ .apply { data = Uri.Builder().appendSliceParams(entryId = entryId).build() }
+ return PendingIntent.getBroadcast(
+ context, 0 /* requestCode */, intent,
+ PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE
+ )
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/presenter/Demo.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/presenter/Demo.kt
new file mode 100644
index 0000000..cff1c0c
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/presenter/Demo.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.spa.slice.presenter
+
+import android.net.Uri
+import androidx.compose.material3.Divider
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.slice.widget.SliceLiveData
+import androidx.slice.widget.SliceView
+
+@Composable
+fun SliceDemo(sliceUri: Uri) {
+ val context = LocalContext.current
+ val lifecycleOwner = LocalLifecycleOwner.current
+ val sliceData = remember {
+ SliceLiveData.fromUri(context, sliceUri)
+ }
+
+ Divider()
+ AndroidView(
+ factory = { localContext ->
+ val view = SliceView(localContext)
+ view.setShowTitleItems(true)
+ view.isScrollable = false
+ view
+ },
+ update = { view -> sliceData.observe(lifecycleOwner, view) }
+ )
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/provider/Demo.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/provider/Demo.kt
new file mode 100644
index 0000000..b65b91f
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/provider/Demo.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.spa.slice.provider
+
+import android.app.PendingIntent
+import android.content.Context
+import android.net.Uri
+import androidx.core.graphics.drawable.IconCompat
+import androidx.slice.Slice
+import androidx.slice.SliceManager
+import androidx.slice.builders.ListBuilder
+import androidx.slice.builders.SliceAction
+import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
+import com.android.settingslib.spa.slice.createBroadcastPendingIntent
+import com.android.settingslib.spa.slice.createBrowsePendingIntent
+
+fun createDemoBrowseSlice(sliceUri: Uri, title: String, summary: String): Slice? {
+ val intent = sliceUri.createBrowsePendingIntent() ?: return null
+ return createDemoSlice(sliceUri, title, summary, intent)
+}
+
+fun createDemoActionSlice(sliceUri: Uri, title: String, summary: String): Slice? {
+ val intent = sliceUri.createBroadcastPendingIntent() ?: return null
+ return createDemoSlice(sliceUri, title, summary, intent)
+}
+
+fun createDemoSlice(sliceUri: Uri, title: String, summary: String, intent: PendingIntent): Slice? {
+ val context = SpaEnvironmentFactory.instance.appContext
+ if (!SliceManager.getInstance(context).pinnedSlices.contains(sliceUri)) return null
+ return ListBuilder(context, sliceUri, ListBuilder.INFINITY)
+ .addRow(ListBuilder.RowBuilder().apply {
+ setPrimaryAction(createSliceAction(context, intent))
+ setTitle(title)
+ setSubtitle(summary)
+ }).build()
+}
+
+private fun createSliceAction(context: Context, intent: PendingIntent): SliceAction {
+ return SliceAction.create(
+ intent,
+ IconCompat.createWithResource(
+ context,
+ com.google.android.material.R.drawable.navigation_empty_icon
+ ),
+ ListBuilder.ICON_IMAGE,
+ "Enter app"
+ )
+}
diff --git a/packages/SettingsLib/Spa/tests/build.gradle b/packages/SettingsLib/Spa/tests/build.gradle
index 529a201..2d501fc 100644
--- a/packages/SettingsLib/Spa/tests/build.gradle
+++ b/packages/SettingsLib/Spa/tests/build.gradle
@@ -21,11 +21,12 @@
android {
namespace 'com.android.settingslib.spa.tests'
- compileSdk spa_target_sdk
+ compileSdk TARGET_SDK
+ buildToolsVersion = BUILD_TOOLS_VERSION
defaultConfig {
- minSdk spa_min_sdk
- targetSdk spa_target_sdk
+ minSdk MIN_SDK
+ targetSdk TARGET_SDK
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -60,7 +61,6 @@
dependencies {
androidTestImplementation project(":spa")
androidTestImplementation project(":testutils")
- androidTestImplementation "androidx.test.ext:junit-ktx:1.1.3"
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$jetpack_compose_version"
androidTestImplementation "com.google.truth:truth:1.1.3"
androidTestImplementation "org.mockito:mockito-android:3.4.6"
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryTest.kt
index 31d2ae4..2017d53 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryTest.kt
@@ -63,7 +63,7 @@
assertThat(entry.toPage).isNull()
assertThat(entry.isAllowSearch).isFalse()
assertThat(entry.isSearchDataDynamic).isFalse()
- assertThat(entry.mutableStatus).isFalse()
+ assertThat(entry.hasMutableStatus).isFalse()
}
@Test
@@ -133,7 +133,7 @@
assertThat(entry.toPage).isNull()
assertThat(entry.isAllowSearch).isTrue()
assertThat(entry.isSearchDataDynamic).isFalse()
- assertThat(entry.mutableStatus).isTrue()
+ assertThat(entry.hasMutableStatus).isTrue()
}
@Test
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageTest.kt
index 539e56b..7097a5d 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageTest.kt
@@ -118,6 +118,7 @@
page.enterPage()
page.leavePage()
page.enterPage()
+ assertThat(page.createBrowseIntent()).isNotNull()
assertThat(spaLogger.getEventCount(page.id, LogEvent.PAGE_ENTER, LogCategory.FRAMEWORK))
.isEqualTo(2)
assertThat(spaLogger.getEventCount(page.id, LogEvent.PAGE_LEAVE, LogCategory.FRAMEWORK))
diff --git a/packages/SettingsLib/Spa/testutils/build.gradle b/packages/SettingsLib/Spa/testutils/build.gradle
index 71d7d8a..cbfbb9c 100644
--- a/packages/SettingsLib/Spa/testutils/build.gradle
+++ b/packages/SettingsLib/Spa/testutils/build.gradle
@@ -20,11 +20,12 @@
}
android {
- compileSdk spa_target_sdk
+ compileSdk TARGET_SDK
+ buildToolsVersion = BUILD_TOOLS_VERSION
defaultConfig {
- minSdk spa_min_sdk
- targetSdk spa_target_sdk
+ minSdk MIN_SDK
+ targetSdk TARGET_SDK
}
sourceSets {
diff --git a/packages/SettingsLib/Spa/testutils/src/SpaTest.kt b/packages/SettingsLib/Spa/testutils/src/SpaTest.kt
new file mode 100644
index 0000000..a397bb4
--- /dev/null
+++ b/packages/SettingsLib/Spa/testutils/src/SpaTest.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.settingslib.spa.testutils
+
+import java.util.concurrent.TimeoutException
+
+/**
+ * Blocks until the given condition is satisfied.
+ */
+fun waitUntil(timeoutMillis: Long = 1000, condition: () -> Boolean) {
+ val startTime = System.currentTimeMillis()
+ while (!condition()) {
+ // Let Android run measure, draw and in general any other async operations.
+ Thread.sleep(10)
+ if (System.currentTimeMillis() - startTime > timeoutMillis) {
+ throw TimeoutException("Condition still not satisfied after $timeoutMillis ms")
+ }
+ }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUser.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUser.kt
index a7de4ce..b2ea4a0 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUser.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUser.kt
@@ -23,7 +23,6 @@
import android.os.UserHandle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.Lifecycle
@@ -34,24 +33,25 @@
*/
@Composable
fun DisposableBroadcastReceiverAsUser(
- userId: Int,
intentFilter: IntentFilter,
+ userHandle: UserHandle,
+ onStart: () -> Unit = {},
onReceive: (Intent) -> Unit,
) {
- val broadcastReceiver = remember {
- object : BroadcastReceiver() {
+ val context = LocalContext.current
+ val lifecycleOwner = LocalLifecycleOwner.current
+ DisposableEffect(lifecycleOwner) {
+ val broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
onReceive(intent)
}
}
- }
- val context = LocalContext.current
- val lifecycleOwner = LocalLifecycleOwner.current
- DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_START) {
context.registerReceiverAsUser(
- broadcastReceiver, UserHandle.of(userId), intentFilter, null, null)
+ broadcastReceiver, userHandle, intentFilter, null, null
+ )
+ onStart()
} else if (event == Lifecycle.Event.ON_STOP) {
context.unregisterReceiver(broadcastReceiver)
}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt
index ee89003..487dbcb 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt
@@ -21,13 +21,10 @@
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.map
/**
* The config used to load the App List.
@@ -40,36 +37,42 @@
/**
* The repository to load the App List data.
*/
-internal class AppListRepository(context: Context) {
+internal interface AppListRepository {
+ /** Loads the list of [ApplicationInfo]. */
+ suspend fun loadApps(config: AppListConfig): List<ApplicationInfo>
+
+ /** Gets the flow of predicate that could used to filter system app. */
+ fun showSystemPredicate(
+ userIdFlow: Flow<Int>,
+ showSystemFlow: Flow<Boolean>,
+ ): Flow<(app: ApplicationInfo) -> Boolean>
+}
+
+
+internal class AppListRepositoryImpl(context: Context) : AppListRepository {
private val packageManager = context.packageManager
- fun loadApps(configFlow: Flow<AppListConfig>): Flow<List<ApplicationInfo>> = configFlow
- .map { loadApps(it) }
- .flowOn(Dispatchers.Default)
+ override suspend fun loadApps(config: AppListConfig): List<ApplicationInfo> = coroutineScope {
+ val hiddenSystemModulesDeferred = async {
+ packageManager.getInstalledModules(0)
+ .filter { it.isHidden }
+ .map { it.packageName }
+ .toSet()
+ }
+ val flags = PackageManager.ApplicationInfoFlags.of(
+ (PackageManager.MATCH_DISABLED_COMPONENTS or
+ PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS).toLong()
+ )
+ val installedApplicationsAsUser =
+ packageManager.getInstalledApplicationsAsUser(flags, config.userId)
- private suspend fun loadApps(config: AppListConfig): List<ApplicationInfo> {
- return coroutineScope {
- val hiddenSystemModulesDeferred = async {
- packageManager.getInstalledModules(0)
- .filter { it.isHidden }
- .map { it.packageName }
- .toSet()
- }
- val flags = PackageManager.ApplicationInfoFlags.of(
- (PackageManager.MATCH_DISABLED_COMPONENTS or
- PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS).toLong()
- )
- val installedApplicationsAsUser =
- packageManager.getInstalledApplicationsAsUser(flags, config.userId)
-
- val hiddenSystemModules = hiddenSystemModulesDeferred.await()
- installedApplicationsAsUser.filter { app ->
- app.isInAppList(config.showInstantApps, hiddenSystemModules)
- }
+ val hiddenSystemModules = hiddenSystemModulesDeferred.await()
+ installedApplicationsAsUser.filter { app ->
+ app.isInAppList(config.showInstantApps, hiddenSystemModules)
}
}
- fun showSystemPredicate(
+ override fun showSystemPredicate(
userIdFlow: Flow<Int>,
showSystemFlow: Flow<Boolean>,
): Flow<(app: ApplicationInfo) -> Boolean> =
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListViewModel.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListViewModel.kt
index 1e487da..650b278 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListViewModel.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListViewModel.kt
@@ -17,6 +17,7 @@
package com.android.settingslib.spaprivileged.model.app
import android.app.Application
+import android.content.Context
import android.content.pm.ApplicationInfo
import android.icu.text.Collator
import androidx.lifecycle.AndroidViewModel
@@ -27,12 +28,16 @@
import java.util.concurrent.ConcurrentHashMap
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
internal data class AppListData<T : AppRecord>(
@@ -43,9 +48,15 @@
AppListData(appEntries.filter(predicate), option)
}
-@OptIn(ExperimentalCoroutinesApi::class)
internal class AppListViewModel<T : AppRecord>(
application: Application,
+) : AppListViewModelImpl<T>(application)
+
+@OptIn(ExperimentalCoroutinesApi::class)
+internal open class AppListViewModelImpl<T : AppRecord>(
+ application: Application,
+ appListRepositoryFactory: (Context) -> AppListRepository = ::AppListRepositoryImpl,
+ appRepositoryFactory: (Context) -> AppRepository = ::AppRepositoryImpl,
) : AndroidViewModel(application) {
val appListConfig = StateFlowBridge<AppListConfig>()
val listModel = StateFlowBridge<AppListModel<T>>()
@@ -53,16 +64,18 @@
val option = StateFlowBridge<Int>()
val searchQuery = StateFlowBridge<String>()
- private val appListRepository = AppListRepository(application)
- private val appRepository = AppRepositoryImpl(application)
+ private val appListRepository = appListRepositoryFactory(application)
+ private val appRepository = appRepositoryFactory(application)
private val collator = Collator.getInstance().freeze()
private val labelMap = ConcurrentHashMap<String, String>()
- private val scope = viewModelScope + Dispatchers.Default
+ private val scope = viewModelScope + Dispatchers.IO
private val userIdFlow = appListConfig.flow.map { it.userId }
+ private val appsStateFlow = MutableStateFlow<List<ApplicationInfo>?>(null)
+
private val recordListFlow = listModel.flow
- .flatMapLatest { it.transform(userIdFlow, appListRepository.loadApps(appListConfig.flow)) }
+ .flatMapLatest { it.transform(userIdFlow, appsStateFlow.filterNotNull()) }
.shareIn(scope = scope, started = SharingStarted.Eagerly, replay = 1)
private val systemFilteredFlow =
@@ -83,6 +96,12 @@
scheduleOnFirstLoaded()
}
+ fun reloadApps() {
+ viewModelScope.launch {
+ appsStateFlow.value = appListRepository.loadApps(appListConfig.flow.first())
+ }
+ }
+
private fun filterAndSort(option: Int) = listModel.flow.flatMapLatest { listModel ->
listModel.filter(userIdFlow, option, systemFilteredFlow)
.asyncMapItem { record ->
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppRepository.kt
index 34f12af..90710db 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppRepository.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppRepository.kt
@@ -22,8 +22,10 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.produceState
+import androidx.compose.ui.res.stringResource
import com.android.settingslib.Utils
import com.android.settingslib.spa.framework.compose.rememberContext
+import com.android.settingslib.spaprivileged.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -34,7 +36,12 @@
fun loadLabel(app: ApplicationInfo): String
@Composable
- fun produceLabel(app: ApplicationInfo): State<String>
+ fun produceLabel(app: ApplicationInfo) =
+ produceState(initialValue = stringResource(R.string.summary_placeholder), app) {
+ withContext(Dispatchers.IO) {
+ value = loadLabel(app)
+ }
+ }
@Composable
fun produceIcon(app: ApplicationInfo): State<Drawable?>
@@ -46,13 +53,6 @@
override fun loadLabel(app: ApplicationInfo): String = app.loadLabel(packageManager).toString()
@Composable
- override fun produceLabel(app: ApplicationInfo) = produceState(initialValue = "", app) {
- withContext(Dispatchers.Default) {
- value = app.loadLabel(packageManager).toString()
- }
- }
-
- @Composable
override fun produceIcon(app: ApplicationInfo) =
produceState<Drawable?>(initialValue = null, app) {
withContext(Dispatchers.Default) {
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt
index 9d6b311..7f5fe9f 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt
@@ -16,6 +16,9 @@
package com.android.settingslib.spaprivileged.template.app
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.UserHandle
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
@@ -33,6 +36,7 @@
import com.android.settingslib.spa.framework.compose.toState
import com.android.settingslib.spa.widget.ui.PlaceholderTitle
import com.android.settingslib.spaprivileged.R
+import com.android.settingslib.spaprivileged.framework.compose.DisposableBroadcastReceiverAsUser
import com.android.settingslib.spaprivileged.model.app.AppListConfig
import com.android.settingslib.spaprivileged.model.app.AppListData
import com.android.settingslib.spaprivileged.model.app.AppListModel
@@ -120,5 +124,15 @@
viewModel.option.Sync(state.option)
viewModel.searchQuery.Sync(state.searchQuery)
- return viewModel.appListDataFlow.collectAsState(null, Dispatchers.Default)
+ DisposableBroadcastReceiverAsUser(
+ intentFilter = IntentFilter(Intent.ACTION_PACKAGE_ADDED).apply {
+ addAction(Intent.ACTION_PACKAGE_REMOVED)
+ addAction(Intent.ACTION_PACKAGE_CHANGED)
+ addDataScheme("package")
+ },
+ userHandle = UserHandle.of(config.userId),
+ onStart = { viewModel.reloadApps() },
+ ) { viewModel.reloadApps() }
+
+ return viewModel.appListDataFlow.collectAsState(null, Dispatchers.IO)
}
diff --git a/packages/SettingsLib/SpaPrivileged/tests/Android.bp b/packages/SettingsLib/SpaPrivileged/tests/Android.bp
index 5afe21e..12955c8 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/Android.bp
+++ b/packages/SettingsLib/SpaPrivileged/tests/Android.bp
@@ -31,6 +31,7 @@
],
static_libs: [
+ "SpaLibTestUtils",
"androidx.compose.ui_ui-test-junit4",
"androidx.compose.ui_ui-test-manifest",
"androidx.test.ext.junit",
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt
index 5d5a24e..bc6925b 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt
@@ -24,9 +24,6 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.toList
-import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
@@ -36,11 +33,9 @@
import org.mockito.Mockito.any
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.eq
-import org.mockito.Mockito.`when` as whenever
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
-
-private const val USER_ID = 0
+import org.mockito.Mockito.`when` as whenever
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
@@ -80,36 +75,28 @@
packageManager.queryIntentActivitiesAsUser(any(), any<ResolveInfoFlags>(), eq(USER_ID))
).thenReturn(emptyList())
- repository = AppListRepository(context)
+ repository = AppListRepositoryImpl(context)
}
@Test
fun notShowInstantApps() = runTest {
val appListConfig = AppListConfig(userId = USER_ID, showInstantApps = false)
- val appListFlow = repository.loadApps(flowOf(appListConfig))
+ val appListFlow = repository.loadApps(appListConfig)
- launch {
- val flowValues = mutableListOf<List<ApplicationInfo>>()
- appListFlow.toList(flowValues)
- assertThat(flowValues).hasSize(1)
-
- assertThat(flowValues[0]).containsExactly(normalApp)
- }
+ assertThat(appListFlow).containsExactly(normalApp)
}
@Test
fun showInstantApps() = runTest {
val appListConfig = AppListConfig(userId = USER_ID, showInstantApps = true)
- val appListFlow = repository.loadApps(flowOf(appListConfig))
+ val appListFlow = repository.loadApps(appListConfig)
- launch {
- val flowValues = mutableListOf<List<ApplicationInfo>>()
- appListFlow.toList(flowValues)
- assertThat(flowValues).hasSize(1)
+ assertThat(appListFlow).containsExactly(normalApp, instantApp)
+ }
- assertThat(flowValues[0]).containsExactly(normalApp, instantApp)
- }
+ private companion object {
+ const val USER_ID = 0
}
}
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListViewModelTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListViewModelTest.kt
new file mode 100644
index 0000000..b570815
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListViewModelTest.kt
@@ -0,0 +1,134 @@
+/*
+ * 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.Application
+import android.content.pm.ApplicationInfo
+import androidx.compose.runtime.Composable
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.framework.compose.stateOf
+import com.android.settingslib.spa.framework.util.asyncMapItem
+import com.android.settingslib.spa.testutils.waitUntil
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+class AppListViewModelTest {
+ @JvmField
+ @Rule
+ val mockito: MockitoRule = MockitoJUnit.rule()
+
+ @Mock
+ private lateinit var application: Application
+
+ private val listModel = TestAppListModel()
+
+ private fun createViewModel(): AppListViewModelImpl<TestAppRecord> {
+ val viewModel = AppListViewModelImpl<TestAppRecord>(
+ application = application,
+ appListRepositoryFactory = { FakeAppListRepository },
+ appRepositoryFactory = { FakeAppRepository },
+ )
+ viewModel.appListConfig.setIfAbsent(CONFIG)
+ viewModel.listModel.setIfAbsent(listModel)
+ viewModel.showSystem.setIfAbsent(false)
+ viewModel.option.setIfAbsent(0)
+ viewModel.searchQuery.setIfAbsent("")
+ viewModel.reloadApps()
+ return viewModel
+ }
+
+ @Test
+ fun appListDataFlow() = runTest {
+ val viewModel = createViewModel()
+
+ val (appEntries, option) = viewModel.appListDataFlow.first()
+
+ assertThat(appEntries).hasSize(1)
+ assertThat(appEntries[0].record.app).isSameInstanceAs(APP)
+ assertThat(appEntries[0].label).isEqualTo(LABEL)
+ assertThat(option).isEqualTo(0)
+ }
+
+ @Test
+ fun onFirstLoaded_calledWhenLoaded() = runTest {
+ val viewModel = createViewModel()
+
+ viewModel.appListDataFlow.first()
+
+ waitUntil { listModel.onFirstLoadedCalled }
+ }
+
+ private object FakeAppListRepository : AppListRepository {
+ override suspend fun loadApps(config: AppListConfig) = listOf(APP)
+
+ override fun showSystemPredicate(
+ userIdFlow: Flow<Int>,
+ showSystemFlow: Flow<Boolean>,
+ ): Flow<(app: ApplicationInfo) -> Boolean> = flowOf { true }
+ }
+
+ private object FakeAppRepository : AppRepository {
+ override fun loadLabel(app: ApplicationInfo) = LABEL
+
+ @Composable
+ override fun produceIcon(app: ApplicationInfo) = stateOf(null)
+ }
+
+ private companion object {
+ const val USER_ID = 0
+ const val PACKAGE_NAME = "package.name"
+ const val LABEL = "Label"
+ val CONFIG = AppListConfig(userId = USER_ID, showInstantApps = false)
+ val APP = ApplicationInfo().apply {
+ packageName = PACKAGE_NAME
+ }
+ }
+}
+
+private data class TestAppRecord(override val app: ApplicationInfo) : AppRecord
+
+private class TestAppListModel : AppListModel<TestAppRecord> {
+ var onFirstLoadedCalled = false
+
+ override fun transform(userIdFlow: Flow<Int>, appListFlow: Flow<List<ApplicationInfo>>) =
+ appListFlow.asyncMapItem { TestAppRecord(it) }
+
+ @Composable
+ override fun getSummary(option: Int, record: TestAppRecord) = null
+
+ override fun filter(
+ userIdFlow: Flow<Int>,
+ option: Int,
+ recordListFlow: Flow<List<TestAppRecord>>,
+ ) = recordListFlow
+
+ override suspend fun onFirstLoaded(recordList: List<TestAppRecord>) {
+ onFirstLoadedCalled = true
+ }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppStorageSizeTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppStorageSizeTest.kt
index cec6d7d..b3638c2 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppStorageSizeTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppStorageSizeTest.kt
@@ -28,7 +28,6 @@
import com.android.settingslib.spa.framework.compose.stateOf
import com.android.settingslib.spaprivileged.framework.common.storageStatsManager
import com.android.settingslib.spaprivileged.model.app.userHandle
-import com.google.common.truth.Truth.assertThat
import java.util.UUID
import org.junit.Before
import org.junit.Rule
@@ -77,7 +76,7 @@
}
}
- assertThat(storageSize.value).isEqualTo("123 B")
+ composeTestRule.waitUntil { storageSize.value == "123 B" }
}
companion object {
diff --git a/packages/SettingsLib/src/com/android/settingslib/graph/ThemedBatteryDrawable.kt b/packages/SettingsLib/src/com/android/settingslib/graph/ThemedBatteryDrawable.kt
index 5fa04f9..faea5b2 100644
--- a/packages/SettingsLib/src/com/android/settingslib/graph/ThemedBatteryDrawable.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/graph/ThemedBatteryDrawable.kt
@@ -412,14 +412,13 @@
}
companion object {
- private const val TAG = "ThemedBatteryDrawable"
- private const val WIDTH = 12f
- private const val HEIGHT = 20f
+ const val WIDTH = 12f
+ const val HEIGHT = 20f
private const val CRITICAL_LEVEL = 15
// On a 12x20 grid, how wide to make the fill protection stroke.
// Scales when our size changes
private const val PROTECTION_STROKE_WIDTH = 3f
// Arbitrarily chosen for visibility at small sizes
- private const val PROTECTION_MIN_STROKE_WIDTH = 6f
+ const val PROTECTION_MIN_STROKE_WIDTH = 6f
}
}
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index aea2f52..f5c9bcd 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -884,7 +884,7 @@
Settings.Secure.SHOW_QR_CODE_SCANNER_SETTING,
Settings.Secure.SKIP_ACCESSIBILITY_SHORTCUT_DIALOG_TIMEOUT_RESTRICTION,
Settings.Secure.SPATIAL_AUDIO_ENABLED,
- Settings.Secure.TIMEOUT_TO_USER_ZERO,
+ Settings.Secure.TIMEOUT_TO_DOCK_USER,
Settings.Secure.UI_NIGHT_MODE_LAST_COMPUTED,
Settings.Secure.UI_NIGHT_MODE_OVERRIDE_OFF,
Settings.Secure.UI_NIGHT_MODE_OVERRIDE_ON);
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 47771aa..11237dc 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -195,6 +195,9 @@
<permission android:name="com.android.systemui.permission.FLAGS"
android:protectionLevel="signature" />
+ <permission android:name="android.permission.ACCESS_KEYGUARD_QUICK_AFFORDANCES"
+ android:protectionLevel="signature|privileged" />
+
<!-- Adding Quick Settings tiles -->
<uses-permission android:name="android.permission.BIND_QUICK_SETTINGS_TILE" />
@@ -993,5 +996,12 @@
android:excludeFromRecents="true"
android:exported="false">
</activity>
+
+ <provider
+ android:authorities="com.android.systemui.keyguard.quickaffordance"
+ android:name="com.android.systemui.keyguard.KeyguardQuickAffordanceProvider"
+ android:exported="true"
+ android:permission="android.permission.ACCESS_KEYGUARD_QUICK_AFFORDANCES"
+ />
</application>
</manifest>
diff --git a/packages/SystemUI/compose/features/AndroidManifest.xml b/packages/SystemUI/compose/features/AndroidManifest.xml
index eada40e..278a89f 100644
--- a/packages/SystemUI/compose/features/AndroidManifest.xml
+++ b/packages/SystemUI/compose/features/AndroidManifest.xml
@@ -34,6 +34,11 @@
android:enabled="false"
tools:replace="android:authorities"
tools:node="remove" />
+ <provider android:name="com.android.systemui.keyguard.KeyguardQuickAffordanceProvider"
+ android:authorities="com.android.systemui.test.keyguard.quickaffordance.disabled"
+ android:enabled="false"
+ tools:replace="android:authorities"
+ tools:node="remove" />
<provider android:name="com.android.keyguard.clock.ClockOptionsProvider"
android:authorities="com.android.systemui.test.keyguard.clock.disabled"
android:enabled="false"
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index ce9829b..55d6379 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -485,6 +485,12 @@
<!-- Whether to show a severe low battery dialog. -->
<bool name="config_severe_battery_dialog">false</bool>
+ <!-- A path representing a shield. Will sometimes be displayed with the battery icon when
+ needed. This path is a 10px wide and 13px tall. -->
+ <string name="config_batterymeterShieldPath" translatable="false">
+ M5 0L0 1.88V6.19C0 9.35 2.13 12.29 5 13.01C7.87 12.29 10 9.35 10 6.19V1.88L5 0Z
+ </string>
+
<!-- A path similar to frameworks/base/core/res/res/values/config.xml
config_mainBuiltInDisplayCutout that describes a path larger than the exact path of a display
cutout. If present as well as config_enableDisplayCutoutProtection is set to true, then
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index ad45471..6577b07 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -105,6 +105,12 @@
so the width of the icon should be 13.0dp * (12.0 / 20.0) -->
<dimen name="status_bar_battery_icon_width">7.8dp</dimen>
+ <!-- The battery icon is 13dp tall, but the other system icons are 15dp tall (see
+ @*android:dimen/status_bar_system_icon_size) with some top and bottom padding embedded in
+ the drawables themselves. So, the battery icon may need an extra 1dp of spacing so that its
+ bottom still aligns with the bottom of all the other system icons. See b/258672854. -->
+ <dimen name="status_bar_battery_extra_vertical_spacing">1dp</dimen>
+
<!-- The font size for the clock in the status bar. -->
<dimen name="status_bar_clock_size">14sp</dimen>
@@ -1496,10 +1502,12 @@
<!-- Dream overlay complications related dimensions -->
<dimen name="dream_overlay_complication_clock_time_text_size">86sp</dimen>
+ <dimen name="dream_overlay_complication_clock_time_padding">20dp</dimen>
<dimen name="dream_overlay_complication_clock_subtitle_text_size">24sp</dimen>
<dimen name="dream_overlay_complication_preview_text_size">36sp</dimen>
<dimen name="dream_overlay_complication_preview_icon_padding">28dp</dimen>
<dimen name="dream_overlay_complication_shadow_padding">2dp</dimen>
+ <dimen name="dream_overlay_complication_smartspace_padding">24dp</dimen>
<!-- The position of the end guide, which dream overlay complications can align their start with
if their end is aligned with the parent end. Represented as the percentage over from the
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 6824d7f..3270e45 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -316,7 +316,7 @@
<!-- Content description of the QR Code scanner for accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
<string name="accessibility_qr_code_scanner_button">QR Code Scanner</string>
<!-- Content description of the unlock button for accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
- <string name="accessibility_unlock_button">Unlock</string>
+ <string name="accessibility_unlock_button">Unlocked</string>
<!-- Content description of the lock icon for accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
<string name="accessibility_lock_icon">Device locked</string>
<!-- Content description hint of the unlock button when fingerprint is on (not shown on the screen). [CHAR LIMIT=NONE] -->
@@ -439,11 +439,17 @@
<string name="accessibility_battery_level">Battery <xliff:g id="number">%d</xliff:g> percent.</string>
<!-- Content description of the battery level icon for accessibility, including the estimated time remaining before the phone runs out of battery (not shown on the screen). [CHAR LIMIT=NONE] -->
- <string name="accessibility_battery_level_with_estimate">Battery <xliff:g id="percentage" example="95%">%1$s</xliff:g> percent, about <xliff:g id="time" example="Until 3:15pm">%2$s</xliff:g> left based on your usage</string>
+ <string name="accessibility_battery_level_with_estimate">Battery <xliff:g id="percentage" example="95%">%1$d</xliff:g> percent, about <xliff:g id="time" example="Until 3:15pm">%2$s</xliff:g> left based on your usage</string>
<!-- Content description of the battery level icon for accessibility while the device is charging (not shown on the screen). [CHAR LIMIT=NONE] -->
<string name="accessibility_battery_level_charging">Battery charging, <xliff:g id="battery_percentage">%d</xliff:g> percent.</string>
+ <!-- Content description of the battery level icon for accessibility, with information that the device charging is paused in order to protect the lifetime of the battery (not shown on screen). [CHAR LIMIT=NONE] -->
+ <string name="accessibility_battery_level_charging_paused">Battery <xliff:g id="percentage" example="90%">%d</xliff:g> percent. Charging paused for battery protection.</string>
+
+ <!-- Content description of the battery level icon for accessibility, including the estimated time remaining before the phone runs out of battery *and* information that the device charging is paused in order to protect the lifetime of the battery (not shown on screen). [CHAR LIMIT=NONE] -->
+ <string name="accessibility_battery_level_charging_paused_with_estimate">Battery <xliff:g id="percentage" example="90%">%1$d</xliff:g> percent, about <xliff:g id="time" example="Until 3:15pm">%2$s</xliff:g> left based on your usage. Charging paused for battery protection.</string>
+
<!-- Content description of overflow icon container of the notifications for accessibility (not shown on the screen)[CHAR LIMIT=NONE] -->
<string name="accessibility_overflow_action">See all notifications</string>
diff --git a/packages/SystemUI/res/xml/qqs_header.xml b/packages/SystemUI/res/xml/qqs_header.xml
index af4be1a..e07a6c1 100644
--- a/packages/SystemUI/res/xml/qqs_header.xml
+++ b/packages/SystemUI/res/xml/qqs_header.xml
@@ -25,7 +25,7 @@
android:id="@+id/clock">
<Layout
android:layout_width="wrap_content"
- android:layout_height="0dp"
+ android:layout_height="@dimen/large_screen_shade_header_min_height"
app:layout_constraintStart_toStartOf="@id/begin_guide"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
@@ -42,7 +42,7 @@
<Constraint
android:id="@+id/date">
<Layout
- android:layout_width="0dp"
+ android:layout_width="wrap_content"
android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
android:layout_marginStart="8dp"
app:layout_constrainedWidth="true"
@@ -57,7 +57,7 @@
<Constraint
android:id="@+id/statusIcons">
<Layout
- android:layout_width="0dp"
+ android:layout_width="wrap_content"
android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
app:layout_constraintHeight_min="@dimen/new_qs_header_non_clickable_element_height"
app:layout_constraintStart_toEndOf="@id/date"
@@ -65,6 +65,7 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="1"
+ app:layout_constraintHorizontal_chainStyle="packed"
/>
</Constraint>
@@ -80,12 +81,16 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="1"
+ app:layout_constraintHorizontal_chainStyle="packed"
/>
</Constraint>
<Constraint
android:id="@+id/carrier_group">
<Layout
+ app:layout_constraintWidth_min="48dp"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/large_screen_shade_header_min_height"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
diff --git a/packages/SystemUI/res/xml/qs_header_new.xml b/packages/SystemUI/res/xml/qs_header_new.xml
index d8a4e77..982c422 100644
--- a/packages/SystemUI/res/xml/qs_header_new.xml
+++ b/packages/SystemUI/res/xml/qs_header_new.xml
@@ -43,6 +43,7 @@
app:layout_constraintBottom_toBottomOf="@id/carrier_group"
app:layout_constraintEnd_toStartOf="@id/carrier_group"
app:layout_constraintHorizontal_bias="0"
+ app:layout_constraintHorizontal_chainStyle="spread_inside"
/>
<Transform
android:scaleX="2.57"
@@ -53,7 +54,7 @@
<Constraint
android:id="@+id/date">
<Layout
- android:layout_width="0dp"
+ android:layout_width="wrap_content"
android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/space"
@@ -67,16 +68,15 @@
<Constraint
android:id="@+id/carrier_group">
<Layout
- app:layout_constraintHeight_min="@dimen/large_screen_shade_header_min_height"
- android:minHeight="@dimen/large_screen_shade_header_min_height"
app:layout_constraintWidth_min="48dp"
- android:layout_width="0dp"
- android:layout_height="0dp"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/large_screen_shade_header_min_height"
app:layout_constraintStart_toEndOf="@id/clock"
app:layout_constraintTop_toBottomOf="@id/privacy_container"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1"
app:layout_constraintBottom_toTopOf="@id/batteryRemainingIcon"
+ app:layout_constraintHorizontal_chainStyle="spread_inside"
/>
<PropertySet
android:alpha="1"
@@ -86,7 +86,7 @@
<Constraint
android:id="@+id/statusIcons">
<Layout
- android:layout_width="0dp"
+ android:layout_width="wrap_content"
android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
app:layout_constrainedWidth="true"
app:layout_constraintStart_toEndOf="@id/space"
@@ -108,6 +108,7 @@
app:layout_constraintTop_toTopOf="@id/date"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="1"
+ app:layout_constraintHorizontal_chainStyle="spread_inside"
/>
</Constraint>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderClient.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderClient.kt
new file mode 100644
index 0000000..8612b3a
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderClient.kt
@@ -0,0 +1,326 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.shared.keyguard.data.content
+
+import android.annotation.SuppressLint
+import android.content.ContentValues
+import android.content.Context
+import android.database.ContentObserver
+import android.graphics.drawable.Drawable
+import android.net.Uri
+import android.os.UserHandle
+import androidx.annotation.DrawableRes
+import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract as Contract
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.withContext
+
+/** Collection of utility functions for using a content provider implementing the [Contract]. */
+object KeyguardQuickAffordanceProviderClient {
+
+ /**
+ * Selects an affordance with the given ID for a slot on the lock screen with the given ID.
+ *
+ * Note that the maximum number of selected affordances on this slot is automatically enforced.
+ * Selecting a slot that is already full (e.g. already has a number of selected affordances at
+ * its maximum capacity) will automatically remove the oldest selected affordance before adding
+ * the one passed in this call. Additionally, selecting an affordance that's already one of the
+ * selected affordances on the slot will move the selected affordance to the newest location in
+ * the slot.
+ */
+ suspend fun insertSelection(
+ context: Context,
+ slotId: String,
+ affordanceId: String,
+ dispatcher: CoroutineDispatcher = Dispatchers.IO,
+ ) {
+ withContext(dispatcher) {
+ context.contentResolver.insert(
+ Contract.SelectionTable.URI,
+ ContentValues().apply {
+ put(Contract.SelectionTable.Columns.SLOT_ID, slotId)
+ put(Contract.SelectionTable.Columns.AFFORDANCE_ID, affordanceId)
+ }
+ )
+ }
+ }
+
+ /** Returns all available slots supported by the device. */
+ suspend fun querySlots(
+ context: Context,
+ dispatcher: CoroutineDispatcher = Dispatchers.IO,
+ ): List<Slot> {
+ return withContext(dispatcher) {
+ context.contentResolver
+ .query(
+ Contract.SlotTable.URI,
+ null,
+ null,
+ null,
+ null,
+ )
+ ?.use { cursor ->
+ buildList {
+ val idColumnIndex = cursor.getColumnIndex(Contract.SlotTable.Columns.ID)
+ val capacityColumnIndex =
+ cursor.getColumnIndex(Contract.SlotTable.Columns.CAPACITY)
+ if (idColumnIndex == -1 || capacityColumnIndex == -1) {
+ return@buildList
+ }
+
+ while (cursor.moveToNext()) {
+ add(
+ Slot(
+ id = cursor.getString(idColumnIndex),
+ capacity = cursor.getInt(capacityColumnIndex),
+ )
+ )
+ }
+ }
+ }
+ }
+ ?: emptyList()
+ }
+
+ /**
+ * Returns [Flow] for observing the collection of slots.
+ *
+ * @see [querySlots]
+ */
+ fun observeSlots(
+ context: Context,
+ dispatcher: CoroutineDispatcher = Dispatchers.IO,
+ ): Flow<List<Slot>> {
+ return observeUri(
+ context,
+ Contract.SlotTable.URI,
+ )
+ .map { querySlots(context, dispatcher) }
+ }
+
+ /**
+ * Returns all available affordances supported by the device, regardless of current slot
+ * placement.
+ */
+ suspend fun queryAffordances(
+ context: Context,
+ dispatcher: CoroutineDispatcher = Dispatchers.IO,
+ ): List<Affordance> {
+ return withContext(dispatcher) {
+ context.contentResolver
+ .query(
+ Contract.AffordanceTable.URI,
+ null,
+ null,
+ null,
+ null,
+ )
+ ?.use { cursor ->
+ buildList {
+ val idColumnIndex =
+ cursor.getColumnIndex(Contract.AffordanceTable.Columns.ID)
+ val nameColumnIndex =
+ cursor.getColumnIndex(Contract.AffordanceTable.Columns.NAME)
+ val iconColumnIndex =
+ cursor.getColumnIndex(Contract.AffordanceTable.Columns.ICON)
+ if (idColumnIndex == -1 || nameColumnIndex == -1 || iconColumnIndex == -1) {
+ return@buildList
+ }
+
+ while (cursor.moveToNext()) {
+ add(
+ Affordance(
+ id = cursor.getString(idColumnIndex),
+ name = cursor.getString(nameColumnIndex),
+ iconResourceId = cursor.getInt(iconColumnIndex),
+ )
+ )
+ }
+ }
+ }
+ }
+ ?: emptyList()
+ }
+
+ /**
+ * Returns [Flow] for observing the collection of affordances.
+ *
+ * @see [queryAffordances]
+ */
+ fun observeAffordances(
+ context: Context,
+ dispatcher: CoroutineDispatcher = Dispatchers.IO,
+ ): Flow<List<Affordance>> {
+ return observeUri(
+ context,
+ Contract.AffordanceTable.URI,
+ )
+ .map { queryAffordances(context, dispatcher) }
+ }
+
+ /** Returns the current slot-affordance selections. */
+ suspend fun querySelections(
+ context: Context,
+ dispatcher: CoroutineDispatcher = Dispatchers.IO,
+ ): List<Selection> {
+ return withContext(dispatcher) {
+ context.contentResolver
+ .query(
+ Contract.SelectionTable.URI,
+ null,
+ null,
+ null,
+ null,
+ )
+ ?.use { cursor ->
+ buildList {
+ val slotIdColumnIndex =
+ cursor.getColumnIndex(Contract.SelectionTable.Columns.SLOT_ID)
+ val affordanceIdColumnIndex =
+ cursor.getColumnIndex(Contract.SelectionTable.Columns.AFFORDANCE_ID)
+ if (slotIdColumnIndex == -1 || affordanceIdColumnIndex == -1) {
+ return@buildList
+ }
+
+ while (cursor.moveToNext()) {
+ add(
+ Selection(
+ slotId = cursor.getString(slotIdColumnIndex),
+ affordanceId = cursor.getString(affordanceIdColumnIndex),
+ )
+ )
+ }
+ }
+ }
+ }
+ ?: emptyList()
+ }
+
+ /**
+ * Returns [Flow] for observing the collection of selections.
+ *
+ * @see [querySelections]
+ */
+ fun observeSelections(
+ context: Context,
+ dispatcher: CoroutineDispatcher = Dispatchers.IO,
+ ): Flow<List<Selection>> {
+ return observeUri(
+ context,
+ Contract.SelectionTable.URI,
+ )
+ .map { querySelections(context, dispatcher) }
+ }
+
+ /** Unselects an affordance with the given ID from the slot with the given ID. */
+ suspend fun deleteSelection(
+ context: Context,
+ slotId: String,
+ affordanceId: String,
+ dispatcher: CoroutineDispatcher = Dispatchers.IO,
+ ) {
+ withContext(dispatcher) {
+ context.contentResolver.delete(
+ Contract.SelectionTable.URI,
+ "${Contract.SelectionTable.Columns.SLOT_ID} = ? AND" +
+ " ${Contract.SelectionTable.Columns.AFFORDANCE_ID} = ?",
+ arrayOf(
+ slotId,
+ affordanceId,
+ ),
+ )
+ }
+ }
+
+ /** Unselects all affordances from the slot with the given ID. */
+ suspend fun deleteAllSelections(
+ context: Context,
+ slotId: String,
+ dispatcher: CoroutineDispatcher = Dispatchers.IO,
+ ) {
+ withContext(dispatcher) {
+ context.contentResolver.delete(
+ Contract.SelectionTable.URI,
+ "${Contract.SelectionTable.Columns.SLOT_ID}",
+ arrayOf(
+ slotId,
+ ),
+ )
+ }
+ }
+
+ private fun observeUri(
+ context: Context,
+ uri: Uri,
+ ): Flow<Unit> {
+ return callbackFlow {
+ val observer =
+ object : ContentObserver(null) {
+ override fun onChange(selfChange: Boolean) {
+ trySend(Unit)
+ }
+ }
+
+ context.contentResolver.registerContentObserver(
+ uri,
+ /* notifyForDescendants= */ true,
+ observer,
+ UserHandle.USER_CURRENT,
+ )
+
+ awaitClose { context.contentResolver.unregisterContentObserver(observer) }
+ }
+ .onStart { emit(Unit) }
+ }
+
+ @SuppressLint("UseCompatLoadingForDrawables")
+ suspend fun getAffordanceIcon(
+ context: Context,
+ @DrawableRes iconResourceId: Int,
+ dispatcher: CoroutineDispatcher = Dispatchers.IO,
+ ): Drawable {
+ return withContext(dispatcher) {
+ context.packageManager
+ .getResourcesForApplication(SYSTEM_UI_PACKAGE_NAME)
+ .getDrawable(iconResourceId)
+ }
+ }
+
+ data class Slot(
+ val id: String,
+ val capacity: Int,
+ )
+
+ data class Affordance(
+ val id: String,
+ val name: String,
+ val iconResourceId: Int,
+ )
+
+ data class Selection(
+ val slotId: String,
+ val affordanceId: String,
+ )
+
+ private const val SYSTEM_UI_PACKAGE_NAME = "com.android.systemui"
+}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderContract.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderContract.kt
new file mode 100644
index 0000000..c2658a9
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderContract.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.shared.keyguard.data.content
+
+import android.content.ContentResolver
+import android.net.Uri
+
+/** Contract definitions for querying content about keyguard quick affordances. */
+object KeyguardQuickAffordanceProviderContract {
+
+ const val AUTHORITY = "com.android.systemui.keyguard.quickaffordance"
+ const val PERMISSION = "android.permission.ACCESS_KEYGUARD_QUICK_AFFORDANCES"
+
+ private val BASE_URI: Uri =
+ Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(AUTHORITY).build()
+
+ /**
+ * Table for slots.
+ *
+ * Slots are positions where affordances can be placed on the lock screen. Affordances that are
+ * placed on slots are said to be "selected". The system supports the idea of multiple
+ * affordances per slot, though the implementation may limit the number of affordances on each
+ * slot.
+ *
+ * Supported operations:
+ * - Query - to know which slots are available, query the [SlotTable.URI] [Uri]. The result set
+ * will contain rows with the [SlotTable.Columns] columns.
+ */
+ object SlotTable {
+ const val TABLE_NAME = "slots"
+ val URI: Uri = BASE_URI.buildUpon().path(TABLE_NAME).build()
+
+ object Columns {
+ /** String. Unique ID for this slot. */
+ const val ID = "id"
+ /** Integer. The maximum number of affordances that can be placed in the slot. */
+ const val CAPACITY = "capacity"
+ }
+ }
+
+ /**
+ * Table for affordances.
+ *
+ * Affordances are actions/buttons that the user can execute. They are placed on slots on the
+ * lock screen.
+ *
+ * Supported operations:
+ * - Query - to know about all the affordances that are available on the device, regardless of
+ * which ones are currently selected, query the [AffordanceTable.URI] [Uri]. The result set will
+ * contain rows, each with the columns specified in [AffordanceTable.Columns].
+ */
+ object AffordanceTable {
+ const val TABLE_NAME = "affordances"
+ val URI: Uri = BASE_URI.buildUpon().path(TABLE_NAME).build()
+
+ object Columns {
+ /** String. Unique ID for this affordance. */
+ const val ID = "id"
+ /** String. User-visible name for this affordance. */
+ const val NAME = "name"
+ /**
+ * Integer. Resource ID for the drawable to load for this affordance. This is a resource
+ * ID from the system UI package.
+ */
+ const val ICON = "icon"
+ }
+ }
+
+ /**
+ * Table for selections.
+ *
+ * Selections are pairs of slot and affordance IDs.
+ *
+ * Supported operations:
+ * - Insert - to insert an affordance and place it in a slot, insert values for the columns into
+ * the [SelectionTable.URI] [Uri]. The maximum capacity rule is enforced by the system.
+ * Selecting a new affordance for a slot that is already full will automatically remove the
+ * oldest affordance from the slot.
+ * - Query - to know which affordances are set on which slots, query the [SelectionTable.URI]
+ * [Uri]. The result set will contain rows, each of which with the columns from
+ * [SelectionTable.Columns].
+ * - Delete - to unselect an affordance, removing it from a slot, delete from the
+ * [SelectionTable.URI] [Uri], passing in values for each column.
+ */
+ object SelectionTable {
+ const val TABLE_NAME = "selections"
+ val URI: Uri = BASE_URI.buildUpon().path(TABLE_NAME).build()
+
+ object Columns {
+ /** String. Unique ID for the slot. */
+ const val SLOT_ID = "slot_id"
+ /** String. Unique ID for the selected affordance. */
+ const val AFFORDANCE_ID = "affordance_id"
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java
index db64f05..8fa7b11 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java
@@ -68,7 +68,7 @@
private final KeyguardUpdateMonitorCallback mUpdateCallback =
new KeyguardUpdateMonitorCallback() {
@Override
- public void onTrustGrantedWithFlags(int flags, int userId) {
+ public void onTrustGrantedWithFlags(int flags, int userId, String message) {
if (userId != KeyguardUpdateMonitor.getCurrentUser()) return;
boolean bouncerVisible = mView.isVisibleToUser();
boolean temporaryAndRenewable =
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
index ffcf42f..5c4126e 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -727,6 +727,11 @@
mViewMode.reloadColors();
}
+ /** Handles density or font scale changes. */
+ void onDensityOrFontScaleChanged() {
+ mViewMode.onDensityOrFontScaleChanged();
+ }
+
/**
* Enscapsulates the differences between bouncer modes for the container.
*/
@@ -752,6 +757,9 @@
/** Refresh colors */
default void reloadColors() {};
+ /** Handles density or font scale changes. */
+ default void onDensityOrFontScaleChanged() {}
+
/** On a successful auth, optionally handle how the view disappears */
default void startDisappearAnimation(SecurityMode securityMode) {};
@@ -899,14 +907,9 @@
mFalsingA11yDelegate = falsingA11yDelegate;
if (mUserSwitcherViewGroup == null) {
- LayoutInflater.from(v.getContext()).inflate(
- R.layout.keyguard_bouncer_user_switcher,
- mView,
- true);
- mUserSwitcherViewGroup = mView.findViewById(R.id.keyguard_bouncer_user_switcher);
+ inflateUserSwitcher();
}
updateSecurityViewLocation();
- mUserSwitcher = mView.findViewById(R.id.user_switcher_header);
setupUserSwitcher();
mUserSwitcherController.addUserSwitchCallback(mUserSwitchCallback);
}
@@ -937,6 +940,12 @@
}
@Override
+ public void onDensityOrFontScaleChanged() {
+ mView.removeView(mUserSwitcherViewGroup);
+ inflateUserSwitcher();
+ }
+
+ @Override
public void onDestroy() {
mUserSwitcherController.removeUserSwitchCallback(mUserSwitchCallback);
}
@@ -1145,6 +1154,15 @@
}
}
+ private void inflateUserSwitcher() {
+ LayoutInflater.from(mView.getContext()).inflate(
+ R.layout.keyguard_bouncer_user_switcher,
+ mView,
+ true);
+ mUserSwitcherViewGroup = mView.findViewById(R.id.keyguard_bouncer_user_switcher);
+ mUserSwitcher = mView.findViewById(R.id.user_switcher_header);
+ }
+
interface UserSwitcherCallback {
void showUnlockToContinueMessage();
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index 7a49926..01be33e 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -251,6 +251,11 @@
public void onUiModeChanged() {
reloadColors();
}
+
+ @Override
+ public void onDensityOrFontScaleChanged() {
+ KeyguardSecurityContainerController.this.onDensityOrFontScaleChanged();
+ }
};
private boolean mBouncerVisible = false;
private final KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback =
@@ -727,6 +732,14 @@
mView.reloadColors();
}
+ /** Handles density or font scale changes. */
+ private void onDensityOrFontScaleChanged() {
+ mSecurityViewFlipperController.onDensityOrFontScaleChanged();
+ mSecurityViewFlipperController.getSecurityView(mCurrentSecurityMode,
+ mKeyguardSecurityCallback);
+ mView.onDensityOrFontScaleChanged();
+ }
+
static class Factory {
private final KeyguardSecurityContainer mView;
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipperController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipperController.java
index bddf4b0..25afe11 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipperController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipperController.java
@@ -83,6 +83,13 @@
}
}
+ /** Handles density or font scale changes. */
+ public void onDensityOrFontScaleChanged() {
+ mView.removeAllViews();
+ mChildren.clear();
+ }
+
+
@VisibleForTesting
KeyguardInputViewController<KeyguardInputView> getSecurityView(SecurityMode securityMode,
KeyguardSecurityCallback keyguardSecurityCallback) {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java
index 83e23bd..8b9823b 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java
@@ -17,6 +17,7 @@
package com.android.keyguard;
import android.content.Context;
+import android.os.Trace;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
@@ -112,4 +113,11 @@
mKeyguardSlice.dump(pw, args);
}
}
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ Trace.beginSection("KeyguardStatusView#onMeasure");
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ Trace.endSection();
+ }
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 055f376..2ac93b5 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -470,19 +470,8 @@
FACE_AUTH_TRIGGERED_TRUST_DISABLED);
}
- mLogger.logTrustChanged(wasTrusted, enabled, userId);
- for (int i = 0; i < mCallbacks.size(); i++) {
- KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
- if (cb != null) {
- cb.onTrustChanged(userId);
- if (enabled && flags != 0) {
- cb.onTrustGrantedWithFlags(flags, userId);
- }
- }
- }
-
+ String message = null;
if (KeyguardUpdateMonitor.getCurrentUser() == userId) {
- CharSequence message = null;
final boolean userHasTrust = getUserHasTrust(userId);
if (userHasTrust && trustGrantedMessages != null) {
for (String msg : trustGrantedMessages) {
@@ -492,14 +481,17 @@
}
}
}
-
- if (message != null) {
- mLogger.logShowTrustGrantedMessage(message.toString());
- }
- for (int i = 0; i < mCallbacks.size(); i++) {
- KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
- if (cb != null) {
- cb.showTrustGrantedMessage(message);
+ }
+ mLogger.logTrustChanged(wasTrusted, enabled, userId);
+ if (message != null) {
+ mLogger.logShowTrustGrantedMessage(message.toString());
+ }
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
+ if (cb != null) {
+ cb.onTrustChanged(userId);
+ if (enabled) {
+ cb.onTrustGrantedWithFlags(flags, userId, message);
}
}
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
index c06e1dc..c5142f3 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
@@ -174,14 +174,12 @@
public void onTrustManagedChanged(int userId) { }
/**
- * Called after trust was granted with non-zero flags.
+ * Called after trust was granted.
+ * @param userId of the user that has been granted trust
+ * @param message optional message the trust agent has provided to show that should indicate
+ * why trust was granted.
*/
- public void onTrustGrantedWithFlags(int flags, int userId) { }
-
- /**
- * Called when setting the trust granted message.
- */
- public void showTrustGrantedMessage(@Nullable CharSequence message) { }
+ public void onTrustGrantedWithFlags(int flags, int userId, @Nullable String message) { }
/**
* Called when a biometric has been acquired.
diff --git a/packages/SystemUI/src/com/android/systemui/battery/AccessorizedBatteryDrawable.kt b/packages/SystemUI/src/com/android/systemui/battery/AccessorizedBatteryDrawable.kt
new file mode 100644
index 0000000..b52ddc1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/battery/AccessorizedBatteryDrawable.kt
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.battery
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.ColorFilter
+import android.graphics.Matrix
+import android.graphics.Paint
+import android.graphics.Path
+import android.graphics.PixelFormat
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffXfermode
+import android.graphics.Rect
+import android.graphics.drawable.DrawableWrapper
+import android.util.PathParser
+import com.android.settingslib.graph.ThemedBatteryDrawable
+import com.android.systemui.R
+import com.android.systemui.battery.BatterySpecs.BATTERY_HEIGHT
+import com.android.systemui.battery.BatterySpecs.BATTERY_HEIGHT_WITH_SHIELD
+import com.android.systemui.battery.BatterySpecs.BATTERY_WIDTH
+import com.android.systemui.battery.BatterySpecs.BATTERY_WIDTH_WITH_SHIELD
+import com.android.systemui.battery.BatterySpecs.SHIELD_LEFT_OFFSET
+import com.android.systemui.battery.BatterySpecs.SHIELD_STROKE
+import com.android.systemui.battery.BatterySpecs.SHIELD_TOP_OFFSET
+
+/**
+ * A battery drawable that accessorizes [ThemedBatteryDrawable] with additional information if
+ * necessary.
+ *
+ * For now, it adds a shield in the bottom-right corner when [displayShield] is true.
+ */
+class AccessorizedBatteryDrawable(
+ private val context: Context,
+ frameColor: Int,
+) : DrawableWrapper(ThemedBatteryDrawable(context, frameColor)) {
+ private val mainBatteryDrawable: ThemedBatteryDrawable
+ get() = drawable as ThemedBatteryDrawable
+
+ private val shieldPath = Path()
+ private val scaledShield = Path()
+ private val scaleMatrix = Matrix()
+
+ private var shieldLeftOffsetScaled = SHIELD_LEFT_OFFSET
+ private var shieldTopOffsetScaled = SHIELD_TOP_OFFSET
+
+ private var density = context.resources.displayMetrics.density
+
+ private val dualTone =
+ context.resources.getBoolean(com.android.internal.R.bool.config_batterymeterDualTone)
+
+ private val shieldTransparentOutlinePaint =
+ Paint(Paint.ANTI_ALIAS_FLAG).also { p ->
+ p.color = Color.TRANSPARENT
+ p.strokeWidth = ThemedBatteryDrawable.PROTECTION_MIN_STROKE_WIDTH
+ p.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
+ p.style = Paint.Style.FILL_AND_STROKE
+ }
+
+ private val shieldPaint =
+ Paint(Paint.ANTI_ALIAS_FLAG).also { p ->
+ p.color = Color.MAGENTA
+ p.style = Paint.Style.FILL
+ p.isDither = true
+ }
+
+ init {
+ loadPaths()
+ }
+
+ override fun onBoundsChange(bounds: Rect) {
+ super.onBoundsChange(bounds)
+ updateSizes()
+ }
+
+ var displayShield: Boolean = false
+
+ private fun updateSizes() {
+ val b = bounds
+ if (b.isEmpty) {
+ return
+ }
+
+ val mainWidth = BatterySpecs.getMainBatteryWidth(b.width().toFloat(), displayShield)
+ val mainHeight = BatterySpecs.getMainBatteryHeight(b.height().toFloat(), displayShield)
+
+ drawable?.setBounds(
+ b.left,
+ b.top,
+ /* right= */ b.left + mainWidth.toInt(),
+ /* bottom= */ b.top + mainHeight.toInt()
+ )
+
+ if (displayShield) {
+ val sx = b.right / BATTERY_WIDTH_WITH_SHIELD
+ val sy = b.bottom / BATTERY_HEIGHT_WITH_SHIELD
+ scaleMatrix.setScale(sx, sy)
+ shieldPath.transform(scaleMatrix, scaledShield)
+
+ shieldLeftOffsetScaled = sx * SHIELD_LEFT_OFFSET
+ shieldTopOffsetScaled = sy * SHIELD_TOP_OFFSET
+
+ val scaledStrokeWidth =
+ (sx * SHIELD_STROKE).coerceAtLeast(
+ ThemedBatteryDrawable.PROTECTION_MIN_STROKE_WIDTH
+ )
+ shieldTransparentOutlinePaint.strokeWidth = scaledStrokeWidth
+ }
+ }
+
+ override fun getIntrinsicHeight(): Int {
+ val height =
+ if (displayShield) {
+ BATTERY_HEIGHT_WITH_SHIELD
+ } else {
+ BATTERY_HEIGHT
+ }
+ return (height * density).toInt()
+ }
+
+ override fun getIntrinsicWidth(): Int {
+ val width =
+ if (displayShield) {
+ BATTERY_WIDTH_WITH_SHIELD
+ } else {
+ BATTERY_WIDTH
+ }
+ return (width * density).toInt()
+ }
+
+ override fun draw(c: Canvas) {
+ c.saveLayer(null, null)
+ // Draw the main battery icon
+ super.draw(c)
+
+ if (displayShield) {
+ c.translate(shieldLeftOffsetScaled, shieldTopOffsetScaled)
+ // We need a transparent outline around the shield, so first draw the transparent-ness
+ // then draw the shield
+ c.drawPath(scaledShield, shieldTransparentOutlinePaint)
+ c.drawPath(scaledShield, shieldPaint)
+ }
+ c.restore()
+ }
+
+ override fun getOpacity(): Int {
+ return PixelFormat.OPAQUE
+ }
+
+ override fun setAlpha(p0: Int) {
+ // Unused internally -- see [ThemedBatteryDrawable.setAlpha].
+ }
+
+ override fun setColorFilter(colorfilter: ColorFilter?) {
+ super.setColorFilter(colorFilter)
+ shieldPaint.colorFilter = colorFilter
+ }
+
+ /** Sets whether the battery is currently charging. */
+ fun setCharging(charging: Boolean) {
+ mainBatteryDrawable.charging = charging
+ }
+
+ /** Sets the current level (out of 100) of the battery. */
+ fun setBatteryLevel(level: Int) {
+ mainBatteryDrawable.setBatteryLevel(level)
+ }
+
+ /** Sets whether power save is enabled. */
+ fun setPowerSaveEnabled(powerSaveEnabled: Boolean) {
+ mainBatteryDrawable.powerSaveEnabled = powerSaveEnabled
+ }
+
+ /** Returns whether power save is currently enabled. */
+ fun getPowerSaveEnabled(): Boolean {
+ return mainBatteryDrawable.powerSaveEnabled
+ }
+
+ /** Sets the colors to use for the icon. */
+ fun setColors(fgColor: Int, bgColor: Int, singleToneColor: Int) {
+ shieldPaint.color = if (dualTone) fgColor else singleToneColor
+ mainBatteryDrawable.setColors(fgColor, bgColor, singleToneColor)
+ }
+
+ /** Notifies this drawable that the density might have changed. */
+ fun notifyDensityChanged() {
+ density = context.resources.displayMetrics.density
+ }
+
+ private fun loadPaths() {
+ val shieldPathString = context.resources.getString(R.string.config_batterymeterShieldPath)
+ shieldPath.set(PathParser.createPathFromPathData(shieldPathString))
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
index 6a10d4a..03d999f 100644
--- a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
+++ b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
@@ -45,7 +45,6 @@
import androidx.annotation.StyleRes;
import androidx.annotation.VisibleForTesting;
-import com.android.settingslib.graph.ThemedBatteryDrawable;
import com.android.systemui.DualToneHandler;
import com.android.systemui.R;
import com.android.systemui.animation.Interpolators;
@@ -68,7 +67,7 @@
public static final int MODE_OFF = 2;
public static final int MODE_ESTIMATE = 3;
- private final ThemedBatteryDrawable mDrawable;
+ private final AccessorizedBatteryDrawable mDrawable;
private final ImageView mBatteryIconView;
private TextView mBatteryPercentView;
@@ -77,7 +76,10 @@
private int mLevel;
private int mShowPercentMode = MODE_DEFAULT;
private boolean mShowPercentAvailable;
+ private String mEstimateText = null;
private boolean mCharging;
+ private boolean mIsOverheated;
+ private boolean mDisplayShieldEnabled;
// Error state where we know nothing about the current battery state
private boolean mBatteryStateUnknown;
// Lazily-loaded since this is expected to be a rare-if-ever state
@@ -106,7 +108,7 @@
final int frameColor = atts.getColor(R.styleable.BatteryMeterView_frameColor,
context.getColor(R.color.meter_background_color));
mPercentageStyleId = atts.getResourceId(R.styleable.BatteryMeterView_textAppearance, 0);
- mDrawable = new ThemedBatteryDrawable(context, frameColor);
+ mDrawable = new AccessorizedBatteryDrawable(context, frameColor);
atts.recycle();
mShowPercentAvailable = context.getResources().getBoolean(
@@ -170,12 +172,14 @@
if (mode == mShowPercentMode) return;
mShowPercentMode = mode;
updateShowPercent();
+ updatePercentText();
}
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
updatePercentView();
+ mDrawable.notifyDensityChanged();
}
public void setColorsFromContext(Context context) {
@@ -203,6 +207,17 @@
mDrawable.setPowerSaveEnabled(isPowerSave);
}
+ void onIsOverheatedChanged(boolean isOverheated) {
+ boolean valueChanged = mIsOverheated != isOverheated;
+ mIsOverheated = isOverheated;
+ if (valueChanged) {
+ updateContentDescription();
+ // The battery drawable is a different size depending on whether it's currently
+ // overheated or not, so we need to re-scale the view when overheated changes.
+ scaleBatteryMeterViews();
+ }
+ }
+
private TextView loadPercentView() {
return (TextView) LayoutInflater.from(getContext())
.inflate(R.layout.battery_percentage_view, null);
@@ -227,13 +242,17 @@
mBatteryEstimateFetcher = fetcher;
}
+ void setDisplayShieldEnabled(boolean displayShieldEnabled) {
+ mDisplayShieldEnabled = displayShieldEnabled;
+ }
+
void updatePercentText() {
if (mBatteryStateUnknown) {
- setContentDescription(getContext().getString(R.string.accessibility_battery_unknown));
return;
}
if (mBatteryEstimateFetcher == null) {
+ setPercentTextAtCurrentLevel();
return;
}
@@ -245,10 +264,9 @@
return;
}
if (estimate != null && mShowPercentMode == MODE_ESTIMATE) {
+ mEstimateText = estimate;
mBatteryPercentView.setText(estimate);
- setContentDescription(getContext().getString(
- R.string.accessibility_battery_level_with_estimate,
- mLevel, estimate));
+ updateContentDescription();
} else {
setPercentTextAtCurrentLevel();
}
@@ -257,28 +275,49 @@
setPercentTextAtCurrentLevel();
}
} else {
- setContentDescription(
- getContext().getString(mCharging ? R.string.accessibility_battery_level_charging
- : R.string.accessibility_battery_level, mLevel));
+ updateContentDescription();
}
}
private void setPercentTextAtCurrentLevel() {
- if (mBatteryPercentView == null) {
- return;
+ if (mBatteryPercentView != null) {
+ mEstimateText = null;
+ String percentText = NumberFormat.getPercentInstance().format(mLevel / 100f);
+ // Setting text actually triggers a layout pass (because the text view is set to
+ // wrap_content width and TextView always relayouts for this). Avoid needless
+ // relayout if the text didn't actually change.
+ if (!TextUtils.equals(mBatteryPercentView.getText(), percentText)) {
+ mBatteryPercentView.setText(percentText);
+ }
}
- String percentText = NumberFormat.getPercentInstance().format(mLevel / 100f);
- // Setting text actually triggers a layout pass (because the text view is set to
- // wrap_content width and TextView always relayouts for this). Avoid needless
- // relayout if the text didn't actually change.
- if (!TextUtils.equals(mBatteryPercentView.getText(), percentText)) {
- mBatteryPercentView.setText(percentText);
+ updateContentDescription();
+ }
+
+ private void updateContentDescription() {
+ Context context = getContext();
+
+ String contentDescription;
+ if (mBatteryStateUnknown) {
+ contentDescription = context.getString(R.string.accessibility_battery_unknown);
+ } else if (mShowPercentMode == MODE_ESTIMATE && !TextUtils.isEmpty(mEstimateText)) {
+ contentDescription = context.getString(
+ mIsOverheated
+ ? R.string.accessibility_battery_level_charging_paused_with_estimate
+ : R.string.accessibility_battery_level_with_estimate,
+ mLevel,
+ mEstimateText);
+ } else if (mIsOverheated) {
+ contentDescription =
+ context.getString(R.string.accessibility_battery_level_charging_paused, mLevel);
+ } else if (mCharging) {
+ contentDescription =
+ context.getString(R.string.accessibility_battery_level_charging, mLevel);
+ } else {
+ contentDescription = context.getString(R.string.accessibility_battery_level, mLevel);
}
- setContentDescription(
- getContext().getString(mCharging ? R.string.accessibility_battery_level_charging
- : R.string.accessibility_battery_level, mLevel));
+ setContentDescription(contentDescription);
}
void updateShowPercent() {
@@ -329,6 +368,7 @@
}
mBatteryStateUnknown = isUnknown;
+ updateContentDescription();
if (mBatteryStateUnknown) {
mBatteryIconView.setImageDrawable(getUnknownStateDrawable());
@@ -349,15 +389,43 @@
res.getValue(R.dimen.status_bar_icon_scale_factor, typedValue, true);
float iconScaleFactor = typedValue.getFloat();
- int batteryHeight = res.getDimensionPixelSize(R.dimen.status_bar_battery_icon_height);
- int batteryWidth = res.getDimensionPixelSize(R.dimen.status_bar_battery_icon_width);
+ float mainBatteryHeight =
+ res.getDimensionPixelSize(R.dimen.status_bar_battery_icon_height) * iconScaleFactor;
+ float mainBatteryWidth =
+ res.getDimensionPixelSize(R.dimen.status_bar_battery_icon_width) * iconScaleFactor;
+
+ // If the battery is marked as overheated, we should display a shield indicating that the
+ // battery is being "defended".
+ boolean displayShield = mDisplayShieldEnabled && mIsOverheated;
+ float fullBatteryIconHeight =
+ BatterySpecs.getFullBatteryHeight(mainBatteryHeight, displayShield);
+ float fullBatteryIconWidth =
+ BatterySpecs.getFullBatteryWidth(mainBatteryWidth, displayShield);
+
+ int marginTop;
+ if (displayShield) {
+ // If the shield is displayed, we need some extra marginTop so that the bottom of the
+ // main icon is still aligned with the bottom of all the other system icons.
+ int shieldHeightAddition = Math.round(fullBatteryIconHeight - mainBatteryHeight);
+ // However, the other system icons have some embedded bottom padding that the battery
+ // doesn't have, so we shouldn't move the battery icon down by the full amount.
+ // See b/258672854.
+ marginTop = shieldHeightAddition
+ - res.getDimensionPixelSize(R.dimen.status_bar_battery_extra_vertical_spacing);
+ } else {
+ marginTop = 0;
+ }
+
int marginBottom = res.getDimensionPixelSize(R.dimen.battery_margin_bottom);
LinearLayout.LayoutParams scaledLayoutParams = new LinearLayout.LayoutParams(
- (int) (batteryWidth * iconScaleFactor), (int) (batteryHeight * iconScaleFactor));
- scaledLayoutParams.setMargins(0, 0, 0, marginBottom);
+ Math.round(fullBatteryIconWidth),
+ Math.round(fullBatteryIconHeight));
+ scaledLayoutParams.setMargins(0, marginTop, 0, marginBottom);
+ mDrawable.setDisplayShield(displayShield);
mBatteryIconView.setLayoutParams(scaledLayoutParams);
+ mBatteryIconView.invalidateDrawable(mDrawable);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java
index ae9a323..77cb9d1 100644
--- a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java
@@ -29,6 +29,8 @@
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.settings.CurrentUserTracker;
import com.android.systemui.statusbar.phone.StatusBarIconController;
import com.android.systemui.statusbar.policy.BatteryController;
@@ -84,6 +86,11 @@
public void onBatteryUnknownStateChanged(boolean isUnknown) {
mView.onBatteryUnknownStateChanged(isUnknown);
}
+
+ @Override
+ public void onIsOverheatedChanged(boolean isOverheated) {
+ mView.onIsOverheatedChanged(isOverheated);
+ }
};
// Some places may need to show the battery conditionally, and not obey the tuner
@@ -98,6 +105,7 @@
BroadcastDispatcher broadcastDispatcher,
@Main Handler mainHandler,
ContentResolver contentResolver,
+ FeatureFlags featureFlags,
BatteryController batteryController) {
super(view);
mConfigurationController = configurationController;
@@ -106,6 +114,7 @@
mBatteryController = batteryController;
mView.setBatteryEstimateFetcher(mBatteryController::getEstimatedTimeRemainingString);
+ mView.setDisplayShieldEnabled(featureFlags.isEnabled(Flags.BATTERY_SHIELD_ICON));
mSlotBattery = getResources().getString(com.android.internal.R.string.status_bar_battery);
mSettingObserver = new SettingObserver(mainHandler);
diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatterySpecs.kt b/packages/SystemUI/src/com/android/systemui/battery/BatterySpecs.kt
new file mode 100644
index 0000000..6455a96
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/battery/BatterySpecs.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.battery
+
+import com.android.settingslib.graph.ThemedBatteryDrawable
+
+/** An object storing specs related to the battery icon in the status bar. */
+object BatterySpecs {
+
+ /** Width of the main battery icon, not including the shield. */
+ const val BATTERY_WIDTH = ThemedBatteryDrawable.WIDTH
+ /** Height of the main battery icon, not including the shield. */
+ const val BATTERY_HEIGHT = ThemedBatteryDrawable.HEIGHT
+
+ private const val SHIELD_WIDTH = 10f
+ private const val SHIELD_HEIGHT = 13f
+
+ /**
+ * Amount that the left side of the shield should be offset from the left side of the battery.
+ */
+ const val SHIELD_LEFT_OFFSET = 8f
+ /** Amount that the top of the shield should be offset from the top of the battery. */
+ const val SHIELD_TOP_OFFSET = 10f
+
+ const val SHIELD_STROKE = 4f
+
+ /** The full width of the battery icon, including the main battery icon *and* the shield. */
+ const val BATTERY_WIDTH_WITH_SHIELD = SHIELD_LEFT_OFFSET + SHIELD_WIDTH
+ /** The full height of the battery icon, including the main battery icon *and* the shield. */
+ const val BATTERY_HEIGHT_WITH_SHIELD = SHIELD_TOP_OFFSET + SHIELD_HEIGHT
+
+ /**
+ * Given the desired height of the main battery icon in pixels, returns the height that the full
+ * battery icon will take up in pixels.
+ *
+ * If there's no shield, this will just return [mainBatteryHeight]. Otherwise, the shield
+ * extends slightly below the bottom of the main battery icon so we need some extra height.
+ */
+ @JvmStatic
+ fun getFullBatteryHeight(mainBatteryHeight: Float, displayShield: Boolean): Float {
+ return if (!displayShield) {
+ mainBatteryHeight
+ } else {
+ val verticalScaleFactor = mainBatteryHeight / BATTERY_HEIGHT
+ verticalScaleFactor * BATTERY_HEIGHT_WITH_SHIELD
+ }
+ }
+
+ /**
+ * Given the desired width of the main battery icon in pixels, returns the width that the full
+ * battery icon will take up in pixels.
+ *
+ * If there's no shield, this will just return [mainBatteryWidth]. Otherwise, the shield extends
+ * past the right side of the main battery icon so we need some extra width.
+ */
+ @JvmStatic
+ fun getFullBatteryWidth(mainBatteryWidth: Float, displayShield: Boolean): Float {
+ return if (!displayShield) {
+ mainBatteryWidth
+ } else {
+ val horizontalScaleFactor = mainBatteryWidth / BATTERY_WIDTH
+ horizontalScaleFactor * BATTERY_WIDTH_WITH_SHIELD
+ }
+ }
+
+ /**
+ * Given the height of the full battery icon, return how tall the main battery icon should be.
+ *
+ * If there's no shield, this will just return [fullBatteryHeight]. Otherwise, the shield takes
+ * up some of the view's height so the main battery width will be just a portion of
+ * [fullBatteryHeight].
+ */
+ @JvmStatic
+ fun getMainBatteryHeight(fullBatteryHeight: Float, displayShield: Boolean): Float {
+ return if (!displayShield) {
+ fullBatteryHeight
+ } else {
+ return (BATTERY_HEIGHT / BATTERY_HEIGHT_WITH_SHIELD) * fullBatteryHeight
+ }
+ }
+
+ /**
+ * Given the width of the full battery icon, return how wide the main battery icon should be.
+ *
+ * If there's no shield, this will just return [fullBatteryWidth]. Otherwise, the shield takes
+ * up some of the view's width so the main battery width will be just a portion of
+ * [fullBatteryWidth].
+ */
+ @JvmStatic
+ fun getMainBatteryWidth(fullBatteryWidth: Float, displayShield: Boolean): Float {
+ return if (!displayShield) {
+ fullBatteryWidth
+ } else {
+ return (BATTERY_WIDTH / BATTERY_WIDTH_WITH_SHIELD) * fullBatteryWidth
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/GlobalRootComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/GlobalRootComponent.java
index fe89c9a..9e8c0ec 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/GlobalRootComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/GlobalRootComponent.java
@@ -21,24 +21,21 @@
import com.android.systemui.dagger.qualifiers.InstrumentationTest;
import com.android.systemui.util.InitializationChecker;
-import javax.inject.Singleton;
-
import dagger.BindsInstance;
-import dagger.Component;
/**
* Base root component for Dagger injection.
*
+ * This class is not actually annotated as a Dagger component, since it is not used directly as one.
+ * Doing so generates unnecessary code bloat.
+ *
* See {@link ReferenceGlobalRootComponent} for the one actually used by AOSP.
*/
-@Singleton
-@Component(modules = {GlobalModule.class})
public interface GlobalRootComponent {
/**
* Builder for a GlobalRootComponent.
*/
- @Component.Builder
interface Builder {
@BindsInstance
Builder context(Context context);
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSysUIComponent.java
index 7ab36e8..d3555ee 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSysUIComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSysUIComponent.java
@@ -16,6 +16,7 @@
package com.android.systemui.dagger;
+import com.android.systemui.keyguard.KeyguardQuickAffordanceProvider;
import com.android.systemui.statusbar.QsFrameTranslateModule;
import dagger.Subcomponent;
@@ -42,4 +43,9 @@
interface Builder extends SysUIComponent.Builder {
ReferenceSysUIComponent build();
}
+
+ /**
+ * Member injection into the supplied argument.
+ */
+ void inject(KeyguardQuickAffordanceProvider keyguardQuickAffordanceProvider);
}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutEngine.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutEngine.java
index 5694f6d..440dcbc 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutEngine.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutEngine.java
@@ -194,7 +194,9 @@
break;
}
- if (!isRoot) {
+ // Add margin if specified by the complication. Otherwise add default margin
+ // between complications.
+ if (mLayoutParams.isMarginSpecified() || !isRoot) {
final int margin = mLayoutParams.getMargin(mDefaultMargin);
switch(direction) {
case ComplicationLayoutParams.DIRECTION_DOWN:
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutParams.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutParams.java
index a21eb19..2b32d34 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutParams.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutParams.java
@@ -261,6 +261,13 @@
}
/**
+ * Returns whether margin has been specified by the complication.
+ */
+ public boolean isMarginSpecified() {
+ return mMargin != MARGIN_UNSPECIFIED;
+ }
+
+ /**
* Returns the margin to apply between complications, or the given default if no margin is
* specified.
*/
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java
index cedd850a..c01cf43 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java
@@ -33,6 +33,7 @@
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.CoreStartable;
import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.controls.ControlsServiceInfo;
import com.android.systemui.controls.dagger.ControlsComponent;
import com.android.systemui.controls.management.ControlsListingController;
import com.android.systemui.controls.ui.ControlsActivity;
@@ -42,6 +43,8 @@
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.util.ViewController;
+import java.util.List;
+
import javax.inject.Inject;
import javax.inject.Named;
@@ -76,16 +79,25 @@
private final DreamOverlayStateController mDreamOverlayStateController;
private final ControlsComponent mControlsComponent;
- private boolean mControlServicesAvailable = false;
+ private boolean mOverlayActive = false;
// Callback for when the home controls service availability changes.
private final ControlsListingController.ControlsListingCallback mControlsCallback =
- serviceInfos -> {
- boolean available = !serviceInfos.isEmpty();
+ services -> updateHomeControlsComplication();
- if (available != mControlServicesAvailable) {
- mControlServicesAvailable = available;
- updateComplicationAvailability();
+ private final DreamOverlayStateController.Callback mOverlayStateCallback =
+ new DreamOverlayStateController.Callback() {
+ @Override
+ public void onStateChanged() {
+ if (mOverlayActive == mDreamOverlayStateController.isOverlayActive()) {
+ return;
+ }
+
+ mOverlayActive = !mOverlayActive;
+
+ if (mOverlayActive) {
+ updateHomeControlsComplication();
+ }
}
};
@@ -102,18 +114,29 @@
public void start() {
mControlsComponent.getControlsListingController().ifPresent(
c -> c.addCallback(mControlsCallback));
+ mDreamOverlayStateController.addCallback(mOverlayStateCallback);
}
- private void updateComplicationAvailability() {
+ private void updateHomeControlsComplication() {
+ mControlsComponent.getControlsListingController().ifPresent(c -> {
+ if (isHomeControlsAvailable(c.getCurrentServices())) {
+ mDreamOverlayStateController.addComplication(mComplication);
+ } else {
+ mDreamOverlayStateController.removeComplication(mComplication);
+ }
+ });
+ }
+
+ private boolean isHomeControlsAvailable(List<ControlsServiceInfo> controlsServices) {
+ if (controlsServices.isEmpty()) {
+ return false;
+ }
+
final boolean hasFavorites = mControlsComponent.getControlsController()
.map(c -> !c.getFavorites().isEmpty())
.orElse(false);
- if (!hasFavorites || !mControlServicesAvailable
- || mControlsComponent.getVisibility() == UNAVAILABLE) {
- mDreamOverlayStateController.removeComplication(mComplication);
- } else {
- mDreamOverlayStateController.addComplication(mComplication);
- }
+ final ControlsComponent.Visibility visibility = mControlsComponent.getVisibility();
+ return hasFavorites && visibility != UNAVAILABLE;
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/RegisteredComplicationsModule.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/RegisteredComplicationsModule.java
index 7d2ce51..69b85b5 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/RegisteredComplicationsModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/RegisteredComplicationsModule.java
@@ -48,9 +48,9 @@
int DREAM_CLOCK_TIME_COMPLICATION_WEIGHT = 1;
int DREAM_SMARTSPACE_COMPLICATION_WEIGHT = 0;
- int DREAM_MEDIA_COMPLICATION_WEIGHT = -1;
- int DREAM_HOME_CONTROLS_CHIP_COMPLICATION_WEIGHT = 1;
- int DREAM_MEDIA_ENTRY_COMPLICATION_WEIGHT = 0;
+ int DREAM_MEDIA_COMPLICATION_WEIGHT = 0;
+ int DREAM_HOME_CONTROLS_CHIP_COMPLICATION_WEIGHT = 2;
+ int DREAM_MEDIA_ENTRY_COMPLICATION_WEIGHT = 1;
/**
* Provides layout parameters for the clock time complication.
@@ -60,10 +60,11 @@
static ComplicationLayoutParams provideClockTimeLayoutParams() {
return new ComplicationLayoutParams(0,
ViewGroup.LayoutParams.WRAP_CONTENT,
- ComplicationLayoutParams.POSITION_TOP
+ ComplicationLayoutParams.POSITION_BOTTOM
| ComplicationLayoutParams.POSITION_START,
- ComplicationLayoutParams.DIRECTION_DOWN,
- DREAM_CLOCK_TIME_COMPLICATION_WEIGHT);
+ ComplicationLayoutParams.DIRECTION_UP,
+ DREAM_CLOCK_TIME_COMPLICATION_WEIGHT,
+ 0 /*margin*/);
}
/**
@@ -77,8 +78,10 @@
res.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_height),
ComplicationLayoutParams.POSITION_BOTTOM
| ComplicationLayoutParams.POSITION_START,
- ComplicationLayoutParams.DIRECTION_END,
- DREAM_HOME_CONTROLS_CHIP_COMPLICATION_WEIGHT);
+ ComplicationLayoutParams.DIRECTION_UP,
+ DREAM_HOME_CONTROLS_CHIP_COMPLICATION_WEIGHT,
+ // Add margin to the bottom of home controls to horizontally align with smartspace.
+ res.getDimensionPixelSize(R.dimen.dream_overlay_complication_clock_time_padding));
}
/**
@@ -101,14 +104,13 @@
*/
@Provides
@Named(DREAM_SMARTSPACE_LAYOUT_PARAMS)
- static ComplicationLayoutParams provideSmartspaceLayoutParams() {
+ static ComplicationLayoutParams provideSmartspaceLayoutParams(@Main Resources res) {
return new ComplicationLayoutParams(0,
ViewGroup.LayoutParams.WRAP_CONTENT,
- ComplicationLayoutParams.POSITION_TOP
+ ComplicationLayoutParams.POSITION_BOTTOM
| ComplicationLayoutParams.POSITION_START,
- ComplicationLayoutParams.DIRECTION_DOWN,
+ ComplicationLayoutParams.DIRECTION_END,
DREAM_SMARTSPACE_COMPLICATION_WEIGHT,
- 0,
- true /*snapToGuide*/);
+ res.getDimensionPixelSize(R.dimen.dream_overlay_complication_smartspace_padding));
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
index f9dca08..101f4a4 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
@@ -44,7 +44,7 @@
DreamOverlayComponent.class,
})
public interface DreamModule {
- String DREAM_ONLY_ENABLED_FOR_SYSTEM_USER = "dream_only_enabled_for_system_user";
+ String DREAM_ONLY_ENABLED_FOR_DOCK_USER = "dream_only_enabled_for_dock_user";
String DREAM_SUPPORTED = "dream_supported";
@@ -70,10 +70,10 @@
/** */
@Provides
- @Named(DREAM_ONLY_ENABLED_FOR_SYSTEM_USER)
- static boolean providesDreamOnlyEnabledForSystemUser(@Main Resources resources) {
+ @Named(DREAM_ONLY_ENABLED_FOR_DOCK_USER)
+ static boolean providesDreamOnlyEnabledForDockUser(@Main Resources resources) {
return resources.getBoolean(
- com.android.internal.R.bool.config_dreamsOnlyEnabledForSystemUser);
+ com.android.internal.R.bool.config_dreamsOnlyEnabledForDockUser);
}
/** */
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 002ffdb..ea627ac 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -239,6 +239,9 @@
// TODO(b/256613548): Tracking Bug
val NEW_STATUS_BAR_WIFI_ICON_BACKEND = unreleasedFlag(609, "new_status_bar_wifi_icon_backend")
+ // TODO(b/256623670): Tracking Bug
+ @JvmField val BATTERY_SHIELD_ICON = unreleasedFlag(610, "battery_shield_icon")
+
// 700 - dialer/calls
// TODO(b/254512734): Tracking Bug
val ONGOING_CALL_STATUS_BAR_CHIP = releasedFlag(700, "ongoing_call_status_bar_chip")
@@ -301,7 +304,7 @@
@Keep
@JvmField
val WM_ENABLE_SHELL_TRANSITIONS =
- sysPropBooleanFlag(1100, "persist.wm.debug.shell_transit", default = false)
+ sysPropBooleanFlag(1100, "persist.wm.debug.shell_transit", default = true)
// TODO(b/254513207): Tracking Bug
@Keep
@@ -380,7 +383,8 @@
// TODO(b/254513155): Tracking Bug
@JvmField
- val SCREENSHOT_WORK_PROFILE_POLICY = unreleasedFlag(1301, "screenshot_work_profile_policy")
+ val SCREENSHOT_WORK_PROFILE_POLICY =
+ unreleasedFlag(1301, "screenshot_work_profile_policy", teamfood = true)
// 1400 - columbus
// TODO(b/254512756): Tracking Bug
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt
new file mode 100644
index 0000000..0f4581c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt
@@ -0,0 +1,297 @@
+/*
+ * 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
+
+import android.content.ContentProvider
+import android.content.ContentValues
+import android.content.Context
+import android.content.UriMatcher
+import android.content.pm.ProviderInfo
+import android.database.Cursor
+import android.database.MatrixCursor
+import android.net.Uri
+import android.util.Log
+import com.android.systemui.SystemUIAppComponentFactoryBase
+import com.android.systemui.SystemUIAppComponentFactoryBase.ContextAvailableCallback
+import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
+import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract as Contract
+import javax.inject.Inject
+import kotlinx.coroutines.runBlocking
+
+class KeyguardQuickAffordanceProvider :
+ ContentProvider(), SystemUIAppComponentFactoryBase.ContextInitializer {
+
+ @Inject lateinit var interactor: KeyguardQuickAffordanceInteractor
+
+ private lateinit var contextAvailableCallback: ContextAvailableCallback
+
+ private val uriMatcher =
+ UriMatcher(UriMatcher.NO_MATCH).apply {
+ addURI(
+ Contract.AUTHORITY,
+ Contract.SlotTable.TABLE_NAME,
+ MATCH_CODE_ALL_SLOTS,
+ )
+ addURI(
+ Contract.AUTHORITY,
+ Contract.AffordanceTable.TABLE_NAME,
+ MATCH_CODE_ALL_AFFORDANCES,
+ )
+ addURI(
+ Contract.AUTHORITY,
+ Contract.SelectionTable.TABLE_NAME,
+ MATCH_CODE_ALL_SELECTIONS,
+ )
+ }
+
+ override fun onCreate(): Boolean {
+ return true
+ }
+
+ override fun attachInfo(context: Context?, info: ProviderInfo?) {
+ contextAvailableCallback.onContextAvailable(checkNotNull(context))
+ super.attachInfo(context, info)
+ }
+
+ override fun setContextAvailableCallback(callback: ContextAvailableCallback) {
+ contextAvailableCallback = callback
+ }
+
+ override fun getType(uri: Uri): String? {
+ val prefix =
+ when (uriMatcher.match(uri)) {
+ MATCH_CODE_ALL_SLOTS,
+ MATCH_CODE_ALL_AFFORDANCES,
+ MATCH_CODE_ALL_SELECTIONS -> "vnd.android.cursor.dir/vnd."
+ else -> null
+ }
+
+ val tableName =
+ when (uriMatcher.match(uri)) {
+ MATCH_CODE_ALL_SLOTS -> Contract.SlotTable.TABLE_NAME
+ MATCH_CODE_ALL_AFFORDANCES -> Contract.AffordanceTable.TABLE_NAME
+ MATCH_CODE_ALL_SELECTIONS -> Contract.SelectionTable.TABLE_NAME
+ else -> null
+ }
+
+ if (prefix == null || tableName == null) {
+ return null
+ }
+
+ return "$prefix${Contract.AUTHORITY}.$tableName"
+ }
+
+ override fun insert(uri: Uri, values: ContentValues?): Uri? {
+ if (uriMatcher.match(uri) != MATCH_CODE_ALL_SELECTIONS) {
+ throw UnsupportedOperationException()
+ }
+
+ return insertSelection(values)
+ }
+
+ override fun query(
+ uri: Uri,
+ projection: Array<out String>?,
+ selection: String?,
+ selectionArgs: Array<out String>?,
+ sortOrder: String?,
+ ): Cursor? {
+ return when (uriMatcher.match(uri)) {
+ MATCH_CODE_ALL_AFFORDANCES -> queryAffordances()
+ MATCH_CODE_ALL_SLOTS -> querySlots()
+ MATCH_CODE_ALL_SELECTIONS -> querySelections()
+ else -> null
+ }
+ }
+
+ override fun update(
+ uri: Uri,
+ values: ContentValues?,
+ selection: String?,
+ selectionArgs: Array<out String>?,
+ ): Int {
+ Log.e(TAG, "Update is not supported!")
+ return 0
+ }
+
+ override fun delete(
+ uri: Uri,
+ selection: String?,
+ selectionArgs: Array<out String>?,
+ ): Int {
+ if (uriMatcher.match(uri) != MATCH_CODE_ALL_SELECTIONS) {
+ throw UnsupportedOperationException()
+ }
+
+ return deleteSelection(uri, selectionArgs)
+ }
+
+ private fun insertSelection(values: ContentValues?): Uri? {
+ if (values == null) {
+ throw IllegalArgumentException("Cannot insert selection, no values passed in!")
+ }
+
+ if (!values.containsKey(Contract.SelectionTable.Columns.SLOT_ID)) {
+ throw IllegalArgumentException(
+ "Cannot insert selection, " +
+ "\"${Contract.SelectionTable.Columns.SLOT_ID}\" not specified!"
+ )
+ }
+
+ if (!values.containsKey(Contract.SelectionTable.Columns.AFFORDANCE_ID)) {
+ throw IllegalArgumentException(
+ "Cannot insert selection, " +
+ "\"${Contract.SelectionTable.Columns.AFFORDANCE_ID}\" not specified!"
+ )
+ }
+
+ val slotId = values.getAsString(Contract.SelectionTable.Columns.SLOT_ID)
+ val affordanceId = values.getAsString(Contract.SelectionTable.Columns.AFFORDANCE_ID)
+
+ if (slotId.isNullOrEmpty()) {
+ throw IllegalArgumentException("Cannot insert selection, slot ID was empty!")
+ }
+
+ if (affordanceId.isNullOrEmpty()) {
+ throw IllegalArgumentException("Cannot insert selection, affordance ID was empty!")
+ }
+
+ val success = runBlocking {
+ interactor.select(
+ slotId = slotId,
+ affordanceId = affordanceId,
+ )
+ }
+
+ return if (success) {
+ Log.d(TAG, "Successfully selected $affordanceId for slot $slotId")
+ context?.contentResolver?.notifyChange(Contract.SelectionTable.URI, null)
+ Contract.SelectionTable.URI
+ } else {
+ Log.d(TAG, "Failed to select $affordanceId for slot $slotId")
+ null
+ }
+ }
+
+ private fun querySelections(): Cursor {
+ return MatrixCursor(
+ arrayOf(
+ Contract.SelectionTable.Columns.SLOT_ID,
+ Contract.SelectionTable.Columns.AFFORDANCE_ID,
+ )
+ )
+ .apply {
+ val affordanceIdsBySlotId = runBlocking { interactor.getSelections() }
+ affordanceIdsBySlotId.entries.forEach { (slotId, affordanceIds) ->
+ affordanceIds.forEach { affordanceId ->
+ addRow(
+ arrayOf(
+ slotId,
+ affordanceId,
+ )
+ )
+ }
+ }
+ }
+ }
+
+ private fun queryAffordances(): Cursor {
+ return MatrixCursor(
+ arrayOf(
+ Contract.AffordanceTable.Columns.ID,
+ Contract.AffordanceTable.Columns.NAME,
+ Contract.AffordanceTable.Columns.ICON,
+ )
+ )
+ .apply {
+ interactor.getAffordancePickerRepresentations().forEach { representation ->
+ addRow(
+ arrayOf(
+ representation.id,
+ representation.name,
+ representation.iconResourceId,
+ )
+ )
+ }
+ }
+ }
+
+ private fun querySlots(): Cursor {
+ return MatrixCursor(
+ arrayOf(
+ Contract.SlotTable.Columns.ID,
+ Contract.SlotTable.Columns.CAPACITY,
+ )
+ )
+ .apply {
+ interactor.getSlotPickerRepresentations().forEach { representation ->
+ addRow(
+ arrayOf(
+ representation.id,
+ representation.maxSelectedAffordances,
+ )
+ )
+ }
+ }
+ }
+
+ private fun deleteSelection(
+ uri: Uri,
+ selectionArgs: Array<out String>?,
+ ): Int {
+ if (selectionArgs == null) {
+ throw IllegalArgumentException(
+ "Cannot delete selection, selection arguments not included!"
+ )
+ }
+
+ val (slotId, affordanceId) =
+ when (selectionArgs.size) {
+ 1 -> Pair(selectionArgs[0], null)
+ 2 -> Pair(selectionArgs[0], selectionArgs[1])
+ else ->
+ throw IllegalArgumentException(
+ "Cannot delete selection, selection arguments has wrong size, expected to" +
+ " have 1 or 2 arguments, had ${selectionArgs.size} instead!"
+ )
+ }
+
+ val deleted = runBlocking {
+ interactor.unselect(
+ slotId = slotId,
+ affordanceId = affordanceId,
+ )
+ }
+
+ return if (deleted) {
+ Log.d(TAG, "Successfully unselected $affordanceId for slot $slotId")
+ context?.contentResolver?.notifyChange(uri, null)
+ 1
+ } else {
+ Log.d(TAG, "Failed to unselect $affordanceId for slot $slotId")
+ 0
+ }
+ }
+
+ companion object {
+ private const val TAG = "KeyguardQuickAffordanceProvider"
+ private const val MATCH_CODE_ALL_SLOTS = 1
+ private const val MATCH_CODE_ALL_AFFORDANCES = 2
+ private const val MATCH_CODE_ALL_SELECTIONS = 3
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt
index 783f752..9a90fe7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt
@@ -23,7 +23,10 @@
import com.android.systemui.keyguard.shared.model.KeyguardBouncerModel
import com.android.systemui.statusbar.phone.KeyguardBouncer
import javax.inject.Inject
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
/** Encapsulates app state for the lock screen primary and alternate bouncer. */
@@ -68,8 +71,12 @@
private val _keyguardAuthenticated = MutableStateFlow<Boolean?>(null)
/** Determines if user is already unlocked */
val keyguardAuthenticated = _keyguardAuthenticated.asStateFlow()
- private val _showMessage = MutableStateFlow<BouncerShowMessageModel?>(null)
- val showMessage = _showMessage.asStateFlow()
+ private val _showMessage =
+ MutableSharedFlow<BouncerShowMessageModel?>(
+ replay = 1,
+ onBufferOverflow = BufferOverflow.DROP_OLDEST
+ )
+ val showMessage = _showMessage.asSharedFlow()
private val _resourceUpdateRequests = MutableStateFlow(false)
val resourceUpdateRequests = _resourceUpdateRequests.asStateFlow()
val bouncerPromptReason: Int
@@ -118,7 +125,7 @@
}
fun setShowMessage(bouncerShowMessageModel: BouncerShowMessageModel?) {
- _showMessage.value = bouncerShowMessageModel
+ _showMessage.tryEmit(bouncerShowMessageModel)
}
fun setKeyguardAuthenticated(keyguardAuthenticated: Boolean?) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt
index fcd653b..910cdf2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt
@@ -117,7 +117,6 @@
@JvmOverloads
fun show(isScrimmed: Boolean) {
// Reset some states as we show the bouncer.
- repository.setShowMessage(null)
repository.setOnScreenTurnedOff(false)
repository.setKeyguardAuthenticated(null)
repository.setPrimaryHide(false)
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt
index a4a96806..647beb9 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt
@@ -61,7 +61,7 @@
@SuppressLint("WrongConstant") // sysui allowed to call STATUS_BAR_SERVICE
val statusBarManager = context.getSystemService(Context.STATUS_BAR_SERVICE)
as StatusBarManager
- val routeInfo = MediaRoute2Info.Builder("id", args[0])
+ val routeInfo = MediaRoute2Info.Builder(if (args.size >= 4) args[3] else "id", args[0])
.addFeature("feature")
val useAppIcon = !(args.size >= 3 && args[2] == "useAppIcon=false")
if (useAppIcon) {
@@ -107,7 +107,7 @@
override fun help(pw: PrintWriter) {
pw.println("Usage: adb shell cmd statusbar $SENDER_COMMAND " +
- "<deviceName> <chipState> useAppIcon=[true|false]")
+ "<deviceName> <chipState> useAppIcon=[true|false] <id>")
}
}
@@ -127,8 +127,10 @@
@SuppressLint("WrongConstant") // sysui is allowed to call STATUS_BAR_SERVICE
val statusBarManager = context.getSystemService(Context.STATUS_BAR_SERVICE)
as StatusBarManager
- val routeInfo = MediaRoute2Info.Builder("id", "Test Name")
- .addFeature("feature")
+ val routeInfo = MediaRoute2Info.Builder(
+ if (args.size >= 3) args[2] else "id",
+ "Test Name"
+ ).addFeature("feature")
val useAppIcon = !(args.size >= 2 && args[1] == "useAppIcon=false")
if (useAppIcon) {
routeInfo.setClientPackageName(TEST_PACKAGE_NAME)
@@ -144,7 +146,7 @@
override fun help(pw: PrintWriter) {
pw.println("Usage: adb shell cmd statusbar $RECEIVER_COMMAND " +
- "<chipState> useAppIcon=[true|false]")
+ "<chipState> useAppIcon=[true|false] <id>")
}
}
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 8bddffc..691953a 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
@@ -121,18 +121,32 @@
uiEventLogger.logReceiverStateChange(chipState)
if (chipState == ChipStateReceiver.FAR_FROM_SENDER) {
- removeView(removalReason = ChipStateReceiver.FAR_FROM_SENDER.name)
+ removeView(routeInfo.id, removalReason = ChipStateReceiver.FAR_FROM_SENDER.name)
return
}
if (appIcon == null) {
- displayView(ChipReceiverInfo(routeInfo, appIconDrawableOverride = null, appName))
+ displayView(
+ ChipReceiverInfo(
+ routeInfo,
+ appIconDrawableOverride = null,
+ appName,
+ id = routeInfo.id,
+ )
+ )
return
}
appIcon.loadDrawableAsync(
context,
Icon.OnDrawableLoadedListener { drawable ->
- displayView(ChipReceiverInfo(routeInfo, drawable, appName))
+ displayView(
+ ChipReceiverInfo(
+ routeInfo,
+ drawable,
+ appName,
+ id = routeInfo.id,
+ )
+ )
},
// Notify the listener on the main handler since the listener will update
// the UI.
@@ -234,4 +248,5 @@
val appNameOverride: CharSequence?,
override val windowTitle: String = MediaTttUtils.WINDOW_TITLE_RECEIVER,
override val wakeReason: String = MediaTttUtils.WAKE_REASON_RECEIVER,
+ override val id: String,
) : TemporaryViewInfo()
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt
index d1ea2d0..bb7bc6f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt
@@ -108,7 +108,7 @@
}
displayedState = null
- chipbarCoordinator.removeView(removalReason)
+ chipbarCoordinator.removeView(routeInfo.id, removalReason)
} else {
displayedState = chipState
chipbarCoordinator.displayView(
@@ -162,6 +162,7 @@
windowTitle = MediaTttUtils.WINDOW_TITLE_SENDER,
wakeReason = MediaTttUtils.WAKE_REASON_SENDER,
timeoutMs = chipStateSender.timeout,
+ id = routeInfo.id,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
index 920a108..d9be281 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
@@ -515,7 +515,13 @@
public void setExpanded(boolean expanded) {
if (DEBUG) Log.d(TAG, "setExpanded " + expanded);
mQsExpanded = expanded;
- updateQsPanelControllerListening();
+ if (mInSplitShade && mQsExpanded) {
+ // in split shade QS is expanded immediately when shade expansion starts and then we
+ // also need to listen to changes - otherwise QS is updated only once its fully expanded
+ setListening(true);
+ } else {
+ updateQsPanelControllerListening();
+ }
updateQsState();
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
index 2a80de0..dd88c83 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
@@ -25,6 +25,7 @@
import android.content.res.Configuration;
import android.content.res.Configuration.Orientation;
import android.metrics.LogMaker;
+import android.util.Log;
import android.view.View;
import com.android.internal.annotations.VisibleForTesting;
@@ -38,6 +39,7 @@
import com.android.systemui.qs.customize.QSCustomizerController;
import com.android.systemui.qs.external.CustomTile;
import com.android.systemui.qs.logging.QSLogger;
+import com.android.systemui.qs.tileimpl.QSTileViewImpl;
import com.android.systemui.util.LargeScreenUtils;
import com.android.systemui.util.ViewController;
import com.android.systemui.util.animation.DisappearParameters;
@@ -237,6 +239,16 @@
private void addTile(final QSTile tile, boolean collapsedView) {
final TileRecord r =
new TileRecord(tile, mHost.createTileView(getContext(), tile, collapsedView));
+ // TODO(b/250618218): Remove the QSLogger in QSTileViewImpl once we know the root cause of
+ // b/250618218.
+ try {
+ QSTileViewImpl qsTileView = (QSTileViewImpl) (r.tileView);
+ if (qsTileView != null) {
+ qsTileView.setQsLogger(mQSLogger);
+ }
+ } catch (ClassCastException e) {
+ Log.e(TAG, "Failed to cast QSTileView to QSTileViewImpl", e);
+ }
mView.addTile(r);
mRecords.add(r);
mCachedSpecs = getTilesSpecs();
diff --git a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
index 931dc8d..9f6317f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
@@ -129,12 +129,36 @@
})
}
- fun logInternetTileUpdate(lastType: Int, callback: String) {
+ fun logInternetTileUpdate(tileSpec: String, lastType: Int, callback: String) {
log(VERBOSE, {
+ str1 = tileSpec
int1 = lastType
- str1 = callback
+ str2 = callback
}, {
- "mLastTileState=$int1, Callback=$str1."
+ "[$str1] mLastTileState=$int1, Callback=$str2."
+ })
+ }
+
+ // TODO(b/250618218): Remove this method once we know the root cause of b/250618218.
+ fun logTileBackgroundColorUpdateIfInternetTile(
+ tileSpec: String,
+ state: Int,
+ disabledByPolicy: Boolean,
+ color: Int
+ ) {
+ // This method is added to further debug b/250618218 which has only been observed from the
+ // InternetTile, so we are only logging the background color change for the InternetTile
+ // to avoid spamming the QSLogger.
+ if (tileSpec != "internet") {
+ return
+ }
+ log(VERBOSE, {
+ str1 = tileSpec
+ int1 = state
+ bool1 = disabledByPolicy
+ int2 = color
+ }, {
+ "[$str1] state=$int1, disabledByPolicy=$bool1, color=$int2."
})
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
index 972b243..b355d4b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
@@ -50,6 +50,7 @@
import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.plugins.qs.QSTile.BooleanState
import com.android.systemui.plugins.qs.QSTileView
+import com.android.systemui.qs.logging.QSLogger
import com.android.systemui.qs.tileimpl.QSIconViewImpl.QS_ANIM_LENGTH
import java.util.Objects
@@ -116,7 +117,7 @@
protected lateinit var sideView: ViewGroup
private lateinit var customDrawableView: ImageView
private lateinit var chevronView: ImageView
-
+ private var mQsLogger: QSLogger? = null
protected var showRippleEffect = true
private lateinit var ripple: RippleDrawable
@@ -188,6 +189,10 @@
updateHeight()
}
+ fun setQsLogger(qsLogger: QSLogger) {
+ mQsLogger = qsLogger
+ }
+
fun updateResources() {
FontSizeUtils.updateFontSize(label, R.dimen.qs_tile_text_size)
FontSizeUtils.updateFontSize(secondaryLabel, R.dimen.qs_tile_text_size)
@@ -493,6 +498,11 @@
// Colors
if (state.state != lastState || state.disabledByPolicy || lastDisabledByPolicy) {
singleAnimator.cancel()
+ mQsLogger?.logTileBackgroundColorUpdateIfInternetTile(
+ state.spec,
+ state.state,
+ state.disabledByPolicy,
+ getBackgroundColorForState(state.state, state.disabledByPolicy))
if (allowAnimations) {
singleAnimator.setValues(
colorValuesHolder(
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DreamTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DreamTile.java
index bebd580..4abe309 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DreamTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DreamTile.java
@@ -70,7 +70,7 @@
private final SettingObserver mDreamSettingObserver;
private final UserTracker mUserTracker;
private final boolean mDreamSupported;
- private final boolean mDreamOnlyEnabledForSystemUser;
+ private final boolean mDreamOnlyEnabledForDockUser;
private boolean mIsDocked = false;
@@ -100,8 +100,8 @@
BroadcastDispatcher broadcastDispatcher,
UserTracker userTracker,
@Named(DreamModule.DREAM_SUPPORTED) boolean dreamSupported,
- @Named(DreamModule.DREAM_ONLY_ENABLED_FOR_SYSTEM_USER)
- boolean dreamOnlyEnabledForSystemUser
+ @Named(DreamModule.DREAM_ONLY_ENABLED_FOR_DOCK_USER)
+ boolean dreamOnlyEnabledForDockUser
) {
super(host, backgroundLooper, mainHandler, falsingManager, metricsLogger,
statusBarStateController, activityStarter, qsLogger);
@@ -123,7 +123,7 @@
};
mUserTracker = userTracker;
mDreamSupported = dreamSupported;
- mDreamOnlyEnabledForSystemUser = dreamOnlyEnabledForSystemUser;
+ mDreamOnlyEnabledForDockUser = dreamOnlyEnabledForDockUser;
}
@Override
@@ -203,7 +203,8 @@
// For now, restrict to debug users.
return Build.isDebuggable()
&& mDreamSupported
- && (!mDreamOnlyEnabledForSystemUser || mUserTracker.getUserHandle().isSystem());
+ // TODO(b/257333623): Allow the Dock User to be non-SystemUser user in HSUM.
+ && (!mDreamOnlyEnabledForDockUser || mUserTracker.getUserHandle().isSystem());
}
@VisibleForTesting
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
index ae46477..350d8b0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
@@ -387,7 +387,8 @@
@Override
protected void handleUpdateState(SignalState state, Object arg) {
- mQSLogger.logInternetTileUpdate(mLastTileState, arg == null ? "null" : arg.toString());
+ mQSLogger.logInternetTileUpdate(
+ getTileSpec(), mLastTileState, arg == null ? "null" : arg.toString());
if (arg instanceof CellularCallbackInfo) {
mLastTileState = LAST_STATE_CELLULAR;
handleUpdateCellularState(state, arg);
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt
index 5961635..01e32b7a 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt
@@ -32,7 +32,7 @@
import com.android.internal.infra.ServiceConnector
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
import javax.inject.Inject
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
@@ -45,7 +45,7 @@
@Inject
constructor(
@Application private val applicationScope: CoroutineScope,
- @Background private val bgDispatcher: CoroutineDispatcher,
+ @Main private val mainDispatcher: CoroutineDispatcher,
private val context: Context,
) {
/**
@@ -70,23 +70,21 @@
userId: Int,
overrideTransition: Boolean,
) {
- withContext(bgDispatcher) {
- dismissKeyguard()
+ dismissKeyguard()
- if (userId == UserHandle.myUserId()) {
- context.startActivity(intent, bundle)
- } else {
- launchCrossProfileIntent(userId, intent, bundle)
- }
+ if (userId == UserHandle.myUserId()) {
+ withContext(mainDispatcher) { context.startActivity(intent, bundle) }
+ } else {
+ launchCrossProfileIntent(userId, intent, bundle)
+ }
- if (overrideTransition) {
- val runner = RemoteAnimationAdapter(SCREENSHOT_REMOTE_RUNNER, 0, 0)
- try {
- WindowManagerGlobal.getWindowManagerService()
- .overridePendingAppTransitionRemote(runner, Display.DEFAULT_DISPLAY)
- } catch (e: Exception) {
- Log.e(TAG, "Error overriding screenshot app transition", e)
- }
+ if (overrideTransition) {
+ val runner = RemoteAnimationAdapter(SCREENSHOT_REMOTE_RUNNER, 0, 0)
+ try {
+ WindowManagerGlobal.getWindowManagerService()
+ .overridePendingAppTransitionRemote(runner, Display.DEFAULT_DISPLAY)
+ } catch (e: Exception) {
+ Log.e(TAG, "Error overriding screenshot app transition", e)
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java
index 8bf956b..5450db9 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java
@@ -46,6 +46,8 @@
import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.screenshot.ScrollCaptureController.LongScreenshot;
import com.google.common.util.concurrent.ListenableFuture;
@@ -67,6 +69,7 @@
private static final String TAG = LogConfig.logTag(LongScreenshotActivity.class);
public static final String EXTRA_CAPTURE_RESPONSE = "capture-response";
+ public static final String EXTRA_SCREENSHOT_USER_HANDLE = "screenshot-userhandle";
private static final String KEY_SAVED_IMAGE_PATH = "saved-image-path";
private final UiEventLogger mUiEventLogger;
@@ -74,6 +77,8 @@
private final Executor mBackgroundExecutor;
private final ImageExporter mImageExporter;
private final LongScreenshotData mLongScreenshotHolder;
+ private final ActionIntentExecutor mActionExecutor;
+ private final FeatureFlags mFeatureFlags;
private ImageView mPreview;
private ImageView mTransitionView;
@@ -85,6 +90,7 @@
private CropView mCropView;
private MagnifierView mMagnifierView;
private ScrollCaptureResponse mScrollCaptureResponse;
+ private UserHandle mScreenshotUserHandle;
private File mSavedImagePath;
private ListenableFuture<File> mCacheSaveFuture;
@@ -103,12 +109,15 @@
@Inject
public LongScreenshotActivity(UiEventLogger uiEventLogger, ImageExporter imageExporter,
@Main Executor mainExecutor, @Background Executor bgExecutor,
- LongScreenshotData longScreenshotHolder) {
+ LongScreenshotData longScreenshotHolder, ActionIntentExecutor actionExecutor,
+ FeatureFlags featureFlags) {
mUiEventLogger = uiEventLogger;
mUiExecutor = mainExecutor;
mBackgroundExecutor = bgExecutor;
mImageExporter = imageExporter;
mLongScreenshotHolder = longScreenshotHolder;
+ mActionExecutor = actionExecutor;
+ mFeatureFlags = featureFlags;
}
@@ -139,6 +148,11 @@
Intent intent = getIntent();
mScrollCaptureResponse = intent.getParcelableExtra(EXTRA_CAPTURE_RESPONSE);
+ mScreenshotUserHandle = intent.getParcelableExtra(EXTRA_SCREENSHOT_USER_HANDLE,
+ UserHandle.class);
+ if (mScreenshotUserHandle == null) {
+ mScreenshotUserHandle = Process.myUserHandle();
+ }
if (savedInstanceState != null) {
String savedImagePath = savedInstanceState.getString(KEY_SAVED_IMAGE_PATH);
@@ -318,36 +332,51 @@
}
private void doEdit(Uri uri) {
- String editorPackage = getString(R.string.config_screenshotEditor);
- Intent intent = new Intent(Intent.ACTION_EDIT);
- if (!TextUtils.isEmpty(editorPackage)) {
- intent.setComponent(ComponentName.unflattenFromString(editorPackage));
- }
- intent.setDataAndType(uri, "image/png");
- intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
- | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+ if (mFeatureFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY) && mScreenshotUserHandle
+ != Process.myUserHandle()) {
+ // TODO: Fix transition for work profile. Omitting it in the meantime.
+ mActionExecutor.launchIntentAsync(
+ ActionIntentCreator.INSTANCE.createEditIntent(uri, this),
+ null,
+ mScreenshotUserHandle.getIdentifier(), false);
+ } else {
+ String editorPackage = getString(R.string.config_screenshotEditor);
+ Intent intent = new Intent(Intent.ACTION_EDIT);
+ if (!TextUtils.isEmpty(editorPackage)) {
+ intent.setComponent(ComponentName.unflattenFromString(editorPackage));
+ }
+ intent.setDataAndType(uri, "image/png");
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
+ | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
- mTransitionView.setImageBitmap(mOutputBitmap);
- mTransitionView.setVisibility(View.VISIBLE);
- mTransitionView.setTransitionName(
- ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME);
- // TODO: listen for transition completing instead of finishing onStop
- mTransitionStarted = true;
- startActivity(intent,
- ActivityOptions.makeSceneTransitionAnimation(this, mTransitionView,
- ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME).toBundle());
+ mTransitionView.setImageBitmap(mOutputBitmap);
+ mTransitionView.setVisibility(View.VISIBLE);
+ mTransitionView.setTransitionName(
+ ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME);
+ // TODO: listen for transition completing instead of finishing onStop
+ mTransitionStarted = true;
+ startActivity(intent,
+ ActivityOptions.makeSceneTransitionAnimation(this, mTransitionView,
+ ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME).toBundle());
+ }
}
private void doShare(Uri uri) {
- Intent intent = new Intent(Intent.ACTION_SEND);
- intent.setType("image/png");
- intent.putExtra(Intent.EXTRA_STREAM, uri);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK
- | Intent.FLAG_GRANT_READ_URI_PERMISSION);
- Intent sharingChooserIntent = Intent.createChooser(intent, null)
- .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ if (mFeatureFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) {
+ Intent shareIntent = ActionIntentCreator.INSTANCE.createShareIntent(uri, null);
+ mActionExecutor.launchIntentAsync(shareIntent, null,
+ mScreenshotUserHandle.getIdentifier(), false);
+ } else {
+ Intent intent = new Intent(Intent.ACTION_SEND);
+ intent.setType("image/png");
+ intent.putExtra(Intent.EXTRA_STREAM, uri);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK
+ | Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ Intent sharingChooserIntent = Intent.createChooser(intent, null)
+ .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
- startActivityAsUser(sharingChooserIntent, UserHandle.CURRENT);
+ startActivityAsUser(sharingChooserIntent, UserHandle.CURRENT);
+ }
}
private void onClicked(View v) {
@@ -389,8 +418,8 @@
mOutputBitmap = renderBitmap(drawable, bounds);
ListenableFuture<ImageExporter.Result> exportFuture = mImageExporter.export(
mBackgroundExecutor, UUID.randomUUID(), mOutputBitmap, ZonedDateTime.now(),
- // TODO: Owner must match the owner of the captured window.
- Process.myUserHandle());
+ mFeatureFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)
+ ? mScreenshotUserHandle : Process.myUserHandle());
exportFuture.addListener(() -> onExportCompleted(action, exportFuture), mUiExecutor);
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index d395bd3..d94c827 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -591,7 +591,7 @@
// Wait until this window is attached to request because it is
// the reference used to locate the target window (below).
withWindowAttached(() -> {
- requestScrollCapture();
+ requestScrollCapture(owner);
mWindow.peekDecorView().getViewRootImpl().setActivityConfigCallback(
new ViewRootImpl.ActivityConfigCallback() {
@Override
@@ -603,11 +603,11 @@
mScreenshotView.hideScrollChip();
// Delay scroll capture eval a bit to allow the underlying activity
// to set up in the new orientation.
- mScreenshotHandler.postDelayed(
- ScreenshotController.this::requestScrollCapture, 150);
+ mScreenshotHandler.postDelayed(() -> {
+ requestScrollCapture(owner);
+ }, 150);
mScreenshotView.updateInsets(
- mWindowManager.getCurrentWindowMetrics()
- .getWindowInsets());
+ mWindowManager.getCurrentWindowMetrics().getWindowInsets());
// Screenshot animation calculations won't be valid anymore,
// so just end
if (mScreenshotAnimation != null
@@ -655,7 +655,7 @@
mScreenshotHandler.cancelTimeout(); // restarted after animation
}
- private void requestScrollCapture() {
+ private void requestScrollCapture(UserHandle owner) {
if (!allowLongScreenshots()) {
Log.d(TAG, "Long screenshots not supported on this device");
return;
@@ -668,10 +668,11 @@
mScrollCaptureClient.request(DEFAULT_DISPLAY);
mLastScrollCaptureRequest = future;
mLastScrollCaptureRequest.addListener(() ->
- onScrollCaptureResponseReady(future), mMainExecutor);
+ onScrollCaptureResponseReady(future, owner), mMainExecutor);
}
- private void onScrollCaptureResponseReady(Future<ScrollCaptureResponse> responseFuture) {
+ private void onScrollCaptureResponseReady(Future<ScrollCaptureResponse> responseFuture,
+ UserHandle owner) {
try {
if (mLastScrollCaptureResponse != null) {
mLastScrollCaptureResponse.close();
@@ -701,7 +702,7 @@
mScreenshotView.prepareScrollingTransition(response, mScreenBitmap, newScreenshot,
mScreenshotTakenInPortrait);
// delay starting scroll capture to make sure the scrim is up before the app moves
- mScreenshotView.post(() -> runBatchScrollCapture(response));
+ mScreenshotView.post(() -> runBatchScrollCapture(response, owner));
});
} catch (InterruptedException | ExecutionException e) {
Log.e(TAG, "requestScrollCapture failed", e);
@@ -710,7 +711,7 @@
ListenableFuture<ScrollCaptureController.LongScreenshot> mLongScreenshotFuture;
- private void runBatchScrollCapture(ScrollCaptureResponse response) {
+ private void runBatchScrollCapture(ScrollCaptureResponse response, UserHandle owner) {
// Clear the reference to prevent close() in dismissScreenshot
mLastScrollCaptureResponse = null;
@@ -744,6 +745,8 @@
longScreenshot));
final Intent intent = new Intent(mContext, LongScreenshotActivity.class);
+ intent.putExtra(LongScreenshotActivity.EXTRA_SCREENSHOT_USER_HANDLE,
+ owner);
intent.setFlags(
Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java
index 8b5a24c..c891686 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java
@@ -89,7 +89,9 @@
@UiEvent(doc = "User has saved a long screenshot to a file")
SCREENSHOT_LONG_SCREENSHOT_SAVED(910),
@UiEvent(doc = "User has discarded the result of a long screenshot")
- SCREENSHOT_LONG_SCREENSHOT_EXIT(911);
+ SCREENSHOT_LONG_SCREENSHOT_EXIT(911),
+ @UiEvent(doc = "A screenshot has been taken and saved to work profile")
+ SCREENSHOT_SAVED_TO_WORK_PROFILE(1240);
private final int mId;
diff --git a/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt
index 4063af3..954534d 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt
@@ -92,7 +92,6 @@
centerEnd,
ConstraintSet.END
)
- constrainWidth(R.id.statusIcons, 0)
},
qsConstraintsChanges = {
setGuidelineBegin(centerStart, offsetFromEdge)
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java
index e52170e..400b0ba 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java
@@ -16,6 +16,7 @@
package com.android.systemui.shade;
+import static android.os.Trace.TRACE_TAG_ALWAYS;
import static android.view.WindowInsets.Type.systemBars;
import static com.android.systemui.statusbar.phone.CentralSurfaces.DEBUG;
@@ -33,6 +34,7 @@
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
+import android.os.Trace;
import android.util.AttributeSet;
import android.view.ActionMode;
import android.view.DisplayCutout;
@@ -299,6 +301,19 @@
return mode;
}
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ Trace.beginSection("NotificationShadeWindowView#onMeasure");
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ Trace.endSection();
+ }
+
+ @Override
+ public void requestLayout() {
+ Trace.instant(TRACE_TAG_ALWAYS, "NotificationShadeWindowView#requestLayout");
+ super.requestLayout();
+ }
+
private class ActionModeCallback2Wrapper extends ActionMode.Callback2 {
private final ActionMode.Callback mWrapped;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index 0deb47d..15f4b12 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -1179,16 +1179,14 @@
@Override
public void onTrustChanged(int userId) {
- if (getCurrentUser() != userId) {
- return;
- }
+ if (!isCurrentUser(userId)) return;
updateDeviceEntryIndication(false);
}
@Override
- public void showTrustGrantedMessage(CharSequence message) {
- mTrustGrantedIndication = message;
- updateDeviceEntryIndication(false);
+ public void onTrustGrantedWithFlags(int flags, int userId, @Nullable String message) {
+ if (!isCurrentUser(userId)) return;
+ showTrustGrantedMessage(flags, message);
}
@Override
@@ -1248,6 +1246,15 @@
}
}
+ private boolean isCurrentUser(int userId) {
+ return getCurrentUser() == userId;
+ }
+
+ void showTrustGrantedMessage(int flags, @Nullable CharSequence message) {
+ mTrustGrantedIndication = message;
+ updateDeviceEntryIndication(false);
+ }
+
private void handleFaceLockoutError(String errString) {
int followupMsgId = canUnlockWithFingerprint() ? R.string.keyguard_suggest_fingerprint
: R.string.keyguard_unlock;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
index 9d2750f..bc456d5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
@@ -18,6 +18,8 @@
import com.android.systemui.animation.Interpolators
import com.android.systemui.statusbar.LightRevealEffect.Companion.getPercentPastThreshold
import com.android.systemui.util.getColorWithAlpha
+import com.android.systemui.util.leak.RotationUtils
+import com.android.systemui.util.leak.RotationUtils.Rotation
import java.util.function.Consumer
/**
@@ -67,22 +69,19 @@
override fun setRevealAmountOnScrim(amount: Float, scrim: LightRevealScrim) {
val interpolatedAmount = INTERPOLATOR.getInterpolation(amount)
val ovalWidthIncreaseAmount =
- getPercentPastThreshold(interpolatedAmount, WIDEN_OVAL_THRESHOLD)
+ getPercentPastThreshold(interpolatedAmount, WIDEN_OVAL_THRESHOLD)
val initialWidthMultiplier = (1f - OVAL_INITIAL_WIDTH_PERCENT) / 2f
with(scrim) {
- revealGradientEndColorAlpha = 1f - getPercentPastThreshold(
- amount, FADE_END_COLOR_OUT_THRESHOLD)
+ revealGradientEndColorAlpha =
+ 1f - getPercentPastThreshold(amount, FADE_END_COLOR_OUT_THRESHOLD)
setRevealGradientBounds(
- scrim.width * initialWidthMultiplier +
- -scrim.width * ovalWidthIncreaseAmount,
- scrim.height * OVAL_INITIAL_TOP_PERCENT -
- scrim.height * interpolatedAmount,
- scrim.width * (1f - initialWidthMultiplier) +
- scrim.width * ovalWidthIncreaseAmount,
- scrim.height * OVAL_INITIAL_BOTTOM_PERCENT +
- scrim.height * interpolatedAmount)
+ scrim.width * initialWidthMultiplier + -scrim.width * ovalWidthIncreaseAmount,
+ scrim.height * OVAL_INITIAL_TOP_PERCENT - scrim.height * interpolatedAmount,
+ scrim.width * (1f - initialWidthMultiplier) + scrim.width * ovalWidthIncreaseAmount,
+ scrim.height * OVAL_INITIAL_BOTTOM_PERCENT + scrim.height * interpolatedAmount
+ )
}
}
}
@@ -97,12 +96,17 @@
scrim.interpolatedRevealAmount = interpolatedAmount
scrim.startColorAlpha =
- getPercentPastThreshold(1 - interpolatedAmount,
- threshold = 1 - START_COLOR_REVEAL_PERCENTAGE)
+ getPercentPastThreshold(
+ 1 - interpolatedAmount,
+ threshold = 1 - START_COLOR_REVEAL_PERCENTAGE
+ )
scrim.revealGradientEndColorAlpha =
- 1f - getPercentPastThreshold(interpolatedAmount,
- threshold = REVEAL_GRADIENT_END_COLOR_ALPHA_START_PERCENTAGE)
+ 1f -
+ getPercentPastThreshold(
+ interpolatedAmount,
+ threshold = REVEAL_GRADIENT_END_COLOR_ALPHA_START_PERCENTAGE
+ )
// Start changing gradient bounds later to avoid harsh gradient in the beginning
val gradientBoundsAmount = lerp(GRADIENT_START_BOUNDS_PERCENTAGE, 1.0f, interpolatedAmount)
@@ -179,7 +183,7 @@
*/
private val OFF_SCREEN_START_AMOUNT = 0.05f
- private val WIDTH_INCREASE_MULTIPLIER = 1.25f
+ private val INCREASE_MULTIPLIER = 1.25f
override fun setRevealAmountOnScrim(amount: Float, scrim: LightRevealScrim) {
val interpolatedAmount = Interpolators.FAST_OUT_SLOW_IN_REVERSE.getInterpolation(amount)
@@ -188,15 +192,36 @@
with(scrim) {
revealGradientEndColorAlpha = 1f - fadeAmount
interpolatedRevealAmount = interpolatedAmount
- setRevealGradientBounds(
+ @Rotation val rotation = RotationUtils.getRotation(scrim.getContext())
+ if (rotation == RotationUtils.ROTATION_NONE) {
+ setRevealGradientBounds(
width * (1f + OFF_SCREEN_START_AMOUNT) -
- width * WIDTH_INCREASE_MULTIPLIER * interpolatedAmount,
- powerButtonY -
- height * interpolatedAmount,
+ width * INCREASE_MULTIPLIER * interpolatedAmount,
+ powerButtonY - height * interpolatedAmount,
width * (1f + OFF_SCREEN_START_AMOUNT) +
- width * WIDTH_INCREASE_MULTIPLIER * interpolatedAmount,
- powerButtonY +
- height * interpolatedAmount)
+ width * INCREASE_MULTIPLIER * interpolatedAmount,
+ powerButtonY + height * interpolatedAmount
+ )
+ } else if (rotation == RotationUtils.ROTATION_LANDSCAPE) {
+ setRevealGradientBounds(
+ powerButtonY - width * interpolatedAmount,
+ (-height * OFF_SCREEN_START_AMOUNT) -
+ height * INCREASE_MULTIPLIER * interpolatedAmount,
+ powerButtonY + width * interpolatedAmount,
+ (-height * OFF_SCREEN_START_AMOUNT) +
+ height * INCREASE_MULTIPLIER * interpolatedAmount
+ )
+ } else {
+ // RotationUtils.ROTATION_SEASCAPE
+ setRevealGradientBounds(
+ (width - powerButtonY) - width * interpolatedAmount,
+ height * (1f + OFF_SCREEN_START_AMOUNT) -
+ height * INCREASE_MULTIPLIER * interpolatedAmount,
+ (width - powerButtonY) + width * interpolatedAmount,
+ height * (1f + OFF_SCREEN_START_AMOUNT) +
+ height * INCREASE_MULTIPLIER * interpolatedAmount
+ )
+ }
}
}
}
@@ -208,9 +233,7 @@
*/
class LightRevealScrim(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
- /**
- * Listener that is called if the scrim's opaqueness changes
- */
+ /** Listener that is called if the scrim's opaqueness changes */
lateinit var isScrimOpaqueChangedListener: Consumer<Boolean>
/**
@@ -224,8 +247,11 @@
revealEffect.setRevealAmountOnScrim(value, this)
updateScrimOpaque()
- Trace.traceCounter(Trace.TRACE_TAG_APP, "light_reveal_amount",
- (field * 100).toInt())
+ Trace.traceCounter(
+ Trace.TRACE_TAG_APP,
+ "light_reveal_amount",
+ (field * 100).toInt()
+ )
invalidate()
}
}
@@ -250,10 +276,10 @@
/**
* 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 smoother. It can help to add fade in effect in the beginning of the animation.
- * The color of the fill is determined by [revealGradientEndColor].
+ * 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
+ * smoother. It can help to add fade in effect in the beginning of the animation. The color of
+ * the fill is determined by [revealGradientEndColor].
*
* 0 - no fill and content is visible, 1 - the content is covered with the start color
*/
@@ -281,9 +307,7 @@
}
}
- /**
- * Is the scrim currently fully opaque
- */
+ /** Is the scrim currently fully opaque */
var isScrimOpaque = false
private set(value) {
if (field != value) {
@@ -318,16 +342,22 @@
* Paint used to draw a transparent-to-white radial gradient. This will be scaled and translated
* via local matrix in [onDraw] so we never need to construct a new shader.
*/
- private val gradientPaint = Paint().apply {
- shader = RadialGradient(
- 0f, 0f, 1f,
- intArrayOf(Color.TRANSPARENT, Color.WHITE), floatArrayOf(0f, 1f),
- Shader.TileMode.CLAMP)
+ private val gradientPaint =
+ Paint().apply {
+ shader =
+ RadialGradient(
+ 0f,
+ 0f,
+ 1f,
+ intArrayOf(Color.TRANSPARENT, Color.WHITE),
+ floatArrayOf(0f, 1f),
+ Shader.TileMode.CLAMP
+ )
- // SRC_OVER ensures that we draw the semitransparent pixels over other views in the same
- // window, rather than outright replacing them.
- xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_OVER)
- }
+ // SRC_OVER ensures that we draw the semitransparent pixels over other views in the same
+ // window, rather than outright replacing them.
+ xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_OVER)
+ }
/**
* Matrix applied to [gradientPaint]'s RadialGradient shader to move the gradient to
@@ -347,8 +377,8 @@
* simply a helper method that sets [revealGradientCenter], [revealGradientWidth], and
* [revealGradientHeight] for you.
*
- * This method does not call [invalidate] - you should do so once you're done changing
- * properties.
+ * This method does not call [invalidate]
+ * - you should do so once you're done changing properties.
*/
fun setRevealGradientBounds(left: Float, top: Float, right: Float, bottom: Float) {
revealGradientWidth = right - left
@@ -359,8 +389,12 @@
}
override fun onDraw(canvas: Canvas?) {
- if (canvas == null || revealGradientWidth <= 0 || revealGradientHeight <= 0 ||
- revealAmount == 0f) {
+ if (
+ canvas == null ||
+ revealGradientWidth <= 0 ||
+ revealGradientHeight <= 0 ||
+ revealAmount == 0f
+ ) {
if (revealAmount < 1f) {
canvas?.drawColor(revealGradientEndColor)
}
@@ -383,8 +417,10 @@
}
private fun setPaintColorFilter() {
- gradientPaint.colorFilter = PorterDuffColorFilter(
- getColorWithAlpha(revealGradientEndColor, revealGradientEndColorAlpha),
- PorterDuff.Mode.MULTIPLY)
+ gradientPaint.colorFilter =
+ PorterDuffColorFilter(
+ getColorWithAlpha(revealGradientEndColor, revealGradientEndColorAlpha),
+ PorterDuff.Mode.MULTIPLY
+ )
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index 3021414..b93e150 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -135,6 +135,8 @@
private static final String TAG = "ExpandableNotifRow";
private static final boolean DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG);
+ private static final boolean DEBUG_ONMEASURE =
+ Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE);
private static final int DEFAULT_DIVIDER_ALPHA = 0x29;
private static final int COLORED_DIVIDER_ALPHA = 0x7B;
private static final int MENU_VIEW_INDEX = 0;
@@ -1724,6 +1726,11 @@
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Trace.beginSection(appendTraceStyleTag("ExpNotRow#onMeasure"));
+ if (DEBUG_ONMEASURE) {
+ Log.d(TAG, "onMeasure("
+ + "widthMeasureSpec=" + MeasureSpec.toString(widthMeasureSpec) + ", "
+ + "heightMeasureSpec=" + MeasureSpec.toString(heightMeasureSpec) + ")");
+ }
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Trace.endSection();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
index 645a02d..d43ca823 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
@@ -25,6 +25,7 @@
import android.graphics.Path;
import android.graphics.Path.Direction;
import android.graphics.drawable.ColorDrawable;
+import android.os.Trace;
import android.service.notification.StatusBarNotification;
import android.util.AttributeSet;
import android.util.Log;
@@ -219,6 +220,7 @@
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ Trace.beginSection("NotificationChildrenContainer#onMeasure");
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY;
boolean isHeightLimited = heightMode == MeasureSpec.AT_MOST;
@@ -267,6 +269,7 @@
}
setMeasuredDimension(width, height);
+ Trace.endSection();
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 2c3330e..41dbf1d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -16,6 +16,8 @@
package com.android.systemui.statusbar.notification.stack;
+import static android.os.Trace.TRACE_TAG_ALWAYS;
+
import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_SCROLL_FLING;
import static com.android.internal.jank.InteractionJankMonitor.CUJ_SHADE_CLEAR_ALL;
import static com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt.BUCKET_SILENT;
@@ -44,6 +46,7 @@
import android.graphics.Path;
import android.graphics.Rect;
import android.os.Bundle;
+import android.os.Trace;
import android.provider.Settings;
import android.util.AttributeSet;
import android.util.IndentingPrintWriter;
@@ -1074,6 +1077,12 @@
@Override
@ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ Trace.beginSection("NotificationStackScrollLayout#onMeasure");
+ if (SPEW) {
+ Log.d(TAG, "onMeasure("
+ + "widthMeasureSpec=" + MeasureSpec.toString(widthMeasureSpec) + ", "
+ + "heightMeasureSpec=" + MeasureSpec.toString(heightMeasureSpec) + ")");
+ }
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
@@ -1090,6 +1099,13 @@
for (int i = 0; i < size; i++) {
measureChild(getChildAt(i), childWidthSpec, childHeightSpec);
}
+ Trace.endSection();
+ }
+
+ @Override
+ public void requestLayout() {
+ Trace.instant(TRACE_TAG_ALWAYS, "NotificationStackScrollLayout#requestLayout");
+ super.requestLayout();
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
index 18877f9..7a49a49 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
@@ -26,6 +26,7 @@
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
+import android.os.Trace;
import android.util.AttributeSet;
import android.util.Pair;
import android.util.TypedValue;
@@ -527,4 +528,11 @@
mClipRect.set(0, mTopClipping, getWidth(), getHeight());
setClipBounds(mClipRect);
}
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ Trace.beginSection("KeyguardStatusBarView#onMeasure");
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ Trace.endSection();
+ }
}
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 681d21b..9e075e9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -469,7 +469,7 @@
// Don't expand to the bouncer. Instead transition back to the lock screen (see
// CentralSurfaces#showBouncerOrLockScreenIfKeyguard)
return;
- } else if (primaryBouncerNeedsScrimming()) {
+ } else if (needsFullscreenBouncer()) {
if (mPrimaryBouncer != null) {
mPrimaryBouncer.setExpansion(KeyguardBouncer.EXPANSION_VISIBLE);
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
index 0369845..2d580ab 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
@@ -225,6 +225,7 @@
BroadcastDispatcher broadcastDispatcher,
@Main Handler mainHandler,
ContentResolver contentResolver,
+ FeatureFlags featureFlags,
BatteryController batteryController
) {
return new BatteryMeterViewController(
@@ -234,6 +235,7 @@
broadcastDispatcher,
mainHandler,
contentResolver,
+ featureFlags,
batteryController);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java
index 149ed0a..d10d7cf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java
@@ -155,6 +155,9 @@
default void onWirelessChargingChanged(boolean isWirlessCharging) {
}
+
+ default void onIsOverheatedChanged(boolean isOverheated) {
+ }
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
index c7ad767..3c2ac7b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
@@ -16,6 +16,9 @@
package com.android.systemui.statusbar.policy;
+import static android.os.BatteryManager.BATTERY_HEALTH_OVERHEAT;
+import static android.os.BatteryManager.BATTERY_HEALTH_UNKNOWN;
+import static android.os.BatteryManager.EXTRA_HEALTH;
import static android.os.BatteryManager.EXTRA_PRESENT;
import android.annotation.WorkerThread;
@@ -87,6 +90,7 @@
protected boolean mPowerSave;
private boolean mAodPowerSave;
private boolean mWirelessCharging;
+ private boolean mIsOverheated = false;
private boolean mTestMode = false;
@VisibleForTesting
boolean mHasReceivedBattery = false;
@@ -184,6 +188,7 @@
cb.onPowerSaveChanged(mPowerSave);
cb.onBatteryUnknownStateChanged(mStateUnknown);
cb.onWirelessChargingChanged(mWirelessCharging);
+ cb.onIsOverheatedChanged(mIsOverheated);
}
@Override
@@ -222,6 +227,13 @@
fireBatteryUnknownStateChanged();
}
+ int batteryHealth = intent.getIntExtra(EXTRA_HEALTH, BATTERY_HEALTH_UNKNOWN);
+ boolean isOverheated = batteryHealth == BATTERY_HEALTH_OVERHEAT;
+ if (isOverheated != mIsOverheated) {
+ mIsOverheated = isOverheated;
+ fireIsOverheatedChanged();
+ }
+
fireBatteryLevelChanged();
} else if (action.equals(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED)) {
updatePowerSave();
@@ -292,6 +304,10 @@
return mPluggedChargingSource == BatteryManager.BATTERY_PLUGGED_WIRELESS;
}
+ public boolean isOverheated() {
+ return mIsOverheated;
+ }
+
@Override
public void getEstimatedTimeRemainingString(EstimateFetchCompletion completion) {
// Need to fetch or refresh the estimate, but it may involve binder calls so offload the
@@ -402,6 +418,15 @@
}
}
+ private void fireIsOverheatedChanged() {
+ synchronized (mChangeCallbacks) {
+ final int n = mChangeCallbacks.size();
+ for (int i = 0; i < n; i++) {
+ mChangeCallbacks.get(i).onIsOverheatedChanged(mIsOverheated);
+ }
+ }
+ }
+
@Override
public void dispatchDemoCommand(String command, Bundle args) {
if (!mDemoModeController.isInDemoMode()) {
@@ -412,6 +437,7 @@
String plugged = args.getString("plugged");
String powerSave = args.getString("powersave");
String present = args.getString("present");
+ String overheated = args.getString("overheated");
if (level != null) {
mLevel = Math.min(Math.max(Integer.parseInt(level), 0), 100);
}
@@ -426,6 +452,10 @@
mStateUnknown = !present.equals("true");
fireBatteryUnknownStateChanged();
}
+ if (overheated != null) {
+ mIsOverheated = overheated.equals("true");
+ fireIsOverheatedChanged();
+ }
fireBatteryLevelChanged();
}
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
index 8270336..a9d05d1 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
@@ -94,6 +94,13 @@
private var wakeReasonAcquired: String? = null
/**
+ * A stack of pairs of device id and temporary view info. This is used when there may be
+ * multiple devices in range, and we want to always display the chip for the most recently
+ * active device.
+ */
+ internal val activeViews: ArrayDeque<Pair<String, T>> = ArrayDeque()
+
+ /**
* Displays the view with the provided [newInfo].
*
* This method handles inflating and attaching the view, then delegates to [updateView] to
@@ -102,6 +109,12 @@
fun displayView(newInfo: T) {
val currentDisplayInfo = displayInfo
+ // Update our list of active devices by removing it if necessary, then adding back at the
+ // front of the list
+ val id = newInfo.id
+ val position = findAndRemoveFromActiveViewsList(id)
+ activeViews.addFirst(Pair(id, newInfo))
+
if (currentDisplayInfo != null &&
currentDisplayInfo.info.windowTitle == newInfo.windowTitle) {
// We're already displaying information in the correctly-titled window, so we just need
@@ -113,7 +126,10 @@
// We're already displaying information but that information is under a different
// window title. So, we need to remove the old window with the old title and add a
// new window with the new title.
- removeView(removalReason = "New info has new window title: ${newInfo.windowTitle}")
+ removeView(
+ id,
+ removalReason = "New info has new window title: ${newInfo.windowTitle}"
+ )
}
// At this point, we're guaranteed to no longer be displaying a view.
@@ -140,7 +156,7 @@
}
wakeLock?.acquire(newInfo.wakeReason)
wakeReasonAcquired = newInfo.wakeReason
- logger.logViewAddition(newInfo.windowTitle)
+ logger.logViewAddition(id, newInfo.windowTitle)
inflateAndUpdateView(newInfo)
}
@@ -151,9 +167,13 @@
// include it just to be safe.
FLAG_CONTENT_ICONS or FLAG_CONTENT_TEXT or FLAG_CONTENT_CONTROLS
)
- cancelViewTimeout?.run()
+
+ // Only cancel timeout of the most recent view displayed, as it will be reset.
+ if (position == 0) {
+ cancelViewTimeout?.run()
+ }
cancelViewTimeout = mainExecutor.executeDelayed(
- { removeView(REMOVAL_REASON_TIMEOUT) },
+ { removeView(id, REMOVAL_REASON_TIMEOUT) },
timeout.toLong()
)
}
@@ -196,28 +216,67 @@
}
/**
- * Hides the view.
+ * Hides the view given its [id].
*
+ * @param id the id of the device responsible of displaying the temp view.
* @param removalReason a short string describing why the view was removed (timeout, state
* change, etc.)
*/
- fun removeView(removalReason: String) {
+ fun removeView(id: String, removalReason: String) {
val currentDisplayInfo = displayInfo ?: return
+ val removalPosition = findAndRemoveFromActiveViewsList(id)
+ if (removalPosition == null) {
+ logger.logViewRemovalIgnored(id, "view not found in the list")
+ return
+ }
+ if (removalPosition != 0) {
+ logger.logViewRemovalIgnored(id, "most recent view is being displayed.")
+ return
+ }
+ logger.logViewRemoval(id, removalReason)
+
+ val newViewToDisplay = if (activeViews.isEmpty()) {
+ null
+ } else {
+ activeViews[0].second
+ }
+
val currentView = currentDisplayInfo.view
animateViewOut(currentView) {
windowManager.removeView(currentView)
wakeLock?.release(wakeReasonAcquired)
}
- logger.logViewRemoval(removalReason)
configurationController.removeCallback(displayScaleListener)
// Re-set to null immediately (instead as part of the animation end runnable) so
- // that if a new view event comes in while this view is animating out, we still display the
- // new view appropriately.
+ // that if a new view event comes in while this view is animating out, we still display
+ // the new view appropriately.
displayInfo = null
// No need to time the view out since it's already gone
cancelViewTimeout?.run()
+
+ if (newViewToDisplay != null) {
+ mainExecutor.executeDelayed({ displayView(newViewToDisplay)}, DISPLAY_VIEW_DELAY)
+ }
+ }
+
+ /**
+ * Finds and removes the active view with the given [id] from the stack, or null if there is no
+ * active view with that ID
+ *
+ * @param id that temporary view belonged to.
+ *
+ * @return index of the view in the stack , otherwise null.
+ */
+ private fun findAndRemoveFromActiveViewsList(id: String): Int? {
+ for (i in 0 until activeViews.size) {
+ if (activeViews[i].first == id) {
+ activeViews.removeAt(i)
+ return i
+ }
+ }
+ return null
}
/**
@@ -258,6 +317,7 @@
}
private const val REMOVAL_REASON_TIMEOUT = "TIMEOUT"
+const val DISPLAY_VIEW_DELAY = 50L
private data class IconInfo(
val iconName: String,
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewInfo.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewInfo.kt
index cbb5002..df83960 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewInfo.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewInfo.kt
@@ -37,6 +37,11 @@
* disappears.
*/
open val timeoutMs: Int = DEFAULT_TIMEOUT_MILLIS
+
+ /**
+ * The id of the temporary view.
+ */
+ abstract val id: String
}
const val DEFAULT_TIMEOUT_MILLIS = 10000
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt
index 428a104..133a384 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt
@@ -24,13 +24,42 @@
internal val buffer: LogBuffer,
internal val tag: String,
) {
- /** Logs that we added the view in a window titled [windowTitle]. */
- fun logViewAddition(windowTitle: String) {
- buffer.log(tag, LogLevel.DEBUG, { str1 = windowTitle }, { "View added. window=$str1" })
+ /** Logs that we added the view with the given [id] in a window titled [windowTitle]. */
+ fun logViewAddition(id: String, windowTitle: String) {
+ buffer.log(
+ tag,
+ LogLevel.DEBUG,
+ {
+ str1 = windowTitle
+ str2 = id
+ },
+ { "View added. window=$str1 id=$str2" }
+ )
}
- /** Logs that we removed the chip for the given [reason]. */
- fun logViewRemoval(reason: String) {
- buffer.log(tag, LogLevel.DEBUG, { str1 = reason }, { "View removed due to: $str1" })
+ /** Logs that we removed the view with the given [id] for the given [reason]. */
+ fun logViewRemoval(id: String, reason: String) {
+ buffer.log(
+ tag,
+ LogLevel.DEBUG,
+ {
+ str1 = reason
+ str2 = id
+ },
+ { "View with id=$str2 is removed due to: $str1" }
+ )
+ }
+
+ /** Logs that we ignored removal of the view with the given [id]. */
+ fun logViewRemovalIgnored(id: String, reason: String) {
+ buffer.log(
+ tag,
+ LogLevel.DEBUG,
+ {
+ str1 = reason
+ str2 = id
+ },
+ { "Removal of view with id=$str2 is ignored because $str1" }
+ )
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt
index 6237365..b92e0ec 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt
@@ -40,6 +40,7 @@
override val windowTitle: String,
override val wakeReason: String,
override val timeoutMs: Int,
+ override val id: String,
) : TemporaryViewInfo()
/** The possible items to display at the end of the chipbar. */
diff --git a/packages/SystemUI/tests/AndroidManifest.xml b/packages/SystemUI/tests/AndroidManifest.xml
index f92a1ba..4891339 100644
--- a/packages/SystemUI/tests/AndroidManifest.xml
+++ b/packages/SystemUI/tests/AndroidManifest.xml
@@ -140,6 +140,12 @@
tools:replace="android:authorities"
tools:node="remove" />
+ <provider android:name="com.android.systemui.keyguard.KeyguardQuickAffordanceProvider"
+ android:authorities="com.android.systemui.test.keyguard.quickaffordance.disabled"
+ android:enabled="false"
+ tools:replace="android:authorities"
+ tools:node="remove" />
+
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.android.systemui.test.fileprovider"
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
index aa4469f..4d58b09 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
@@ -548,6 +548,22 @@
verify(mKeyguardPasswordViewControllerMock, never()).showMessage(null, null);
}
+ @Test
+ public void onDensityorFontScaleChanged() {
+ ArgumentCaptor<ConfigurationController.ConfigurationListener>
+ configurationListenerArgumentCaptor = ArgumentCaptor.forClass(
+ ConfigurationController.ConfigurationListener.class);
+ mKeyguardSecurityContainerController.onViewAttached();
+ verify(mConfigurationController).addCallback(configurationListenerArgumentCaptor.capture());
+ configurationListenerArgumentCaptor.getValue().onDensityOrFontScaleChanged();
+
+ verify(mView).onDensityOrFontScaleChanged();
+ verify(mKeyguardSecurityViewFlipperController).onDensityOrFontScaleChanged();
+ verify(mKeyguardSecurityViewFlipperController).getSecurityView(any(SecurityMode.class),
+ any(KeyguardSecurityCallback.class));
+ }
+
+
private KeyguardSecurityContainer.SwipeListener getRegisteredSwipeListener() {
mKeyguardSecurityContainerController.onViewAttached();
verify(mView).setSwipeListener(mSwipeListenerArgumentCaptor.capture());
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
index 1d2b09c..36ed669 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
@@ -311,6 +311,17 @@
}
@Test
+ public void testOnDensityOrFontScaleChanged() {
+ setupUserSwitcher();
+ View oldUserSwitcher = mKeyguardSecurityContainer.findViewById(
+ R.id.keyguard_bouncer_user_switcher);
+ mKeyguardSecurityContainer.onDensityOrFontScaleChanged();
+ View newUserSwitcher = mKeyguardSecurityContainer.findViewById(
+ R.id.keyguard_bouncer_user_switcher);
+ assertThat(oldUserSwitcher).isNotEqualTo(newUserSwitcher);
+ }
+
+ @Test
public void testTouchesAreRecognizedAsBeingOnTheOtherSideOfSecurity() {
setupUserSwitcher();
setViewWidth(VIEW_WIDTH);
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java
index 9296d3d..fd02ac9 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java
@@ -106,4 +106,10 @@
}
}
}
+
+ @Test
+ public void onDensityOrFontScaleChanged() {
+ mKeyguardSecurityViewFlipperController.onDensityOrFontScaleChanged();
+ verify(mView).removeAllViews();
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
index 6d043c5..27094c0 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
@@ -1313,7 +1313,10 @@
Arrays.asList("Unlocked by wearable"));
// THEN the showTrustGrantedMessage should be called with the first message
- verify(mTestCallback).showTrustGrantedMessage("Unlocked by wearable");
+ verify(mTestCallback).onTrustGrantedWithFlags(
+ eq(0),
+ eq(KeyguardUpdateMonitor.getCurrentUser()),
+ eq("Unlocked by wearable"));
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/battery/AccessorizedBatteryDrawableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/battery/AccessorizedBatteryDrawableTest.kt
new file mode 100644
index 0000000..982f033
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/battery/AccessorizedBatteryDrawableTest.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.battery
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.battery.BatterySpecs.BATTERY_HEIGHT
+import com.android.systemui.battery.BatterySpecs.BATTERY_HEIGHT_WITH_SHIELD
+import com.android.systemui.battery.BatterySpecs.BATTERY_WIDTH
+import com.android.systemui.battery.BatterySpecs.BATTERY_WIDTH_WITH_SHIELD
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+@SmallTest
+class AccessorizedBatteryDrawableTest : SysuiTestCase() {
+ @Test
+ fun intrinsicSize_shieldFalse_isBatterySize() {
+ val drawable = AccessorizedBatteryDrawable(context, frameColor = 0)
+ drawable.displayShield = false
+
+ val density = context.resources.displayMetrics.density
+ assertThat(drawable.intrinsicHeight).isEqualTo((BATTERY_HEIGHT * density).toInt())
+ assertThat(drawable.intrinsicWidth).isEqualTo((BATTERY_WIDTH * density).toInt())
+ }
+
+ @Test
+ fun intrinsicSize_shieldTrue_isBatteryPlusShieldSize() {
+ val drawable = AccessorizedBatteryDrawable(context, frameColor = 0)
+ drawable.displayShield = true
+
+ val density = context.resources.displayMetrics.density
+ assertThat(drawable.intrinsicHeight)
+ .isEqualTo((BATTERY_HEIGHT_WITH_SHIELD * density).toInt())
+ assertThat(drawable.intrinsicWidth).isEqualTo((BATTERY_WIDTH_WITH_SHIELD * density).toInt())
+ }
+
+ // TODO(b/255625888): Screenshot tests for this drawable would be amazing!
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java
index 1d038a4..bc8f961 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java
@@ -35,6 +35,8 @@
import com.android.systemui.SysuiTestCase;
import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.flags.FakeFeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.statusbar.policy.BatteryController;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.tuner.TunerService;
@@ -59,6 +61,7 @@
private Handler mHandler;
@Mock
private ContentResolver mContentResolver;
+ private FakeFeatureFlags mFeatureFlags;
@Mock
private BatteryController mBatteryController;
@@ -71,19 +74,13 @@
when(mBatteryMeterView.getContext()).thenReturn(mContext);
when(mBatteryMeterView.getResources()).thenReturn(mContext.getResources());
- mController = new BatteryMeterViewController(
- mBatteryMeterView,
- mConfigurationController,
- mTunerService,
- mBroadcastDispatcher,
- mHandler,
- mContentResolver,
- mBatteryController
- );
+ mFeatureFlags = new FakeFeatureFlags();
+ mFeatureFlags.set(Flags.BATTERY_SHIELD_ICON, false);
}
@Test
public void onViewAttached_callbacksRegistered() {
+ initController();
mController.onViewAttached();
verify(mConfigurationController).addCallback(any());
@@ -101,6 +98,7 @@
@Test
public void onViewDetached_callbacksUnregistered() {
+ initController();
// Set everything up first.
mController.onViewAttached();
@@ -114,6 +112,7 @@
@Test
public void ignoreTunerUpdates_afterOnViewAttached_callbackUnregistered() {
+ initController();
// Start out receiving tuner updates
mController.onViewAttached();
@@ -124,10 +123,43 @@
@Test
public void ignoreTunerUpdates_beforeOnViewAttached_callbackNeverRegistered() {
+ initController();
+
mController.ignoreTunerUpdates();
mController.onViewAttached();
verify(mTunerService, never()).addTunable(any(), any());
}
+
+ @Test
+ public void shieldFlagDisabled_viewNotified() {
+ mFeatureFlags.set(Flags.BATTERY_SHIELD_ICON, false);
+
+ initController();
+
+ verify(mBatteryMeterView).setDisplayShieldEnabled(false);
+ }
+
+ @Test
+ public void shieldFlagEnabled_viewNotified() {
+ mFeatureFlags.set(Flags.BATTERY_SHIELD_ICON, true);
+
+ initController();
+
+ verify(mBatteryMeterView).setDisplayShieldEnabled(true);
+ }
+
+ private void initController() {
+ mController = new BatteryMeterViewController(
+ mBatteryMeterView,
+ mConfigurationController,
+ mTunerService,
+ mBroadcastDispatcher,
+ mHandler,
+ mContentResolver,
+ mFeatureFlags,
+ mBatteryController
+ );
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt
index b4ff2a5..eb7d9c3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt
@@ -17,7 +17,9 @@
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper
+import android.widget.ImageView
import androidx.test.filters.SmallTest
+import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.battery.BatteryMeterView.BatteryEstimateFetcher
import com.android.systemui.statusbar.policy.BatteryController.EstimateFetchCompletion
@@ -58,6 +60,182 @@
// No assert needed
}
+ @Test
+ fun contentDescription_unknown() {
+ mBatteryMeterView.onBatteryUnknownStateChanged(true)
+
+ assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+ context.getString(R.string.accessibility_battery_unknown)
+ )
+ }
+
+ @Test
+ fun contentDescription_estimate() {
+ mBatteryMeterView.onBatteryLevelChanged(15, false)
+ mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE)
+ mBatteryMeterView.setBatteryEstimateFetcher(Fetcher())
+
+ mBatteryMeterView.updatePercentText()
+
+ assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+ context.getString(
+ R.string.accessibility_battery_level_with_estimate, 15, ESTIMATE
+ )
+ )
+ }
+
+ @Test
+ fun contentDescription_estimateAndOverheated() {
+ mBatteryMeterView.onBatteryLevelChanged(17, false)
+ mBatteryMeterView.onIsOverheatedChanged(true)
+ mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE)
+ mBatteryMeterView.setBatteryEstimateFetcher(Fetcher())
+
+ mBatteryMeterView.updatePercentText()
+
+ assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+ context.getString(
+ R.string.accessibility_battery_level_charging_paused_with_estimate,
+ 17,
+ ESTIMATE,
+ )
+ )
+ }
+
+ @Test
+ fun contentDescription_overheated() {
+ mBatteryMeterView.onBatteryLevelChanged(90, false)
+ mBatteryMeterView.onIsOverheatedChanged(true)
+
+ assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+ context.getString(R.string.accessibility_battery_level_charging_paused, 90)
+ )
+ }
+
+ @Test
+ fun contentDescription_charging() {
+ mBatteryMeterView.onBatteryLevelChanged(45, true)
+
+ assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+ context.getString(R.string.accessibility_battery_level_charging, 45)
+ )
+ }
+
+ @Test
+ fun contentDescription_notCharging() {
+ mBatteryMeterView.onBatteryLevelChanged(45, false)
+
+ assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+ context.getString(R.string.accessibility_battery_level, 45)
+ )
+ }
+
+ @Test
+ fun changesFromEstimateToPercent_textAndContentDescriptionChanges() {
+ mBatteryMeterView.onBatteryLevelChanged(15, false)
+ mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE)
+ mBatteryMeterView.setBatteryEstimateFetcher(Fetcher())
+
+ mBatteryMeterView.updatePercentText()
+
+ assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+ context.getString(
+ R.string.accessibility_battery_level_with_estimate, 15, ESTIMATE
+ )
+ )
+
+ // Update the show mode from estimate to percent
+ mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ON)
+
+ assertThat(mBatteryMeterView.batteryPercentViewText).isEqualTo("15%")
+ assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+ context.getString(R.string.accessibility_battery_level, 15)
+ )
+ }
+
+ @Test
+ fun contentDescription_manyUpdates_alwaysUpdated() {
+ // Overheated
+ mBatteryMeterView.onBatteryLevelChanged(90, false)
+ mBatteryMeterView.onIsOverheatedChanged(true)
+ assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+ context.getString(R.string.accessibility_battery_level_charging_paused, 90)
+ )
+
+ // Overheated & estimate
+ mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE)
+ mBatteryMeterView.setBatteryEstimateFetcher(Fetcher())
+ mBatteryMeterView.updatePercentText()
+ assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+ context.getString(
+ R.string.accessibility_battery_level_charging_paused_with_estimate,
+ 90,
+ ESTIMATE,
+ )
+ )
+
+ // Just estimate
+ mBatteryMeterView.onIsOverheatedChanged(false)
+ assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+ context.getString(
+ R.string.accessibility_battery_level_with_estimate,
+ 90,
+ ESTIMATE,
+ )
+ )
+
+ // Just percent
+ mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ON)
+ assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+ context.getString(R.string.accessibility_battery_level, 90)
+ )
+
+ // Charging
+ mBatteryMeterView.onBatteryLevelChanged(90, true)
+ assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+ context.getString(R.string.accessibility_battery_level_charging, 90)
+ )
+ }
+
+ @Test
+ fun isOverheatedChanged_true_drawableGetsTrue() {
+ mBatteryMeterView.setDisplayShieldEnabled(true)
+ val drawable = getBatteryDrawable()
+
+ mBatteryMeterView.onIsOverheatedChanged(true)
+
+ assertThat(drawable.displayShield).isTrue()
+ }
+
+ @Test
+ fun isOverheatedChanged_false_drawableGetsFalse() {
+ mBatteryMeterView.setDisplayShieldEnabled(true)
+ val drawable = getBatteryDrawable()
+
+ // Start as true
+ mBatteryMeterView.onIsOverheatedChanged(true)
+
+ // Update to false
+ mBatteryMeterView.onIsOverheatedChanged(false)
+
+ assertThat(drawable.displayShield).isFalse()
+ }
+
+ @Test
+ fun isOverheatedChanged_true_featureflagOff_drawableGetsFalse() {
+ mBatteryMeterView.setDisplayShieldEnabled(false)
+ val drawable = getBatteryDrawable()
+
+ mBatteryMeterView.onIsOverheatedChanged(true)
+
+ assertThat(drawable.displayShield).isFalse()
+ }
+
+ private fun getBatteryDrawable(): AccessorizedBatteryDrawable {
+ return (mBatteryMeterView.getChildAt(0) as ImageView)
+ .drawable as AccessorizedBatteryDrawable
+ }
+
private class Fetcher : BatteryEstimateFetcher {
override fun fetchBatteryTimeRemainingEstimate(
completion: EstimateFetchCompletion) {
@@ -68,4 +246,4 @@
private companion object {
const val ESTIMATE = "2 hours 2 minutes"
}
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/battery/BatterySpecsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/battery/BatterySpecsTest.kt
new file mode 100644
index 0000000..39cb047
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/battery/BatterySpecsTest.kt
@@ -0,0 +1,101 @@
+/*
+ * 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.battery
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.battery.BatterySpecs.BATTERY_HEIGHT
+import com.android.systemui.battery.BatterySpecs.BATTERY_HEIGHT_WITH_SHIELD
+import com.android.systemui.battery.BatterySpecs.BATTERY_WIDTH
+import com.android.systemui.battery.BatterySpecs.BATTERY_WIDTH_WITH_SHIELD
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+@SmallTest
+class BatterySpecsTest : SysuiTestCase() {
+ @Test
+ fun getFullBatteryHeight_shieldFalse_returnsMainHeight() {
+ val fullHeight = BatterySpecs.getFullBatteryHeight(56f, displayShield = false)
+
+ assertThat(fullHeight).isEqualTo(56f)
+ }
+
+ @Test
+ fun getFullBatteryHeight_shieldTrue_returnsMainHeightPlusShield() {
+ val mainHeight = BATTERY_HEIGHT * 5
+ val fullHeight = BatterySpecs.getFullBatteryHeight(mainHeight, displayShield = true)
+
+ // Since the main battery was scaled 5x, the output height should also be scaled 5x
+ val expectedFullHeight = BATTERY_HEIGHT_WITH_SHIELD * 5
+
+ assertThat(fullHeight).isWithin(.0001f).of(expectedFullHeight)
+ }
+
+ @Test
+ fun getFullBatteryWidth_shieldFalse_returnsMainWidth() {
+ val fullWidth = BatterySpecs.getFullBatteryWidth(33f, displayShield = false)
+
+ assertThat(fullWidth).isEqualTo(33f)
+ }
+
+ @Test
+ fun getFullBatteryWidth_shieldTrue_returnsMainWidthPlusShield() {
+ val mainWidth = BATTERY_WIDTH * 3.3f
+
+ val fullWidth = BatterySpecs.getFullBatteryWidth(mainWidth, displayShield = true)
+
+ // Since the main battery was scaled 3.3x, the output width should also be scaled 5x
+ val expectedFullWidth = BATTERY_WIDTH_WITH_SHIELD * 3.3f
+ assertThat(fullWidth).isWithin(.0001f).of(expectedFullWidth)
+ }
+
+ @Test
+ fun getMainBatteryHeight_shieldFalse_returnsFullHeight() {
+ val mainHeight = BatterySpecs.getMainBatteryHeight(89f, displayShield = false)
+
+ assertThat(mainHeight).isEqualTo(89f)
+ }
+
+ @Test
+ fun getMainBatteryHeight_shieldTrue_returnsNotFullHeight() {
+ val fullHeight = BATTERY_HEIGHT_WITH_SHIELD * 7.7f
+
+ val mainHeight = BatterySpecs.getMainBatteryHeight(fullHeight, displayShield = true)
+
+ // Since the full height was scaled 7.7x, the main height should also be scaled 7.7x.
+ val expectedHeight = BATTERY_HEIGHT * 7.7f
+ assertThat(mainHeight).isWithin(.0001f).of(expectedHeight)
+ }
+
+ @Test
+ fun getMainBatteryWidth_shieldFalse_returnsFullWidth() {
+ val mainWidth = BatterySpecs.getMainBatteryWidth(2345f, displayShield = false)
+
+ assertThat(mainWidth).isEqualTo(2345f)
+ }
+
+ @Test
+ fun getMainBatteryWidth_shieldTrue_returnsNotFullWidth() {
+ val fullWidth = BATTERY_WIDTH_WITH_SHIELD * 0.6f
+
+ val mainWidth = BatterySpecs.getMainBatteryWidth(fullWidth, displayShield = true)
+
+ // Since the full width was scaled 0.6x, the main height should also be scaled 0.6x.
+ val expectedWidth = BATTERY_WIDTH * 0.6f
+ assertThat(mainWidth).isWithin(.0001f).of(expectedWidth)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutEngineTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutEngineTest.java
index 849ac5e..7a2ba95 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutEngineTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutEngineTest.java
@@ -347,21 +347,22 @@
addComplication(engine, thirdViewInfo);
- // The first added view should now be underneath the second view.
+ // The first added view should now be underneath the third view.
verifyChange(firstViewInfo, false, lp -> {
assertThat(lp.topToBottom == thirdViewInfo.view.getId()).isTrue();
assertThat(lp.endToEnd == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
assertThat(lp.topMargin).isEqualTo(margin);
});
- // The second view should be in underneath the third view.
+ // The second view should be to the start of the third view.
verifyChange(secondViewInfo, false, lp -> {
assertThat(lp.endToStart == thirdViewInfo.view.getId()).isTrue();
assertThat(lp.topToTop == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
assertThat(lp.getMarginEnd()).isEqualTo(margin);
});
- // The third view should be in at the top.
+ // The third view should be at the top end corner. No margin should be applied if not
+ // specified.
verifyChange(thirdViewInfo, true, lp -> {
assertThat(lp.topToTop == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
assertThat(lp.endToEnd == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
@@ -425,14 +426,14 @@
addComplication(engine, thirdViewInfo);
- // The first added view should now be underneath the second view.
+ // The first added view should now be underneath the third view.
verifyChange(firstViewInfo, false, lp -> {
assertThat(lp.topToBottom == thirdViewInfo.view.getId()).isTrue();
assertThat(lp.endToEnd == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
assertThat(lp.topMargin).isEqualTo(complicationMargin);
});
- // The second view should be in underneath the third view.
+ // The second view should be to the start of the third view.
verifyChange(secondViewInfo, false, lp -> {
assertThat(lp.endToStart == thirdViewInfo.view.getId()).isTrue();
assertThat(lp.topToTop == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
@@ -441,6 +442,69 @@
}
/**
+ * Ensures the root complication applies margin if specified.
+ */
+ @Test
+ public void testRootComplicationSpecifiedMargin() {
+ final int defaultMargin = 5;
+ final int complicationMargin = 10;
+ final ComplicationLayoutEngine engine =
+ new ComplicationLayoutEngine(mLayout, defaultMargin, mTouchSession, 0, 0);
+
+ final ViewInfo firstViewInfo = new ViewInfo(
+ new ComplicationLayoutParams(
+ 100,
+ 100,
+ ComplicationLayoutParams.POSITION_TOP
+ | ComplicationLayoutParams.POSITION_END,
+ ComplicationLayoutParams.DIRECTION_DOWN,
+ 0),
+ Complication.CATEGORY_STANDARD,
+ mLayout);
+
+ addComplication(engine, firstViewInfo);
+
+ final ViewInfo secondViewInfo = new ViewInfo(
+ new ComplicationLayoutParams(
+ 100,
+ 100,
+ ComplicationLayoutParams.POSITION_TOP
+ | ComplicationLayoutParams.POSITION_END,
+ ComplicationLayoutParams.DIRECTION_START,
+ 0),
+ Complication.CATEGORY_SYSTEM,
+ mLayout);
+
+ addComplication(engine, secondViewInfo);
+
+ firstViewInfo.clearInvocations();
+ secondViewInfo.clearInvocations();
+
+ final ViewInfo thirdViewInfo = new ViewInfo(
+ new ComplicationLayoutParams(
+ 100,
+ 100,
+ ComplicationLayoutParams.POSITION_TOP
+ | ComplicationLayoutParams.POSITION_END,
+ ComplicationLayoutParams.DIRECTION_START,
+ 1,
+ complicationMargin),
+ Complication.CATEGORY_SYSTEM,
+ mLayout);
+
+ addComplication(engine, thirdViewInfo);
+
+ // The third view is the root view and has specified margin, which should be applied based
+ // on its direction.
+ verifyChange(thirdViewInfo, true, lp -> {
+ assertThat(lp.getMarginStart()).isEqualTo(0);
+ assertThat(lp.getMarginEnd()).isEqualTo(complicationMargin);
+ assertThat(lp.topMargin).isEqualTo(0);
+ assertThat(lp.bottomMargin).isEqualTo(0);
+ });
+ }
+
+ /**
* Ensures layout in a particular position updates.
*/
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutParamsTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutParamsTest.java
index cb7e47b..ce7561e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutParamsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutParamsTest.java
@@ -97,6 +97,31 @@
}
/**
+ * Ensures ComplicationLayoutParams correctly returns whether the complication specified margin.
+ */
+ @Test
+ public void testIsMarginSpecified() {
+ final ComplicationLayoutParams paramsNoMargin = new ComplicationLayoutParams(
+ 100,
+ 100,
+ ComplicationLayoutParams.POSITION_TOP
+ | ComplicationLayoutParams.POSITION_START,
+ ComplicationLayoutParams.DIRECTION_DOWN,
+ 0);
+ assertThat(paramsNoMargin.isMarginSpecified()).isFalse();
+
+ final ComplicationLayoutParams paramsWithMargin = new ComplicationLayoutParams(
+ 100,
+ 100,
+ ComplicationLayoutParams.POSITION_TOP
+ | ComplicationLayoutParams.POSITION_START,
+ ComplicationLayoutParams.DIRECTION_DOWN,
+ 0,
+ 20 /*margin*/);
+ assertThat(paramsWithMargin.isMarginSpecified()).isTrue();
+ }
+
+ /**
* Ensures unspecified margin uses default.
*/
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java
index aa8c93e..30ad485 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java
@@ -90,7 +90,10 @@
private ActivityStarter mActivityStarter;
@Mock
- UiEventLogger mUiEventLogger;
+ private UiEventLogger mUiEventLogger;
+
+ @Captor
+ private ArgumentCaptor<DreamOverlayStateController.Callback> mStateCallbackCaptor;
@Before
public void setup() {
@@ -164,6 +167,29 @@
verify(mDreamOverlayStateController).addComplication(mComplication);
}
+ @Test
+ public void complicationAvailability_checkAvailabilityWhenDreamOverlayBecomesActive() {
+ final DreamHomeControlsComplication.Registrant registrant =
+ new DreamHomeControlsComplication.Registrant(mComplication,
+ mDreamOverlayStateController, mControlsComponent);
+ registrant.start();
+
+ setServiceAvailable(true);
+ setHaveFavorites(false);
+
+ // Complication not available on start.
+ verify(mDreamOverlayStateController, never()).addComplication(mComplication);
+
+ // Favorite controls added, complication should be available now.
+ setHaveFavorites(true);
+
+ // Dream overlay becomes active.
+ setDreamOverlayActive(true);
+
+ // Verify complication is added.
+ verify(mDreamOverlayStateController).addComplication(mComplication);
+ }
+
/**
* Ensures clicking home controls chip logs UiEvent.
*/
@@ -196,10 +222,17 @@
private void setServiceAvailable(boolean value) {
final List<ControlsServiceInfo> serviceInfos = mock(List.class);
+ when(mControlsListingController.getCurrentServices()).thenReturn(serviceInfos);
when(serviceInfos.isEmpty()).thenReturn(!value);
triggerControlsListingCallback(serviceInfos);
}
+ private void setDreamOverlayActive(boolean value) {
+ when(mDreamOverlayStateController.isOverlayActive()).thenReturn(value);
+ verify(mDreamOverlayStateController).addCallback(mStateCallbackCaptor.capture());
+ mStateCallbackCaptor.getValue().onStateChanged();
+ }
+
private void triggerControlsListingCallback(List<ControlsServiceInfo> serviceInfos) {
verify(mControlsListingController).addCallback(mCallbackCaptor.capture());
mCallbackCaptor.getValue().onServicesUpdated(serviceInfos);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt
new file mode 100644
index 0000000..4d66a16
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt
@@ -0,0 +1,302 @@
+/*
+ * 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
+
+import android.content.pm.PackageManager
+import android.content.pm.ProviderInfo
+import androidx.test.filters.SmallTest
+import com.android.internal.widget.LockPatternUtils
+import com.android.systemui.SystemUIAppComponentFactoryBase
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderClient as Client
+import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract as Contract
+import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(JUnit4::class)
+class KeyguardQuickAffordanceProviderTest : SysuiTestCase() {
+
+ @Mock private lateinit var lockPatternUtils: LockPatternUtils
+ @Mock private lateinit var keyguardStateController: KeyguardStateController
+ @Mock private lateinit var userTracker: UserTracker
+ @Mock private lateinit var activityStarter: ActivityStarter
+
+ private lateinit var underTest: KeyguardQuickAffordanceProvider
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ underTest = KeyguardQuickAffordanceProvider()
+ val quickAffordanceRepository =
+ KeyguardQuickAffordanceRepository(
+ scope = CoroutineScope(IMMEDIATE),
+ backgroundDispatcher = IMMEDIATE,
+ selectionManager = KeyguardQuickAffordanceSelectionManager(),
+ configs =
+ setOf(
+ FakeKeyguardQuickAffordanceConfig(
+ key = AFFORDANCE_1,
+ pickerIconResourceId = 1,
+ ),
+ FakeKeyguardQuickAffordanceConfig(
+ key = AFFORDANCE_2,
+ pickerIconResourceId = 2,
+ ),
+ ),
+ )
+ underTest.interactor =
+ KeyguardQuickAffordanceInteractor(
+ keyguardInteractor =
+ KeyguardInteractor(
+ repository = FakeKeyguardRepository(),
+ ),
+ registry = mock(),
+ lockPatternUtils = lockPatternUtils,
+ keyguardStateController = keyguardStateController,
+ userTracker = userTracker,
+ activityStarter = activityStarter,
+ featureFlags =
+ FakeFeatureFlags().apply {
+ set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, true)
+ },
+ repository = { quickAffordanceRepository },
+ )
+
+ underTest.attachInfoForTesting(
+ context,
+ ProviderInfo().apply { authority = Contract.AUTHORITY },
+ )
+ context.contentResolver.addProvider(Contract.AUTHORITY, underTest)
+ context.testablePermissions.setPermission(
+ Contract.PERMISSION,
+ PackageManager.PERMISSION_GRANTED,
+ )
+ }
+
+ @Test
+ fun `onAttachInfo - reportsContext`() {
+ val callback: SystemUIAppComponentFactoryBase.ContextAvailableCallback = mock()
+ underTest.setContextAvailableCallback(callback)
+
+ underTest.attachInfo(context, null)
+
+ verify(callback).onContextAvailable(context)
+ }
+
+ @Test
+ fun getType() {
+ assertThat(underTest.getType(Contract.AffordanceTable.URI))
+ .isEqualTo(
+ "vnd.android.cursor.dir/vnd." +
+ "${Contract.AUTHORITY}.${Contract.AffordanceTable.TABLE_NAME}"
+ )
+ assertThat(underTest.getType(Contract.SlotTable.URI))
+ .isEqualTo(
+ "vnd.android.cursor.dir/vnd.${Contract.AUTHORITY}.${Contract.SlotTable.TABLE_NAME}"
+ )
+ assertThat(underTest.getType(Contract.SelectionTable.URI))
+ .isEqualTo(
+ "vnd.android.cursor.dir/vnd." +
+ "${Contract.AUTHORITY}.${Contract.SelectionTable.TABLE_NAME}"
+ )
+ }
+
+ @Test
+ fun `insert and query selection`() =
+ runBlocking(IMMEDIATE) {
+ val slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START
+ val affordanceId = AFFORDANCE_2
+
+ Client.insertSelection(
+ context = context,
+ slotId = slotId,
+ affordanceId = affordanceId,
+ dispatcher = IMMEDIATE,
+ )
+
+ assertThat(
+ Client.querySelections(
+ context = context,
+ dispatcher = IMMEDIATE,
+ )
+ )
+ .isEqualTo(
+ listOf(
+ Client.Selection(
+ slotId = slotId,
+ affordanceId = affordanceId,
+ )
+ )
+ )
+ }
+
+ @Test
+ fun `query slots`() =
+ runBlocking(IMMEDIATE) {
+ assertThat(
+ Client.querySlots(
+ context = context,
+ dispatcher = IMMEDIATE,
+ )
+ )
+ .isEqualTo(
+ listOf(
+ Client.Slot(
+ id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
+ capacity = 1,
+ ),
+ Client.Slot(
+ id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
+ capacity = 1,
+ ),
+ )
+ )
+ }
+
+ @Test
+ fun `query affordances`() =
+ runBlocking(IMMEDIATE) {
+ assertThat(
+ Client.queryAffordances(
+ context = context,
+ dispatcher = IMMEDIATE,
+ )
+ )
+ .isEqualTo(
+ listOf(
+ Client.Affordance(
+ id = AFFORDANCE_1,
+ name = AFFORDANCE_1,
+ iconResourceId = 1,
+ ),
+ Client.Affordance(
+ id = AFFORDANCE_2,
+ name = AFFORDANCE_2,
+ iconResourceId = 2,
+ ),
+ )
+ )
+ }
+
+ @Test
+ fun `delete and query selection`() =
+ runBlocking(IMMEDIATE) {
+ Client.insertSelection(
+ context = context,
+ slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
+ affordanceId = AFFORDANCE_1,
+ dispatcher = IMMEDIATE,
+ )
+ Client.insertSelection(
+ context = context,
+ slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
+ affordanceId = AFFORDANCE_2,
+ dispatcher = IMMEDIATE,
+ )
+
+ Client.deleteSelection(
+ context = context,
+ slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
+ affordanceId = AFFORDANCE_2,
+ dispatcher = IMMEDIATE,
+ )
+
+ assertThat(
+ Client.querySelections(
+ context = context,
+ dispatcher = IMMEDIATE,
+ )
+ )
+ .isEqualTo(
+ listOf(
+ Client.Selection(
+ slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
+ affordanceId = AFFORDANCE_1,
+ )
+ )
+ )
+ }
+
+ @Test
+ fun `delete all selections in a slot`() =
+ runBlocking(IMMEDIATE) {
+ Client.insertSelection(
+ context = context,
+ slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
+ affordanceId = AFFORDANCE_1,
+ dispatcher = IMMEDIATE,
+ )
+ Client.insertSelection(
+ context = context,
+ slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
+ affordanceId = AFFORDANCE_2,
+ dispatcher = IMMEDIATE,
+ )
+
+ Client.deleteAllSelections(
+ context = context,
+ slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
+ dispatcher = IMMEDIATE,
+ )
+
+ assertThat(
+ Client.querySelections(
+ context = context,
+ dispatcher = IMMEDIATE,
+ )
+ )
+ .isEqualTo(
+ listOf(
+ Client.Selection(
+ slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
+ affordanceId = AFFORDANCE_1,
+ )
+ )
+ )
+ }
+
+ companion object {
+ private val IMMEDIATE = Dispatchers.Main.immediate
+ private const val AFFORDANCE_1 = "affordance_1"
+ private const val AFFORDANCE_2 = "affordance_2"
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt
index 21e5068..3269f5a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt
@@ -93,7 +93,6 @@
@Test
fun testShow_isScrimmed() {
mPrimaryBouncerInteractor.show(true)
- verify(repository).setShowMessage(null)
verify(repository).setOnScreenTurnedOff(false)
verify(repository).setKeyguardAuthenticated(null)
verify(repository).setPrimaryHide(false)
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 68a5f47..885cc54 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
@@ -261,7 +261,12 @@
@Test
fun updateView_noOverrides_usesInfoFromAppIcon() {
controllerReceiver.displayView(
- ChipReceiverInfo(routeInfo, appIconDrawableOverride = null, appNameOverride = null)
+ ChipReceiverInfo(
+ routeInfo,
+ appIconDrawableOverride = null,
+ appNameOverride = null,
+ id = "id",
+ )
)
val view = getChipView()
@@ -274,7 +279,12 @@
val drawableOverride = context.getDrawable(R.drawable.ic_celebration)!!
controllerReceiver.displayView(
- ChipReceiverInfo(routeInfo, drawableOverride, appNameOverride = null)
+ ChipReceiverInfo(
+ routeInfo,
+ drawableOverride,
+ appNameOverride = null,
+ id = "id",
+ )
)
val view = getChipView()
@@ -286,7 +296,12 @@
val appNameOverride = "Sweet New App"
controllerReceiver.displayView(
- ChipReceiverInfo(routeInfo, appIconDrawableOverride = null, appNameOverride)
+ ChipReceiverInfo(
+ routeInfo,
+ appIconDrawableOverride = null,
+ appNameOverride,
+ id = "id",
+ )
)
val view = getChipView()
@@ -340,7 +355,7 @@
.addFeature("feature")
.setClientPackageName(packageName)
.build()
- return ChipReceiverInfo(routeInfo, null, null)
+ return ChipReceiverInfo(routeInfo, null, null, id = "id")
}
private fun ViewGroup.getAppIconView() = this.requireViewById<ImageView>(R.id.app_icon)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
index cd7a949..72e022e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
@@ -439,6 +439,17 @@
verify(mQSPanelController).setExpanded(false);
}
+ @Test
+ public void startsListeningAfterStateChangeToExpanded_inSplitShade() {
+ QSFragment fragment = resumeAndGetFragment();
+ enableSplitShade();
+ fragment.setQsVisible(true);
+ clearInvocations(mQSPanelController);
+
+ fragment.setExpanded(true);
+ verify(mQSPanelController).setListening(true, true);
+ }
+
@Override
protected Fragment instantiate(Context context, String className, Bundle arguments) {
MockitoAnnotations.initMocks(this);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt
index 0ce9056..d7eb337 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt
@@ -24,6 +24,7 @@
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -320,6 +321,48 @@
assertThat(changes.largeScreenConstraintsChanges).isNull()
}
+ @Test
+ fun testRelevantViewsAreNotMatchConstraints() {
+ val views = mapOf(
+ R.id.clock to "clock",
+ R.id.date to "date",
+ R.id.statusIcons to "icons",
+ R.id.privacy_container to "privacy",
+ R.id.carrier_group to "carriers",
+ R.id.batteryRemainingIcon to "battery",
+ )
+ views.forEach { (id, name) ->
+ assertWithMessage("$name has 0 height in qqs")
+ .that(qqsConstraint.getConstraint(id).layout.mHeight).isNotEqualTo(0)
+ assertWithMessage("$name has 0 width in qqs")
+ .that(qqsConstraint.getConstraint(id).layout.mWidth).isNotEqualTo(0)
+ assertWithMessage("$name has 0 height in qs")
+ .that(qsConstraint.getConstraint(id).layout.mHeight).isNotEqualTo(0)
+ assertWithMessage("$name has 0 width in qs")
+ .that(qsConstraint.getConstraint(id).layout.mWidth).isNotEqualTo(0)
+ }
+ }
+
+ @Test
+ fun testCheckViewsDontChangeSizeBetweenAnimationConstraints() {
+ val views = mapOf(
+ R.id.clock to "clock",
+ R.id.date to "date",
+ R.id.statusIcons to "icons",
+ R.id.privacy_container to "privacy",
+ R.id.carrier_group to "carriers",
+ R.id.batteryRemainingIcon to "battery",
+ )
+ views.forEach { (id, name) ->
+ assertWithMessage("$name changes height")
+ .that(qqsConstraint.getConstraint(id).layout.mHeight)
+ .isEqualTo(qsConstraint.getConstraint(id).layout.mHeight)
+ assertWithMessage("$name changes width")
+ .that(qqsConstraint.getConstraint(id).layout.mWidth)
+ .isEqualTo(qsConstraint.getConstraint(id).layout.mWidth)
+ }
+ }
+
private operator fun ConstraintsChanges.invoke() {
qqsConstraintsChanges?.invoke(qqsConstraint)
qsConstraintsChanges?.invoke(qsConstraint)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
index 77c690a..fe4da47 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
@@ -1067,7 +1067,7 @@
// GIVEN a trust granted message but trust isn't granted
final String trustGrantedMsg = "testing trust granted message";
- mController.getKeyguardCallback().showTrustGrantedMessage(trustGrantedMsg);
+ mController.getKeyguardCallback().onTrustGrantedWithFlags(0, 0, trustGrantedMsg);
verifyHideIndication(INDICATION_TYPE_TRUST);
@@ -1091,7 +1091,7 @@
// WHEN the showTrustGranted method is called
final String trustGrantedMsg = "testing trust granted message";
- mController.getKeyguardCallback().showTrustGrantedMessage(trustGrantedMsg);
+ mController.getKeyguardCallback().onTrustGrantedWithFlags(0, 0, trustGrantedMsg);
// THEN verify the trust granted message shows
verifyIndicationMessage(
@@ -1108,7 +1108,7 @@
when(mKeyguardUpdateMonitor.getUserHasTrust(anyInt())).thenReturn(true);
// WHEN the showTrustGranted method is called with a null message
- mController.getKeyguardCallback().showTrustGrantedMessage(null);
+ mController.getKeyguardCallback().onTrustGrantedWithFlags(0, 0, null);
// THEN verify the default trust granted message shows
verifyIndicationMessage(
@@ -1125,7 +1125,7 @@
when(mKeyguardUpdateMonitor.getUserHasTrust(anyInt())).thenReturn(true);
// WHEN the showTrustGranted method is called with an EMPTY string
- mController.getKeyguardCallback().showTrustGrantedMessage("");
+ mController.getKeyguardCallback().onTrustGrantedWithFlags(0, 0, "");
// THEN verify NO trust message is shown
verifyNoMessage(INDICATION_TYPE_TRUST);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index 49c3a21..9f70565 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -222,9 +222,16 @@
}
@Test
- public void onPanelExpansionChanged_neverHidesScrimmedBouncer() {
+ public void onPanelExpansionChanged_neverHidesFullscreenBouncer() {
when(mPrimaryBouncer.isShowing()).thenReturn(true);
- when(mPrimaryBouncer.isScrimmed()).thenReturn(true);
+ when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
+ KeyguardSecurityModel.SecurityMode.SimPuk);
+ mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
+ verify(mPrimaryBouncer).setExpansion(eq(KeyguardBouncer.EXPANSION_VISIBLE));
+
+ reset(mPrimaryBouncer);
+ when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
+ KeyguardSecurityModel.SecurityMode.SimPin);
mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
verify(mPrimaryBouncer).setExpansion(eq(KeyguardBouncer.EXPANSION_VISIBLE));
}
@@ -271,13 +278,6 @@
}
@Test
- public void onPanelExpansionChanged_neverTranslatesBouncerWhenOccluded() {
- mStatusBarKeyguardViewManager.setOccluded(true /* occluded */, false /* animate */);
- mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
- verify(mPrimaryBouncer, never()).setExpansion(eq(0.5f));
- }
-
- @Test
public void onPanelExpansionChanged_neverTranslatesBouncerWhenWakeAndUnlock() {
when(mBiometricUnlockController.getMode())
.thenReturn(BiometricUnlockController.MODE_WAKE_AND_UNLOCK);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java
index 43d0fe9..1eee08c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java
@@ -221,4 +221,33 @@
Assert.assertFalse(mBatteryController.isChargingSourceDock());
}
+
+ @Test
+ public void batteryStateChanged_healthNotOverheated_outputsFalse() {
+ Intent intent = new Intent(Intent.ACTION_BATTERY_CHANGED);
+ intent.putExtra(BatteryManager.EXTRA_HEALTH, BatteryManager.BATTERY_HEALTH_GOOD);
+
+ mBatteryController.onReceive(getContext(), intent);
+
+ Assert.assertFalse(mBatteryController.isOverheated());
+ }
+
+ @Test
+ public void batteryStateChanged_healthOverheated_outputsTrue() {
+ Intent intent = new Intent(Intent.ACTION_BATTERY_CHANGED);
+ intent.putExtra(BatteryManager.EXTRA_HEALTH, BatteryManager.BATTERY_HEALTH_OVERHEAT);
+
+ mBatteryController.onReceive(getContext(), intent);
+
+ Assert.assertTrue(mBatteryController.isOverheated());
+ }
+
+ @Test
+ public void batteryStateChanged_noHealthGiven_outputsFalse() {
+ Intent intent = new Intent(Intent.ACTION_BATTERY_CHANGED);
+
+ mBatteryController.onReceive(getContext(), intent);
+
+ Assert.assertFalse(mBatteryController.isOverheated());
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt
index 8572478..09f0d4a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt
@@ -119,7 +119,7 @@
)
)
- verify(logger).logViewAddition("Fake Window Title")
+ verify(logger).logViewAddition("id", "Fake Window Title")
}
@Test
@@ -153,7 +153,7 @@
underTest.displayView(getState())
assertThat(fakeWakeLock.isHeld).isTrue()
- underTest.removeView("test reason")
+ underTest.removeView("id", "test reason")
assertThat(fakeWakeLock.isHeld).isFalse()
}
@@ -263,21 +263,143 @@
}
@Test
+ fun multipleViewsWithDifferentIds_recentActiveViewIsDisplayed() {
+ underTest.displayView(ViewInfo("First name", id = "id1"))
+
+ verify(windowManager).addView(any(), any())
+
+ reset(windowManager)
+ underTest.displayView(ViewInfo("Second name", id = "id2"))
+ underTest.removeView("id2", "test reason")
+
+ verify(windowManager).removeView(any())
+
+ fakeClock.advanceTime(DISPLAY_VIEW_DELAY + 1)
+
+ assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id1")
+ assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("First name")
+
+ reset(windowManager)
+ fakeClock.advanceTime(TIMEOUT_MS + 1)
+
+ verify(windowManager).removeView(any())
+ assertThat(underTest.activeViews.size).isEqualTo(0)
+ }
+
+ @Test
+ fun multipleViewsWithDifferentIds_oldViewRemoved_recentViewIsDisplayed() {
+ underTest.displayView(ViewInfo("First name", id = "id1"))
+
+ verify(windowManager).addView(any(), any())
+
+ reset(windowManager)
+ underTest.displayView(ViewInfo("Second name", id = "id2"))
+ underTest.removeView("id1", "test reason")
+
+ verify(windowManager, never()).removeView(any())
+ assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id2")
+ assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("Second name")
+
+ fakeClock.advanceTime(TIMEOUT_MS + 1)
+
+ verify(windowManager).removeView(any())
+ assertThat(underTest.activeViews.size).isEqualTo(0)
+ }
+
+ @Test
+ fun multipleViewsWithDifferentIds_threeDifferentViews_recentActiveViewIsDisplayed() {
+ underTest.displayView(ViewInfo("First name", id = "id1"))
+ underTest.displayView(ViewInfo("Second name", id = "id2"))
+ underTest.displayView(ViewInfo("Third name", id = "id3"))
+
+ verify(windowManager).addView(any(), any())
+
+ reset(windowManager)
+ underTest.removeView("id3", "test reason")
+
+ verify(windowManager).removeView(any())
+
+ fakeClock.advanceTime(DISPLAY_VIEW_DELAY + 1)
+
+ assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id2")
+ assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("Second name")
+
+ reset(windowManager)
+ underTest.removeView("id2", "test reason")
+
+ verify(windowManager).removeView(any())
+
+ fakeClock.advanceTime(DISPLAY_VIEW_DELAY + 1)
+
+ assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id1")
+ assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("First name")
+
+ reset(windowManager)
+ fakeClock.advanceTime(TIMEOUT_MS + 1)
+
+ verify(windowManager).removeView(any())
+ assertThat(underTest.activeViews.size).isEqualTo(0)
+ }
+
+ @Test
+ fun multipleViewsWithDifferentIds_oneViewStateChanged_stackHasRecentState() {
+ underTest.displayView(ViewInfo("First name", id = "id1"))
+ underTest.displayView(ViewInfo("New name", id = "id1"))
+
+ verify(windowManager).addView(any(), any())
+
+ reset(windowManager)
+ underTest.displayView(ViewInfo("Second name", id = "id2"))
+ underTest.removeView("id2", "test reason")
+
+ verify(windowManager).removeView(any())
+
+ fakeClock.advanceTime(DISPLAY_VIEW_DELAY + 1)
+
+ assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id1")
+ assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("New name")
+ assertThat(underTest.activeViews[0].second.name).isEqualTo("New name")
+
+ reset(windowManager)
+ fakeClock.advanceTime(TIMEOUT_MS + 1)
+
+ verify(windowManager).removeView(any())
+ assertThat(underTest.activeViews.size).isEqualTo(0)
+ }
+
+ @Test
+ fun multipleViewsWithDifferentIds_viewsTimeouts_noViewLeftToDisplay() {
+ underTest.displayView(ViewInfo("First name", id = "id1"))
+ fakeClock.advanceTime(TIMEOUT_MS / 3)
+ underTest.displayView(ViewInfo("Second name", id = "id2"))
+ fakeClock.advanceTime(TIMEOUT_MS / 3)
+ underTest.displayView(ViewInfo("Third name", id = "id3"))
+
+ reset(windowManager)
+ fakeClock.advanceTime(TIMEOUT_MS + 1)
+
+ verify(windowManager).removeView(any())
+ verify(windowManager, never()).addView(any(), any())
+ assertThat(underTest.activeViews.size).isEqualTo(0)
+ }
+
+ @Test
fun removeView_viewRemovedAndRemovalLogged() {
// First, add the view
underTest.displayView(getState())
// Then, remove it
val reason = "test reason"
- underTest.removeView(reason)
+ val deviceId = "id"
+ underTest.removeView(deviceId, reason)
verify(windowManager).removeView(any())
- verify(logger).logViewRemoval(reason)
+ verify(logger).logViewRemoval(deviceId, reason)
}
@Test
fun removeView_noAdd_viewNotRemoved() {
- underTest.removeView("reason")
+ underTest.removeView("id", "reason")
verify(windowManager, never()).removeView(any())
}
@@ -329,7 +451,8 @@
val name: String,
override val windowTitle: String = "Window Title",
override val wakeReason: String = "WAKE_REASON",
- override val timeoutMs: Int = 1
+ override val timeoutMs: Int = 1,
+ override val id: String = "id",
) : TemporaryViewInfo()
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt
index d155050..116b8fe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt
@@ -44,7 +44,7 @@
@Test
fun logViewAddition_bufferHasLog() {
- logger.logViewAddition("Test Window Title")
+ logger.logViewAddition("test id", "Test Window Title")
val stringWriter = StringWriter()
buffer.dump(PrintWriter(stringWriter), tailLength = 0)
@@ -57,7 +57,8 @@
@Test
fun logViewRemoval_bufferHasTagAndReason() {
val reason = "test reason"
- logger.logViewRemoval(reason)
+ val deviceId = "test id"
+ logger.logViewRemoval(deviceId, reason)
val stringWriter = StringWriter()
buffer.dump(PrintWriter(stringWriter), tailLength = 0)
@@ -65,6 +66,7 @@
assertThat(actualString).contains(TAG)
assertThat(actualString).contains(reason)
+ assertThat(actualString).contains(deviceId)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
index 8e37aa2..47c84ab 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
@@ -377,6 +377,7 @@
windowTitle = WINDOW_TITLE,
wakeReason = WAKE_REASON,
timeoutMs = TIMEOUT,
+ id = DEVICE_ID,
)
}
@@ -401,3 +402,4 @@
private const val TIMEOUT = 10000
private const val WINDOW_TITLE = "Test Chipbar Window Title"
private const val WAKE_REASON = "TEST_CHIPBAR_WAKE_REASON"
+private const val DEVICE_ID = "id"
diff --git a/packages/VpnDialogs/Android.bp b/packages/VpnDialogs/Android.bp
index 05135b2..e4f80e2 100644
--- a/packages/VpnDialogs/Android.bp
+++ b/packages/VpnDialogs/Android.bp
@@ -23,10 +23,15 @@
default_applicable_licenses: ["frameworks_base_license"],
}
+android_library {
+ name: "VpnDialogsLib",
+ srcs: ["src/**/*.java"],
+}
+
android_app {
name: "VpnDialogs",
certificate: "platform",
privileged: true,
- srcs: ["src/**/*.java"],
+ static_libs: ["VpnDialogsLib"],
platform_apis: true,
}
diff --git a/packages/VpnDialogs/res/values/strings.xml b/packages/VpnDialogs/res/values/strings.xml
index f971a09..28e7272 100644
--- a/packages/VpnDialogs/res/values/strings.xml
+++ b/packages/VpnDialogs/res/values/strings.xml
@@ -100,4 +100,33 @@
without any consequences. [CHAR LIMIT=20] -->
<string name="dismiss">Dismiss</string>
+ <!-- Malicious VPN apps may provide very long labels or cunning HTML to trick the system dialogs
+ into displaying what they want. The system will attempt to sanitize the label, and if the
+ label is deemed dangerous, then this string is used instead. The first argument is the
+ first 30 characters of the label, and the second argument is the package name of the app.
+ Example : Normally a VPN app may be called "My VPN app" in which case the dialog will read
+ "My VPN app wants to set up a VPN connection...". If the label is very long, then, this
+ will be used to show "VerylongVPNlabel… (com.my.vpn.app) wants to set up a VPN
+ connection...". For this case, the code will refer to sanitized_vpn_label_with_ellipsis.
+ -->
+ <string name="sanitized_vpn_label_with_ellipsis">
+ <xliff:g id="sanitized_vpn_label_with_ellipsis" example="My VPN app">%1$s</xliff:g>… (
+ <xliff:g id="sanitized_vpn_label_with_ellipsis" example="com.my.vpn.app">%2$s</xliff:g>)
+ </string>
+
+ <!-- Malicious VPN apps may provide very long labels or cunning HTML to trick the system dialogs
+ into displaying what they want. The system will attempt to sanitize the label, and if the
+ label is deemed dangerous, then this string is used instead. The first argument is the
+ label, and the second argument is the package name of the app.
+ Example : Normally a VPN app may be called "My VPN app" in which case the dialog will read
+ "My VPN app wants to set up a VPN connection...". If the VPN label contains HTML tag but
+ the length is not very long, the dialog will show "VpnLabelWith<br>HtmlTag
+ (com.my.vpn.app) wants to set up a VPN connection...". For this case, the code will refer
+ to sanitized_vpn_label.
+ -->
+ <string name="sanitized_vpn_label">
+ <xliff:g id="sanitized_vpn_label" example="My VPN app">%1$s</xliff:g> (
+ <xliff:g id="sanitized_vpn_label" example="com.my.vpn.app">%2$s</xliff:g>)
+ </string>
+
</resources>
diff --git a/packages/VpnDialogs/src/com/android/vpndialogs/ConfirmDialog.java b/packages/VpnDialogs/src/com/android/vpndialogs/ConfirmDialog.java
index fb23678..a98d6d8 100644
--- a/packages/VpnDialogs/src/com/android/vpndialogs/ConfirmDialog.java
+++ b/packages/VpnDialogs/src/com/android/vpndialogs/ConfirmDialog.java
@@ -33,6 +33,7 @@
import android.widget.Button;
import android.widget.TextView;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.AlertActivity;
import com.android.internal.net.VpnConfig;
@@ -40,12 +41,19 @@
implements DialogInterface.OnClickListener, ImageGetter {
private static final String TAG = "VpnConfirm";
+ // Usually the label represents the app name, 150 code points might be enough to display the app
+ // name, and 150 code points won't cover the warning message from VpnDialog.
+ @VisibleForTesting
+ static final int MAX_VPN_LABEL_LENGTH = 150;
+
@VpnManager.VpnType private final int mVpnType;
private String mPackage;
private VpnManager mVm;
+ private View mView;
+
public ConfirmDialog() {
this(VpnManager.TYPE_VPN_SERVICE);
}
@@ -54,6 +62,43 @@
mVpnType = vpnType;
}
+ /**
+ * This function will use the string resource to combine the VPN label and the package name.
+ *
+ * If the VPN label violates the length restriction, the first 30 code points of VPN label and
+ * the package name will be returned. Or return the VPN label and the package name directly if
+ * the VPN label doesn't violate the length restriction.
+ *
+ * The result will be something like,
+ * - ThisIsAVeryLongVpnAppNameWhich... (com.vpn.app)
+ * if the VPN label violates the length restriction.
+ * or
+ * - VpnLabelWith<br>HtmlTag (com.vpn.app)
+ * if the VPN label doesn't violate the length restriction.
+ *
+ */
+ private String getSimplifiedLabel(String vpnLabel, String packageName) {
+ if (vpnLabel.codePointCount(0, vpnLabel.length()) > 30) {
+ return getString(R.string.sanitized_vpn_label_with_ellipsis,
+ vpnLabel.substring(0, vpnLabel.offsetByCodePoints(0, 30)),
+ packageName);
+ }
+
+ return getString(R.string.sanitized_vpn_label, vpnLabel, packageName);
+ }
+
+ @VisibleForTesting
+ protected String getSanitizedVpnLabel(String vpnLabel, String packageName) {
+ final String sanitizedVpnLabel = Html.escapeHtml(vpnLabel);
+ final boolean exceedMaxVpnLabelLength = sanitizedVpnLabel.codePointCount(0,
+ sanitizedVpnLabel.length()) > MAX_VPN_LABEL_LENGTH;
+ if (exceedMaxVpnLabelLength || !vpnLabel.equals(sanitizedVpnLabel)) {
+ return getSimplifiedLabel(sanitizedVpnLabel, packageName);
+ }
+
+ return sanitizedVpnLabel;
+ }
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -75,15 +120,16 @@
finish();
return;
}
- View view = View.inflate(this, R.layout.confirm, null);
- ((TextView) view.findViewById(R.id.warning)).setText(
- Html.fromHtml(getString(R.string.warning, getVpnLabel()),
- this, null /* tagHandler */));
+ mView = View.inflate(this, R.layout.confirm, null);
+ ((TextView) mView.findViewById(R.id.warning)).setText(
+ Html.fromHtml(getString(R.string.warning, getSanitizedVpnLabel(
+ getVpnLabel().toString(), mPackage)),
+ this /* imageGetter */, null /* tagHandler */));
mAlertParams.mTitle = getText(R.string.prompt);
mAlertParams.mPositiveButtonText = getText(android.R.string.ok);
mAlertParams.mPositiveButtonListener = this;
mAlertParams.mNegativeButtonText = getText(android.R.string.cancel);
- mAlertParams.mView = view;
+ mAlertParams.mView = mView;
setupAlert();
getWindow().setCloseOnTouchOutside(false);
@@ -92,6 +138,11 @@
button.setFilterTouchesWhenObscured(true);
}
+ @VisibleForTesting
+ public CharSequence getWarningText() {
+ return ((TextView) mView.findViewById(R.id.warning)).getText();
+ }
+
private CharSequence getVpnLabel() {
try {
return VpnConfig.getVpnLabel(this, mPackage);
diff --git a/packages/VpnDialogs/tests/Android.bp b/packages/VpnDialogs/tests/Android.bp
new file mode 100644
index 0000000..68639bd
--- /dev/null
+++ b/packages/VpnDialogs/tests/Android.bp
@@ -0,0 +1,36 @@
+// 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 {
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test {
+ name: "VpnDialogsTests",
+ // Use platform certificate because the test will invoke a hidden API.
+ // (e.g. VpnManager#prepareVpn()).
+ certificate: "platform",
+ libs: [
+ "android.test.runner",
+ "android.test.base",
+ ],
+ static_libs: [
+ "androidx.test.core",
+ "androidx.test.rules",
+ "androidx.test.ext.junit",
+ "mockito-target-minus-junit4",
+ "VpnDialogsLib",
+ ],
+ srcs: ["src/**/*.java"],
+}
diff --git a/packages/VpnDialogs/tests/AndroidManifest.xml b/packages/VpnDialogs/tests/AndroidManifest.xml
new file mode 100644
index 0000000..f26c1fe
--- /dev/null
+++ b/packages/VpnDialogs/tests/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?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.
+ */
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ package="com.android.vpndialogs.tests">
+
+ <application android:debuggable="true">
+ <uses-library android:name="android.test.runner" />
+ <activity android:name="com.android.vpndialogs.VpnDialogTest$InstrumentedConfirmDialog"/>
+ </application>
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.vpndialogs.tests"
+ android:label="Vpn dialog tests">
+ </instrumentation>
+</manifest>
diff --git a/packages/VpnDialogs/tests/src/com/android/vpndialogs/VpnDialogTest.java b/packages/VpnDialogs/tests/src/com/android/vpndialogs/VpnDialogTest.java
new file mode 100644
index 0000000..7cfa466
--- /dev/null
+++ b/packages/VpnDialogs/tests/src/com/android/vpndialogs/VpnDialogTest.java
@@ -0,0 +1,154 @@
+/*
+ * 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.vpndialogs;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doReturn;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.net.VpnManager;
+
+import androidx.test.core.app.ActivityScenario;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+public class VpnDialogTest {
+ private ActivityScenario<ConfirmDialog> mActivityScenario;
+
+ @SuppressWarnings("StaticMockMember")
+ @Mock
+ private static PackageManager sPm;
+
+ @SuppressWarnings("StaticMockMember")
+ @Mock
+ private static VpnManager sVm;
+
+ @Mock
+ private ApplicationInfo mAi;
+
+ private static final String VPN_APP_NAME = "VpnApp";
+ private static final String VPN_APP_PACKAGE_NAME = "com.android.vpndialogs.VpnDialogTest";
+ private static final String VPN_LABEL_CONTAINS_HTML_TAG =
+ "<b><a href=\"https://www.malicious.vpn.app.com\">Google Play</a>";
+ private static final String VPN_LABEL_CONTAINS_HTML_TAG_AND_VIOLATE_LENGTH_RESTRICTION =
+ "<b><a href=\"https://www.malicious.vpn.app.com\">Google Play</a></b>"
+ + " Wants to connect the network. <br></br><br></br><br></br><br></br><br></br>"
+ + " <br></br><br></br><br></br><br></br><br></br><br></br><br></br><br></br> Deny it?";
+ private static final String VPN_LABEL_VIOLATES_LENGTH_RESTRICTION = "This is a VPN label"
+ + " which violates the length restriction. The length restriction here are 150 code"
+ + " points. So the VPN label should be sanitized, and shows the package name to the"
+ + " user.";
+
+ public static class InstrumentedConfirmDialog extends ConfirmDialog {
+ @Override
+ public PackageManager getPackageManager() {
+ return sPm;
+ }
+
+ @Override
+ public @Nullable Object getSystemService(@ServiceName @NonNull String name) {
+ switch (name) {
+ case Context.VPN_MANAGEMENT_SERVICE:
+ return sVm;
+ default:
+ return super.getSystemService(name);
+ }
+ }
+
+ @Override
+ public String getCallingPackage() {
+ return VPN_APP_PACKAGE_NAME;
+ }
+ }
+
+ private void launchActivity() {
+ final Context context = getInstrumentation().getContext();
+ mActivityScenario = ActivityScenario.launch(
+ new Intent(context, InstrumentedConfirmDialog.class));
+ }
+
+ @Test
+ public void testGetSanitizedVpnLabel_withNormalCase() throws Exception {
+ // Test the normal case that the VPN label showed in the VpnDialog is the app name.
+ doReturn(VPN_APP_NAME).when(mAi).loadLabel(sPm);
+ launchActivity();
+ mActivityScenario.onActivity(activity -> {
+ assertTrue(activity.getWarningText().toString().contains(VPN_APP_NAME));
+ });
+ }
+
+ private void verifySanitizedVpnLabel(String originalLabel) {
+ doReturn(originalLabel).when(mAi).loadLabel(sPm);
+ launchActivity();
+ mActivityScenario.onActivity(activity -> {
+ // The VPN label was sanitized because violating length restriction or having a html
+ // tag, so the warning message will contain the package name.
+ assertTrue(activity.getWarningText().toString().contains(activity.getCallingPackage()));
+ // Also, the length of sanitized VPN label shouldn't longer than MAX_VPN_LABEL_LENGTH
+ // and it shouldn't contain html tag.
+ final String sanitizedVpnLabel =
+ activity.getSanitizedVpnLabel(originalLabel, VPN_APP_PACKAGE_NAME);
+ assertTrue(sanitizedVpnLabel.codePointCount(0, sanitizedVpnLabel.length())
+ < ConfirmDialog.MAX_VPN_LABEL_LENGTH);
+ assertFalse(sanitizedVpnLabel.contains("<b>"));
+ });
+ }
+
+ @Test
+ public void testGetSanitizedVpnLabel_withHtmlTag() throws Exception {
+ // Test the case that the VPN label was sanitized because there is a html tag.
+ verifySanitizedVpnLabel(VPN_LABEL_CONTAINS_HTML_TAG);
+ }
+
+ @Test
+ public void testGetSanitizedVpnLabel_withHtmlTagAndViolateLengthRestriction() throws Exception {
+ // Test the case that the VPN label was sanitized because there is a html tag.
+ verifySanitizedVpnLabel(VPN_LABEL_CONTAINS_HTML_TAG_AND_VIOLATE_LENGTH_RESTRICTION);
+ }
+
+ @Test
+ public void testGetSanitizedVpnLabel_withLengthRestriction() throws Exception {
+ // Test the case that the VPN label was sanitized because hitting the length restriction.
+ verifySanitizedVpnLabel(VPN_LABEL_VIOLATES_LENGTH_RESTRICTION);
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ doReturn(false).when(sVm).prepareVpn(anyString(), anyString(), anyInt());
+ doReturn(null).when(sPm).queryIntentServices(any(), anyInt());
+ doReturn(mAi).when(sPm).getApplicationInfo(anyString(), anyInt());
+ }
+}
diff --git a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
index 000bafe..ce7854d 100644
--- a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
+++ b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
@@ -86,6 +86,15 @@
}
/**
+ * For communicating when activities are blocked from entering PIP on the display by this
+ * policy controller.
+ */
+ public interface PipBlockedCallback {
+ /** Called when an activity is blocked from entering PIP. */
+ void onEnteringPipBlocked(int uid);
+ }
+
+ /**
* If required, allow the secure activity to display on remote device since
* {@link android.os.Build.VERSION_CODES#TIRAMISU}.
*/
@@ -112,6 +121,7 @@
@GuardedBy("mGenericWindowPolicyControllerLock")
final ArraySet<Integer> mRunningUids = new ArraySet<>();
@Nullable private final ActivityListener mActivityListener;
+ @Nullable private final PipBlockedCallback mPipBlockedCallback;
private final Handler mHandler = new Handler(Looper.getMainLooper());
@NonNull
@GuardedBy("mGenericWindowPolicyControllerLock")
@@ -155,6 +165,7 @@
@NonNull Set<ComponentName> blockedActivities,
@ActivityPolicy int defaultActivityPolicy,
@NonNull ActivityListener activityListener,
+ @NonNull PipBlockedCallback pipBlockedCallback,
@NonNull ActivityBlockedCallback activityBlockedCallback,
@NonNull SecureWindowCallback secureWindowCallback,
@AssociationRequest.DeviceProfile String deviceProfile) {
@@ -169,6 +180,7 @@
setInterestedWindowFlags(windowFlags, systemWindowFlags);
mActivityListener = activityListener;
mDeviceProfile = deviceProfile;
+ mPipBlockedCallback = pipBlockedCallback;
mSecureWindowCallback = secureWindowCallback;
}
@@ -317,6 +329,17 @@
}
}
+ @Override
+ public boolean isEnteringPipAllowed(int uid) {
+ if (super.isEnteringPipAllowed(uid)) {
+ return true;
+ }
+ mHandler.post(() -> {
+ mPipBlockedCallback.onEnteringPipBlocked(uid);
+ });
+ return false;
+ }
+
/**
* Returns true if an app with the given UID has an activity running on the virtual display for
* this controller.
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 5ebbf07..be21075 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -624,6 +624,7 @@
mParams.getBlockedActivities(),
mParams.getDefaultActivityPolicy(),
createListenerAdapter(),
+ this::onEnteringPipBlocked,
this::onActivityBlocked,
this::onSecureWindowShown,
mAssociationInfo.getDeviceProfile());
@@ -779,6 +780,11 @@
return mVirtualDisplayIds.contains(displayId);
}
+ void onEnteringPipBlocked(int uid) {
+ showToastWhereUidIsRunning(uid, com.android.internal.R.string.vdm_pip_blocked,
+ Toast.LENGTH_LONG, mContext.getMainLooper());
+ }
+
interface OnDeviceCloseListener {
void onClose(int associationId);
}
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 553146d..84f2b63 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -168,7 +168,7 @@
"android.hardware.rebootescrow-V1-java",
"android.hardware.soundtrigger-V2.3-java",
"android.hardware.power.stats-V1-java",
- "android.hardware.power-V3-java",
+ "android.hardware.power-V4-java",
"android.hidl.manager-V1.2-java",
"capture_state_listener-aidl-java",
"icu4j_calendar_astronomer",
diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java
index 416de0a..c7c2655 100644
--- a/services/core/java/com/android/server/accounts/AccountManagerService.java
+++ b/services/core/java/com/android/server/accounts/AccountManagerService.java
@@ -1870,7 +1870,7 @@
}
if (accounts.accountsDb.findAllDeAccounts().size() > 100) {
Log.w(TAG, "insertAccountIntoDatabase: " + account.toSafeString()
- + ", skipping since more than 50 accounts on device exist");
+ + ", skipping since more than 100 accounts on device exist");
return false;
}
long accountId = accounts.accountsDb.insertCeAccount(account, password);
@@ -3520,10 +3520,10 @@
@Override
protected String toDebugString(long now) {
- String requiredFeaturesStr = TextUtils.join(",", requiredFeatures);
return super.toDebugString(now) + ", startAddAccountSession" + ", accountType "
+ accountType + ", requiredFeatures "
- + (requiredFeatures != null ? requiredFeaturesStr : null);
+ + (requiredFeatures != null
+ ? TextUtils.join(",", requiredFeatures) : "null");
}
}.bind();
} finally {
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index af6eaf4..7d64077 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -11173,9 +11173,9 @@
pw.printf("%s%s: %-60s (%s in swap)\n", prefix, stringifyKBSize(mi.pss),
mi.label, stringifyKBSize(mi.swapPss));
} else {
- pw.printf("%s%s: %s %s\n", prefix, stringifyKBSize(dumpPss ? mi.pss : mi.mRss),
+ pw.printf("%s%s: %s%s\n", prefix, stringifyKBSize(dumpPss ? mi.pss : mi.mRss),
mi.label,
- mi.userId != UserHandle.USER_SYSTEM ? "(user " + mi.userId + ")" : "");
+ mi.userId != UserHandle.USER_SYSTEM ? " (user " + mi.userId + ")" : "");
}
} else if (mi.isProc) {
pw.print("proc,"); pw.print(tag); pw.print(","); pw.print(mi.shortLabel);
diff --git a/services/core/java/com/android/server/am/BroadcastConstants.java b/services/core/java/com/android/server/am/BroadcastConstants.java
index dfac82c..56909e3 100644
--- a/services/core/java/com/android/server/am/BroadcastConstants.java
+++ b/services/core/java/com/android/server/am/BroadcastConstants.java
@@ -170,7 +170,7 @@
*/
public long DELAY_NORMAL_MILLIS = DEFAULT_DELAY_NORMAL_MILLIS;
private static final String KEY_DELAY_NORMAL_MILLIS = "bcast_delay_normal_millis";
- private static final long DEFAULT_DELAY_NORMAL_MILLIS = +500;
+ private static final long DEFAULT_DELAY_NORMAL_MILLIS = 0;
/**
* For {@link BroadcastQueueModernImpl}: Delay to apply to broadcasts
@@ -178,7 +178,7 @@
*/
public long DELAY_CACHED_MILLIS = DEFAULT_DELAY_CACHED_MILLIS;
private static final String KEY_DELAY_CACHED_MILLIS = "bcast_delay_cached_millis";
- private static final long DEFAULT_DELAY_CACHED_MILLIS = +120_000;
+ private static final long DEFAULT_DELAY_CACHED_MILLIS = +30_000;
/**
* For {@link BroadcastQueueModernImpl}: Delay to apply to urgent
diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
index 3dee262..6793876 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
@@ -37,7 +37,6 @@
import static com.android.server.am.OomAdjuster.OOM_ADJ_REASON_FINISH_RECEIVER;
import static com.android.server.am.OomAdjuster.OOM_ADJ_REASON_START_RECEIVER;
-import android.annotation.DurationMillisLong;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UptimeMillisLong;
@@ -240,11 +239,7 @@
}
case MSG_DELIVERY_TIMEOUT_SOFT: {
synchronized (mService) {
- final SomeArgs args = (SomeArgs) msg.obj;
- final BroadcastProcessQueue queue = (BroadcastProcessQueue) args.arg1;
- final long originalTimeout = args.argl1;
- args.recycle();
- deliveryTimeoutSoftLocked(queue, originalTimeout);
+ deliveryTimeoutSoftLocked((BroadcastProcessQueue) msg.obj);
}
return true;
}
@@ -752,11 +747,8 @@
queue.lastCpuDelayTime = queue.app.getCpuDelayTime();
final long timeout = r.isForeground() ? mFgConstants.TIMEOUT : mBgConstants.TIMEOUT;
- final SomeArgs args = SomeArgs.obtain();
- args.arg1 = queue;
- args.argl1 = timeout;
mLocalHandler.sendMessageDelayed(
- Message.obtain(mLocalHandler, MSG_DELIVERY_TIMEOUT_SOFT, args), timeout);
+ Message.obtain(mLocalHandler, MSG_DELIVERY_TIMEOUT_SOFT, queue), timeout);
}
if (r.allowBackgroundActivityStarts) {
@@ -843,16 +835,15 @@
r.resultTo = null;
}
- private void deliveryTimeoutSoftLocked(@NonNull BroadcastProcessQueue queue,
- @DurationMillisLong long originalTimeout) {
+ private void deliveryTimeoutSoftLocked(@NonNull BroadcastProcessQueue queue) {
if (queue.app != null) {
// Instead of immediately triggering an ANR, extend the timeout by
// the amount of time the process was runnable-but-waiting; we're
// only willing to do this once before triggering an hard ANR
final long cpuDelayTime = queue.app.getCpuDelayTime() - queue.lastCpuDelayTime;
- final long hardTimeout = MathUtils.constrain(cpuDelayTime, 0, originalTimeout);
+ final long timeout = MathUtils.constrain(cpuDelayTime, 0, mConstants.TIMEOUT);
mLocalHandler.sendMessageDelayed(
- Message.obtain(mLocalHandler, MSG_DELIVERY_TIMEOUT_HARD, queue), hardTimeout);
+ Message.obtain(mLocalHandler, MSG_DELIVERY_TIMEOUT_HARD, queue), timeout);
} else {
deliveryTimeoutHardLocked(queue);
}
diff --git a/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java b/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java
index 3c281d1..5114bd5 100644
--- a/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java
+++ b/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java
@@ -26,6 +26,7 @@
import static android.app.AppOpsManager.MODE_ALLOWED;
import static android.app.AppOpsManager.MODE_IGNORED;
import static android.app.AppOpsManager.OP_CAMERA;
+import static android.app.AppOpsManager.OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO;
import static android.app.AppOpsManager.OP_RECORD_AUDIO;
import static android.app.AppOpsManager.UID_STATE_FOREGROUND_SERVICE;
import static android.app.AppOpsManager.UID_STATE_MAX_LAST_NON_RESTRICTED;
@@ -171,6 +172,7 @@
return MODE_ALLOWED;
}
case OP_RECORD_AUDIO:
+ case OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO:
if ((capability & PROCESS_CAPABILITY_FOREGROUND_MICROPHONE) == 0) {
return MODE_IGNORED;
} else {
diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java
index fab7f1d..d971953 100644
--- a/services/core/java/com/android/server/biometrics/BiometricService.java
+++ b/services/core/java/com/android/server/biometrics/BiometricService.java
@@ -780,6 +780,8 @@
@Override // Binder call
public void resetLockout(
int userId, byte[] hardwareAuthToken) {
+ super.resetLockout_enforcePermission();
+
Slog.d(TAG, "resetLockout(userId=" + userId
+ ", hat=" + (hardwareAuthToken == null ? "null " : "present") + ")");
mBiometricContext.getAuthSessionCoordinator()
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java
index 48367b2..dca9ef2 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java
@@ -942,6 +942,8 @@
@android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
@Override
public void setUdfpsOverlay(@NonNull IUdfpsOverlay controller) {
+ super.setUdfpsOverlay_enforcePermission();
+
for (ServiceProvider provider : mRegistry.getProviders()) {
provider.setUdfpsOverlay(controller);
}
diff --git a/services/core/java/com/android/server/broadcastradio/hal2/BroadcastRadioService.java b/services/core/java/com/android/server/broadcastradio/hal2/BroadcastRadioService.java
index 5605737..4c37609 100644
--- a/services/core/java/com/android/server/broadcastradio/hal2/BroadcastRadioService.java
+++ b/services/core/java/com/android/server/broadcastradio/hal2/BroadcastRadioService.java
@@ -33,6 +33,7 @@
import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import java.util.Collection;
import java.util.HashMap;
@@ -132,10 +133,22 @@
}
}
+ @VisibleForTesting
+ BroadcastRadioService(int nextModuleId, Object lock, IServiceManager manager) {
+ mNextModuleId = nextModuleId;
+ mLock = lock;
+ Objects.requireNonNull(manager, "Service manager cannot be null");
+ try {
+ manager.registerForNotifications(IBroadcastRadio.kInterfaceName, "", mServiceListener);
+ } catch (RemoteException ex) {
+ Slog.e(TAG, "Failed to register for service notifications: ", ex);
+ }
+ }
+
public @NonNull Collection<RadioManager.ModuleProperties> listModules() {
Slog.v(TAG, "List HIDL 2.0 modules");
synchronized (mLock) {
- return mModules.values().stream().map(module -> module.mProperties)
+ return mModules.values().stream().map(module -> module.getProperties())
.collect(Collectors.toList());
}
}
@@ -154,7 +167,7 @@
public ITuner openSession(int moduleId, @Nullable RadioManager.BandConfig legacyConfig,
boolean withAudio, @NonNull ITunerCallback callback) throws RemoteException {
- Slog.v(TAG, "Open HIDL 2.0 session");
+ Slog.v(TAG, "Open HIDL 2.0 session with module id " + moduleId);
Objects.requireNonNull(callback);
if (!withAudio) {
diff --git a/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java b/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java
index 0a23e38..5913e068 100644
--- a/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java
+++ b/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java
@@ -58,7 +58,7 @@
private static final int RADIO_EVENT_LOGGER_QUEUE_SIZE = 25;
@NonNull private final IBroadcastRadio mService;
- @NonNull public final RadioManager.ModuleProperties mProperties;
+ @NonNull private final RadioManager.ModuleProperties mProperties;
private final Object mLock;
@NonNull private final Handler mHandler;
@@ -177,6 +177,10 @@
return mService;
}
+ public RadioManager.ModuleProperties getProperties() {
+ return mProperties;
+ }
+
public @NonNull TunerSession openSession(@NonNull android.hardware.radio.ITunerCallback userCb)
throws RemoteException {
mEventLogger.logRadioEvent("Open TunerSession");
diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java
index c06101f..bf0b388 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController2.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController2.java
@@ -2511,19 +2511,22 @@
float appliedThermalCapNits =
event.getThermalMax() == PowerManager.BRIGHTNESS_MAX
? -1f : convertToNits(event.getThermalMax());
-
- FrameworkStatsLog.write(FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED,
- convertToNits(event.getInitialBrightness()),
- convertToNits(event.getBrightness()),
- event.getSlowAmbientLux(),
- event.getPhysicalDisplayId(),
- event.isShortTermModelActive(),
- appliedLowPowerMode,
- appliedRbcStrength,
- appliedHbmMaxNits,
- appliedThermalCapNits,
- event.isAutomaticBrightnessEnabled(),
- FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__REASON__REASON_MANUAL);
+ if (mLogicalDisplay.getPrimaryDisplayDeviceLocked() != null
+ && mLogicalDisplay.getPrimaryDisplayDeviceLocked()
+ .getDisplayDeviceInfoLocked().type == Display.TYPE_INTERNAL) {
+ FrameworkStatsLog.write(FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED,
+ convertToNits(event.getInitialBrightness()),
+ convertToNits(event.getBrightness()),
+ event.getSlowAmbientLux(),
+ event.getPhysicalDisplayId(),
+ event.isShortTermModelActive(),
+ appliedLowPowerMode,
+ appliedRbcStrength,
+ appliedHbmMaxNits,
+ appliedThermalCapNits,
+ event.isAutomaticBrightnessEnabled(),
+ FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__REASON__REASON_MANUAL);
+ }
}
private final class DisplayControllerHandler extends Handler {
diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
index 16f1f23..cb97e28 100644
--- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java
+++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
@@ -180,12 +180,6 @@
LogicalDisplayMapper(@NonNull Context context, @NonNull DisplayDeviceRepository repo,
@NonNull Listener listener, @NonNull DisplayManagerService.SyncRoot syncRoot,
@NonNull Handler handler) {
- this(context, repo, listener, syncRoot, handler, new DeviceStateToLayoutMap());
- }
-
- LogicalDisplayMapper(@NonNull Context context, @NonNull DisplayDeviceRepository repo,
- @NonNull Listener listener, @NonNull DisplayManagerService.SyncRoot syncRoot,
- @NonNull Handler handler, DeviceStateToLayoutMap deviceStateToLayoutMap) {
mSyncRoot = syncRoot;
mPowerManager = context.getSystemService(PowerManager.class);
mInteractive = mPowerManager.isInteractive();
@@ -200,7 +194,7 @@
mDeviceStatesOnWhichToSleep = toSparseBooleanArray(context.getResources().getIntArray(
com.android.internal.R.array.config_deviceStatesOnWhichToSleep));
mDisplayDeviceRepo.addListener(this);
- mDeviceStateToLayoutMap = deviceStateToLayoutMap;
+ mDeviceStateToLayoutMap = new DeviceStateToLayoutMap();
}
@Override
@@ -401,7 +395,9 @@
// the transition is smooth. Plus, on some devices, only one internal displays can be
// on at a time. We use DISPLAY_PHASE_LAYOUT_TRANSITION to mark a display that needs to be
// temporarily turned off.
- resetLayoutLocked(mDeviceState, state, LogicalDisplay.DISPLAY_PHASE_LAYOUT_TRANSITION);
+ if (mDeviceState != DeviceStateManager.INVALID_DEVICE_STATE) {
+ resetLayoutLocked(mDeviceState, state, LogicalDisplay.DISPLAY_PHASE_LAYOUT_TRANSITION);
+ }
mPendingDeviceState = state;
final boolean wakeDevice = shouldDeviceBeWoken(mPendingDeviceState, mDeviceState,
mInteractive, mBootCompleted);
@@ -944,8 +940,8 @@
newDisplay.swapDisplaysLocked(oldDisplay);
}
- if (displayLayout.isEnabled()) {
- setDisplayPhase(newDisplay, LogicalDisplay.DISPLAY_PHASE_ENABLED);
+ if (!displayLayout.isEnabled()) {
+ setDisplayPhase(newDisplay, LogicalDisplay.DISPLAY_PHASE_DISABLED);
}
}
@@ -965,7 +961,7 @@
final LogicalDisplay display = new LogicalDisplay(displayId, layerStack, device);
display.updateLocked(mDisplayDeviceRepo);
mLogicalDisplays.put(displayId, display);
- setDisplayPhase(display, LogicalDisplay.DISPLAY_PHASE_DISABLED);
+ setDisplayPhase(display, LogicalDisplay.DISPLAY_PHASE_ENABLED);
return display;
}
diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java
index facc6b2..4a0ba22 100644
--- a/services/core/java/com/android/server/dreams/DreamManagerService.java
+++ b/services/core/java/com/android/server/dreams/DreamManagerService.java
@@ -116,7 +116,7 @@
private final DreamUiEventLogger mDreamUiEventLogger;
private final ComponentName mAmbientDisplayComponent;
private final boolean mDismissDreamOnActivityStart;
- private final boolean mDreamsOnlyEnabledForSystemUser;
+ private final boolean mDreamsOnlyEnabledForDockUser;
private final boolean mDreamsEnabledByDefaultConfig;
private final boolean mDreamsActivatedOnChargeByDefault;
private final boolean mDreamsActivatedOnDockByDefault;
@@ -214,8 +214,8 @@
mContext.getResources().getStringArray(R.array.config_loggable_dream_prefixes));
AmbientDisplayConfiguration adc = new AmbientDisplayConfiguration(mContext);
mAmbientDisplayComponent = ComponentName.unflattenFromString(adc.ambientDisplayComponent());
- mDreamsOnlyEnabledForSystemUser =
- mContext.getResources().getBoolean(R.bool.config_dreamsOnlyEnabledForSystemUser);
+ mDreamsOnlyEnabledForDockUser =
+ mContext.getResources().getBoolean(R.bool.config_dreamsOnlyEnabledForDockUser);
mDismissDreamOnActivityStart = mContext.getResources().getBoolean(
R.bool.config_dismissDreamOnActivityStart);
@@ -292,10 +292,9 @@
pw.println();
pw.println("mCurrentDream=" + mCurrentDream);
pw.println("mForceAmbientDisplayEnabled=" + mForceAmbientDisplayEnabled);
- pw.println("mDreamsOnlyEnabledForSystemUser=" + mDreamsOnlyEnabledForSystemUser);
+ pw.println("mDreamsOnlyEnabledForDockUser=" + mDreamsOnlyEnabledForDockUser);
pw.println("mDreamsEnabledSetting=" + mDreamsEnabledSetting);
pw.println("mForceAmbientDisplayEnabled=" + mForceAmbientDisplayEnabled);
- pw.println("mDreamsOnlyEnabledForSystemUser=" + mDreamsOnlyEnabledForSystemUser);
pw.println("mDreamsActivatedOnDockByDefault=" + mDreamsActivatedOnDockByDefault);
pw.println("mDreamsActivatedOnChargeByDefault=" + mDreamsActivatedOnChargeByDefault);
pw.println("mIsDocked=" + mIsDocked);
@@ -602,7 +601,8 @@
}
private boolean dreamsEnabledForUser(int userId) {
- return !mDreamsOnlyEnabledForSystemUser || (userId == UserHandle.USER_SYSTEM);
+ // TODO(b/257333623): Support non-system Dock Users in HSUM.
+ return !mDreamsOnlyEnabledForDockUser || (userId == UserHandle.USER_SYSTEM);
}
private ServiceInfo getServiceInfo(ComponentName name) {
diff --git a/services/core/java/com/android/server/input/BatteryController.java b/services/core/java/com/android/server/input/BatteryController.java
index 9d4f181..c83fa2d 100644
--- a/services/core/java/com/android/server/input/BatteryController.java
+++ b/services/core/java/com/android/server/input/BatteryController.java
@@ -32,6 +32,7 @@
import android.os.UEventObserver;
import android.util.ArrayMap;
import android.util.ArraySet;
+import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.Slog;
import android.view.InputDevice;
@@ -382,24 +383,28 @@
}
}
- public void dump(PrintWriter pw, String prefix) {
+ public void dump(PrintWriter pw) {
+ IndentingPrintWriter ipw = new IndentingPrintWriter(pw);
synchronized (mLock) {
- final String indent = prefix + " ";
- final String indent2 = indent + " ";
-
- pw.println(prefix + TAG + ":");
- pw.println(indent + "State: Polling = " + mIsPolling
+ ipw.println(TAG + ":");
+ ipw.increaseIndent();
+ ipw.println("State: Polling = " + mIsPolling
+ ", Interactive = " + mIsInteractive);
- pw.println(indent + "Listeners: " + mListenerRecords.size() + " battery listeners");
+ ipw.println("Listeners: " + mListenerRecords.size() + " battery listeners");
+ ipw.increaseIndent();
for (int i = 0; i < mListenerRecords.size(); i++) {
- pw.println(indent2 + i + ": " + mListenerRecords.valueAt(i));
+ ipw.println(i + ": " + mListenerRecords.valueAt(i));
}
+ ipw.decreaseIndent();
- pw.println(indent + "Device Monitors: " + mDeviceMonitors.size() + " monitors");
+ ipw.println("Device Monitors: " + mDeviceMonitors.size() + " monitors");
+ ipw.increaseIndent();
for (int i = 0; i < mDeviceMonitors.size(); i++) {
- pw.println(indent2 + i + ": " + mDeviceMonitors.valueAt(i));
+ ipw.println(i + ": " + mDeviceMonitors.valueAt(i));
}
+ ipw.decreaseIndent();
+ ipw.decreaseIndent();
}
}
diff --git a/services/core/java/com/android/server/input/InputManagerInternal.java b/services/core/java/com/android/server/input/InputManagerInternal.java
index 6234421..298098a 100644
--- a/services/core/java/com/android/server/input/InputManagerInternal.java
+++ b/services/core/java/com/android/server/input/InputManagerInternal.java
@@ -17,10 +17,15 @@
package com.android.server.input;
import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
import android.graphics.PointF;
import android.hardware.display.DisplayViewport;
import android.os.IBinder;
import android.view.InputChannel;
+import android.view.inputmethod.InputMethodSubtype;
+
+import com.android.internal.inputmethod.InputMethodSubtypeHandle;
import java.util.List;
@@ -136,6 +141,26 @@
public abstract InputChannel createInputChannel(String inputChannelName);
/**
+ * Pilfer pointers from the input channel with the given token so that ongoing gestures are
+ * canceled for all other channels.
+ */
+ public abstract void pilferPointers(IBinder token);
+
+ /**
+ * Called when the current input method and/or {@link InputMethodSubtype} is updated.
+ *
+ * @param userId User ID to be notified about.
+ * @param subtypeHandle A {@link InputMethodSubtypeHandle} corresponds to {@code subtype}.
+ * @param subtype A {@link InputMethodSubtype} object, or {@code null} when the current
+ * {@link InputMethodSubtype} is not suitable for the physical keyboard layout
+ * mapping.
+ * @see InputMethodSubtype#isSuitableForPhysicalKeyboardLayoutMapping()
+ */
+ public abstract void onInputMethodSubtypeChangedForKeyboardLayoutMapping(@UserIdInt int userId,
+ @Nullable InputMethodSubtypeHandle subtypeHandle,
+ @Nullable InputMethodSubtype subtype);
+
+ /**
* Increments keyboard backlight level if the device has an associated keyboard backlight
* {@see Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT}
*/
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index d2282c3..e5c48f9 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -23,6 +23,7 @@
import android.annotation.EnforcePermission;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.UserIdInt;
import android.app.ActivityManagerInternal;
import android.app.Notification;
import android.app.NotificationManager;
@@ -91,6 +92,7 @@
import android.provider.Settings.SettingNotFoundException;
import android.text.TextUtils;
import android.util.ArrayMap;
+import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArray;
@@ -110,11 +112,13 @@
import android.view.SurfaceControl;
import android.view.VerifiedInputEvent;
import android.view.ViewConfiguration;
+import android.view.inputmethod.InputMethodSubtype;
import android.widget.Toast;
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.inputmethod.InputMethodSubtypeHandle;
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.internal.notification.SystemNotificationChannels;
import com.android.internal.os.SomeArgs;
@@ -302,9 +306,9 @@
private final AdditionalDisplayInputProperties mCurrentDisplayProperties =
new AdditionalDisplayInputProperties();
@GuardedBy("mAdditionalDisplayInputPropertiesLock")
- private int mIconType = PointerIcon.TYPE_NOT_SPECIFIED;
+ private int mPointerIconType = PointerIcon.TYPE_NOT_SPECIFIED;
@GuardedBy("mAdditionalDisplayInputPropertiesLock")
- private PointerIcon mIcon;
+ private PointerIcon mPointerIcon;
// Holds all the registered gesture monitors that are implemented as spy windows. The spy
// windows are mapped by their InputChannel tokens.
@@ -2325,12 +2329,12 @@
throw new IllegalArgumentException("Use setCustomPointerIcon to set custom pointers");
}
synchronized (mAdditionalDisplayInputPropertiesLock) {
- mIcon = null;
- mIconType = iconType;
+ mPointerIcon = null;
+ mPointerIconType = iconType;
if (!mCurrentDisplayProperties.pointerIconVisible) return;
- mNative.setPointerIconType(mIconType);
+ mNative.setPointerIconType(mPointerIconType);
}
}
@@ -2339,12 +2343,12 @@
public void setCustomPointerIcon(PointerIcon icon) {
Objects.requireNonNull(icon);
synchronized (mAdditionalDisplayInputPropertiesLock) {
- mIconType = PointerIcon.TYPE_CUSTOM;
- mIcon = icon;
+ mPointerIconType = PointerIcon.TYPE_CUSTOM;
+ mPointerIcon = icon;
if (!mCurrentDisplayProperties.pointerIconVisible) return;
- mNative.setCustomPointerIcon(mIcon);
+ mNative.setCustomPointerIcon(mPointerIcon);
}
}
@@ -2676,6 +2680,8 @@
@EnforcePermission(Manifest.permission.BLUETOOTH)
@Override
public String getInputDeviceBluetoothAddress(int deviceId) {
+ super.getInputDeviceBluetoothAddress_enforcePermission();
+
return mNative.getBluetoothAddress(deviceId);
}
@@ -2689,74 +2695,78 @@
@Override
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
+ IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " ");
- pw.println("INPUT MANAGER (dumpsys input)\n");
+ ipw.println("INPUT MANAGER (dumpsys input)\n");
String dumpStr = mNative.dump();
if (dumpStr != null) {
pw.println(dumpStr);
}
- pw.println("Input Manager Service (Java) State:");
- dumpAssociations(pw, " " /*prefix*/);
- dumpSpyWindowGestureMonitors(pw, " " /*prefix*/);
- dumpDisplayInputPropertiesValues(pw, " " /*prefix*/);
- mBatteryController.dump(pw, " " /*prefix*/);
- mKeyboardBacklightController.dump(pw, " " /*prefix*/);
+ ipw.println("Input Manager Service (Java) State:");
+ ipw.increaseIndent();
+ dumpAssociations(ipw);
+ dumpSpyWindowGestureMonitors(ipw);
+ dumpDisplayInputPropertiesValues(ipw);
+ mBatteryController.dump(ipw);
+ mKeyboardBacklightController.dump(ipw);
}
- private void dumpAssociations(PrintWriter pw, String prefix) {
+ private void dumpAssociations(IndentingPrintWriter pw) {
if (!mStaticAssociations.isEmpty()) {
- pw.println(prefix + "Static Associations:");
+ pw.println("Static Associations:");
mStaticAssociations.forEach((k, v) -> {
- pw.print(prefix + " port: " + k);
+ pw.print(" port: " + k);
pw.println(" display: " + v);
});
}
synchronized (mAssociationsLock) {
if (!mRuntimeAssociations.isEmpty()) {
- pw.println(prefix + "Runtime Associations:");
+ pw.println("Runtime Associations:");
mRuntimeAssociations.forEach((k, v) -> {
- pw.print(prefix + " port: " + k);
+ pw.print(" port: " + k);
pw.println(" display: " + v);
});
}
if (!mUniqueIdAssociations.isEmpty()) {
- pw.println(prefix + "Unique Id Associations:");
+ pw.println("Unique Id Associations:");
mUniqueIdAssociations.forEach((k, v) -> {
- pw.print(prefix + " port: " + k);
+ pw.print(" port: " + k);
pw.println(" uniqueId: " + v);
});
}
}
}
- private void dumpSpyWindowGestureMonitors(PrintWriter pw, String prefix) {
+ private void dumpSpyWindowGestureMonitors(IndentingPrintWriter pw) {
synchronized (mInputMonitors) {
if (mInputMonitors.isEmpty()) return;
- pw.println(prefix + "Gesture Monitors (implemented as spy windows):");
+ pw.println("Gesture Monitors (implemented as spy windows):");
int i = 0;
for (final GestureMonitorSpyWindow monitor : mInputMonitors.values()) {
- pw.append(prefix + " " + i++ + ": ").println(monitor.dump());
+ pw.append(" " + i++ + ": ").println(monitor.dump());
}
}
}
- private void dumpDisplayInputPropertiesValues(PrintWriter pw, String prefix) {
+ private void dumpDisplayInputPropertiesValues(IndentingPrintWriter pw) {
synchronized (mAdditionalDisplayInputPropertiesLock) {
if (mAdditionalDisplayInputProperties.size() != 0) {
- pw.println(prefix + "mAdditionalDisplayInputProperties:");
+ pw.println("mAdditionalDisplayInputProperties:");
+ pw.increaseIndent();
for (int i = 0; i < mAdditionalDisplayInputProperties.size(); i++) {
- pw.println(prefix + " displayId: "
+ pw.println("displayId: "
+ mAdditionalDisplayInputProperties.keyAt(i));
final AdditionalDisplayInputProperties properties =
mAdditionalDisplayInputProperties.valueAt(i);
- pw.println(prefix + " pointerAcceleration: " + properties.pointerAcceleration);
- pw.println(prefix + " pointerIconVisible: " + properties.pointerIconVisible);
+ pw.println("pointerAcceleration: " + properties.pointerAcceleration);
+ pw.println("pointerIconVisible: " + properties.pointerIconVisible);
}
+ pw.decreaseIndent();
}
if (mOverriddenPointerDisplayId != Display.INVALID_DISPLAY) {
- pw.println(prefix + "mOverriddenPointerDisplayId: " + mOverriddenPointerDisplayId);
+ pw.println("mOverriddenPointerDisplayId: " + mOverriddenPointerDisplayId);
}
}
}
@@ -3782,6 +3792,21 @@
}
@Override
+ public void pilferPointers(IBinder token) {
+ mNative.pilferPointers(token);
+ }
+
+ @Override
+ public void onInputMethodSubtypeChangedForKeyboardLayoutMapping(@UserIdInt int userId,
+ @Nullable InputMethodSubtypeHandle subtypeHandle,
+ @Nullable InputMethodSubtype subtype) {
+ if (DEBUG) {
+ Slog.i(TAG, "InputMethodSubtype changed: userId=" + userId
+ + " subtypeHandle=" + subtypeHandle);
+ }
+ }
+
+ @Override
public void incrementKeyboardBacklight(int deviceId) {
mKeyboardBacklightController.incrementKeyboardBacklight(deviceId);
}
@@ -3841,11 +3866,11 @@
if (properties.pointerIconVisible != mCurrentDisplayProperties.pointerIconVisible) {
mCurrentDisplayProperties.pointerIconVisible = properties.pointerIconVisible;
if (properties.pointerIconVisible) {
- if (mIconType == PointerIcon.TYPE_CUSTOM) {
- Objects.requireNonNull(mIcon);
- mNative.setCustomPointerIcon(mIcon);
+ if (mPointerIconType == PointerIcon.TYPE_CUSTOM) {
+ Objects.requireNonNull(mPointerIcon);
+ mNative.setCustomPointerIcon(mPointerIcon);
} else {
- mNative.setPointerIconType(mIconType);
+ mNative.setPointerIconType(mPointerIconType);
}
} else {
mNative.setPointerIconType(PointerIcon.TYPE_NULL);
diff --git a/services/core/java/com/android/server/input/KeyboardBacklightController.java b/services/core/java/com/android/server/input/KeyboardBacklightController.java
index e33f28c..b207e27 100644
--- a/services/core/java/com/android/server/input/KeyboardBacklightController.java
+++ b/services/core/java/com/android/server/input/KeyboardBacklightController.java
@@ -24,6 +24,7 @@
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
+import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArray;
@@ -216,12 +217,14 @@
return null;
}
- void dump(PrintWriter pw, String prefix) {
- pw.println(prefix + TAG + ": " + mKeyboardBacklights.size() + " keyboard backlights");
+ void dump(PrintWriter pw) {
+ IndentingPrintWriter ipw = new IndentingPrintWriter(pw);
+ ipw.println(TAG + ": " + mKeyboardBacklights.size() + " keyboard backlights");
+ ipw.increaseIndent();
for (int i = 0; i < mKeyboardBacklights.size(); i++) {
Light light = mKeyboardBacklights.get(i);
- pw.println(prefix + " " + i + ": { id: " + light.getId() + ", name: " + light.getName()
- + " }");
+ ipw.println(i + ": { id: " + light.getId() + ", name: " + light.getName() + " }");
}
+ ipw.decreaseIndent();
}
}
diff --git a/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java b/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java
index 1a0f6f7..015e576 100644
--- a/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java
+++ b/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java
@@ -28,6 +28,7 @@
import android.view.InputChannel;
import android.view.MotionEvent;
import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.ImeTracker;
import android.view.inputmethod.InputBinding;
import android.view.inputmethod.InputMethodSubtype;
import android.window.ImeOnBackInvokedDispatcher;
@@ -198,9 +199,10 @@
// TODO(b/192412909): Convert this back to void method
@AnyThread
- boolean showSoftInput(IBinder showInputToken, int flags, ResultReceiver resultReceiver) {
+ boolean showSoftInput(IBinder showInputToken, @Nullable ImeTracker.Token statsToken, int flags,
+ ResultReceiver resultReceiver) {
try {
- mTarget.showSoftInput(showInputToken, flags, resultReceiver);
+ mTarget.showSoftInput(showInputToken, statsToken, flags, resultReceiver);
} catch (RemoteException e) {
logRemoteException(e);
return false;
@@ -210,9 +212,10 @@
// TODO(b/192412909): Convert this back to void method
@AnyThread
- boolean hideSoftInput(IBinder hideInputToken, int flags, ResultReceiver resultReceiver) {
+ boolean hideSoftInput(IBinder hideInputToken, @Nullable ImeTracker.Token statsToken, int flags,
+ ResultReceiver resultReceiver) {
try {
- mTarget.hideSoftInput(hideInputToken, flags, resultReceiver);
+ mTarget.hideSoftInput(hideInputToken, statsToken, flags, resultReceiver);
} catch (RemoteException e) {
logRemoteException(e);
return false;
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 4d1c5ae..8b083bd 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -129,6 +129,7 @@
import android.view.WindowManager.LayoutParams;
import android.view.WindowManager.LayoutParams.SoftInputModeFlags;
import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.ImeTracker;
import android.view.inputmethod.InputBinding;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethod;
@@ -151,6 +152,7 @@
import com.android.internal.inputmethod.IAccessibilityInputMethodSession;
import com.android.internal.inputmethod.IInlineSuggestionsRequestCallback;
import com.android.internal.inputmethod.IInputContentUriToken;
+import com.android.internal.inputmethod.IInputMethod;
import com.android.internal.inputmethod.IInputMethodClient;
import com.android.internal.inputmethod.IInputMethodPrivilegedOperations;
import com.android.internal.inputmethod.IInputMethodSession;
@@ -162,6 +164,7 @@
import com.android.internal.inputmethod.InputBindResult;
import com.android.internal.inputmethod.InputMethodDebug;
import com.android.internal.inputmethod.InputMethodNavButtonFlags;
+import com.android.internal.inputmethod.InputMethodSubtypeHandle;
import com.android.internal.inputmethod.SoftInputShowHideReason;
import com.android.internal.inputmethod.StartInputFlags;
import com.android.internal.inputmethod.StartInputReason;
@@ -641,6 +644,10 @@
*/
private boolean mInputShown;
+ /** The token tracking the current IME request or {@code null} otherwise. */
+ @Nullable
+ private ImeTracker.Token mCurStatsToken;
+
/**
* {@code true} if the current input method is in fullscreen mode.
*/
@@ -760,7 +767,7 @@
* <dd>
* If this bit is ON, some of IME view, e.g. software input, candidate view, is visible.
* </dd>
- * dt>{@link InputMethodService#IME_INVISIBLE}</dt>
+ * <dt>{@link InputMethodService#IME_INVISIBLE}</dt>
* <dd> If this bit is ON, IME is ready with views from last EditorInfo but is
* currently invisible.
* </dd>
@@ -784,8 +791,7 @@
/**
* Internal state snapshot when
- * {@link com.android.internal.view.IInputMethod#startInput(IBinder, IRemoteInputConnection, EditorInfo,
- * boolean)} is about to be called.
+ * {@link IInputMethod#startInput(IInputMethod.StartInputParams)} is about to be called.
*
* <p>Calling that IPC endpoint basically means that
* {@link InputMethodService#doStartInput(InputConnection, EditorInfo, boolean)} will be called
@@ -1070,7 +1076,7 @@
/**
* Add a new entry and discard the oldest entry as needed.
- * @param info {@lin StartInputInfo} to be added.
+ * @param info {@link StartInputInfo} to be added.
*/
void addEntry(@NonNull StartInputInfo info) {
final int index = mNextIndex;
@@ -1188,18 +1194,18 @@
} else if (accessibilityRequestingNoImeUri.equals(uri)) {
final int accessibilitySoftKeyboardSetting = Settings.Secure.getIntForUser(
mContext.getContentResolver(),
- Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE, 0, mUserId);
+ Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE, 0 /* def */, mUserId);
mAccessibilityRequestingNoSoftKeyboard =
(accessibilitySoftKeyboardSetting & AccessibilityService.SHOW_MODE_MASK)
== AccessibilityService.SHOW_MODE_HIDDEN;
if (mAccessibilityRequestingNoSoftKeyboard) {
final boolean showRequested = mShowRequested;
- hideCurrentInputLocked(mCurFocusedWindow, 0, null,
+ hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */,
+ 0 /* flags */, null /* resultReceiver */,
SoftInputShowHideReason.HIDE_SETTINGS_ON_CHANGE);
mShowRequested = showRequested;
} else if (mShowRequested) {
- showCurrentInputLocked(mCurFocusedWindow,
- InputMethodManager.SHOW_IMPLICIT, null,
+ showCurrentInputImplicitLocked(mCurFocusedWindow,
SoftInputShowHideReason.SHOW_SETTINGS_ON_CHANGE);
}
} else {
@@ -1665,8 +1671,8 @@
}
// Hide soft input before user switch task since switch task may block main handler a while
// and delayed the hideCurrentInputLocked().
- hideCurrentInputLocked(
- mCurFocusedWindow, 0, null, SoftInputShowHideReason.HIDE_SWITCH_USER);
+ hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
+ null /* resultReceiver */, SoftInputShowHideReason.HIDE_SWITCH_USER);
final UserSwitchHandlerTask task = new UserSwitchHandlerTask(this, userId,
clientToBeReset);
mUserSwitchHandlerTask = task;
@@ -2221,7 +2227,7 @@
}
final ClientDeathRecipient deathRecipient = new ClientDeathRecipient(this, client);
try {
- client.asBinder().linkToDeath(deathRecipient, 0);
+ client.asBinder().linkToDeath(deathRecipient, 0 /* flags */);
} catch (RemoteException e) {
throw new IllegalStateException(e);
}
@@ -2246,7 +2252,7 @@
synchronized (ImfLock.class) {
ClientState cs = mClients.remove(client.asBinder());
if (cs != null) {
- client.asBinder().unlinkToDeath(cs.mClientDeathRecipient, 0);
+ client.asBinder().unlinkToDeath(cs.mClientDeathRecipient, 0 /* flags */);
clearClientSessionLocked(cs);
clearClientSessionForAccessibilityLocked(cs);
@@ -2259,8 +2265,8 @@
}
if (mCurClient == cs) {
- hideCurrentInputLocked(
- mCurFocusedWindow, 0, null, SoftInputShowHideReason.HIDE_REMOVE_CLIENT);
+ hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
+ null /* resultReceiver */, SoftInputShowHideReason.HIDE_REMOVE_CLIENT);
if (mBoundToMethod) {
mBoundToMethod = false;
IInputMethodInvoker curMethod = getCurMethodLocked();
@@ -2307,6 +2313,8 @@
mCurClient.mSessionRequestedForAccessibility = false;
mCurClient = null;
mCurVirtualDisplayToScreenMatrix = null;
+ ImeTracker.get().onFailed(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME);
+ mCurStatsToken = null;
mMenuController.hideInputMethodMenuLocked();
}
@@ -2379,8 +2387,11 @@
navButtonFlags, mCurImeDispatcher);
if (mShowRequested) {
if (DEBUG) Slog.v(TAG, "Attach new input asks to show input");
- showCurrentInputLocked(mCurFocusedWindow, getAppShowFlagsLocked(), null,
- SoftInputShowHideReason.ATTACH_NEW_INPUT);
+ // Re-use current statsToken, if it exists.
+ final ImeTracker.Token statsToken = mCurStatsToken;
+ mCurStatsToken = null;
+ showCurrentInputLocked(mCurFocusedWindow, statsToken, getAppShowFlagsLocked(),
+ null /* resultReceiver */, SoftInputShowHideReason.ATTACH_NEW_INPUT);
}
String curId = getCurIdLocked();
@@ -2499,7 +2510,8 @@
if (mDisplayIdToShowIme == INVALID_DISPLAY) {
mImeHiddenByDisplayPolicy = true;
- hideCurrentInputLocked(mCurFocusedWindow, 0, null,
+ hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
+ null /* resultReceiver */,
SoftInputShowHideReason.HIDE_DISPLAY_IME_POLICY_HIDE);
return InputBindResult.NO_IME;
}
@@ -3201,6 +3213,18 @@
}
@GuardedBy("ImfLock.class")
+ private void notifyInputMethodSubtypeChangedLocked(@UserIdInt int userId,
+ @NonNull InputMethodInfo imi, @Nullable InputMethodSubtype subtype) {
+ final InputMethodSubtype normalizedSubtype =
+ subtype != null && subtype.isSuitableForPhysicalKeyboardLayoutMapping()
+ ? subtype : null;
+ final InputMethodSubtypeHandle newSubtypeHandle = normalizedSubtype != null
+ ? InputMethodSubtypeHandle.of(imi, normalizedSubtype) : null;
+ mInputManagerInternal.onInputMethodSubtypeChangedForKeyboardLayoutMapping(
+ userId, newSubtypeHandle, normalizedSubtype);
+ }
+
+ @GuardedBy("ImfLock.class")
void setInputMethodLocked(String id, int subtypeId) {
InputMethodInfo info = mMethodMap.get(id);
if (info == null) {
@@ -3209,8 +3233,10 @@
// See if we need to notify a subtype change within the same IME.
if (id.equals(getSelectedMethodIdLocked())) {
+ final int userId = mSettings.getCurrentUserId();
final int subtypeCount = info.getSubtypeCount();
if (subtypeCount <= 0) {
+ notifyInputMethodSubtypeChangedLocked(userId, info, null);
return;
}
final InputMethodSubtype oldSubtype = mCurrentSubtype;
@@ -3225,6 +3251,7 @@
if (newSubtype == null || oldSubtype == null) {
Slog.w(TAG, "Illegal subtype state: old subtype = " + oldSubtype
+ ", new subtype = " + newSubtype);
+ notifyInputMethodSubtypeChangedLocked(userId, info, null);
return;
}
if (newSubtype != oldSubtype) {
@@ -3262,22 +3289,23 @@
}
@Override
- public boolean showSoftInput(IInputMethodClient client, IBinder windowToken, int flags,
- int lastClickTooType, ResultReceiver resultReceiver,
- @SoftInputShowHideReason int reason) {
+ public boolean showSoftInput(IInputMethodClient client, IBinder windowToken,
+ @Nullable ImeTracker.Token statsToken, int flags, int lastClickTooType,
+ ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.showSoftInput");
int uid = Binder.getCallingUid();
ImeTracing.getInstance().triggerManagerServiceDump(
"InputMethodManagerService#showSoftInput");
synchronized (ImfLock.class) {
- if (!canInteractWithImeLocked(uid, client, "showSoftInput")) {
+ if (!canInteractWithImeLocked(uid, client, "showSoftInput", statsToken)) {
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_CLIENT_FOCUSED);
return false;
}
final long ident = Binder.clearCallingIdentity();
try {
if (DEBUG) Slog.v(TAG, "Client requesting input be shown");
- return showCurrentInputLocked(
- windowToken, flags, lastClickTooType, resultReceiver, reason);
+ return showCurrentInputLocked(windowToken, statsToken, flags, lastClickTooType,
+ resultReceiver, reason);
} finally {
Binder.restoreCallingIdentity(ident);
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
@@ -3294,7 +3322,8 @@
"InputMethodManagerService#startStylusHandwriting");
int uid = Binder.getCallingUid();
synchronized (ImfLock.class) {
- if (!canInteractWithImeLocked(uid, client, "startStylusHandwriting")) {
+ if (!canInteractWithImeLocked(uid, client, "startStylusHandwriting",
+ null /* statsToken */)) {
return;
}
if (!hasSupportedStylusLocked()) {
@@ -3349,19 +3378,33 @@
}
@GuardedBy("ImfLock.class")
- boolean showCurrentInputLocked(IBinder windowToken, int flags,
- ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
- return showCurrentInputLocked(
- windowToken, flags, MotionEvent.TOOL_TYPE_UNKNOWN, resultReceiver, reason);
+ boolean showCurrentInputLocked(IBinder windowToken, @Nullable ImeTracker.Token statsToken,
+ int flags, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+ return showCurrentInputLocked(windowToken, statsToken, flags,
+ MotionEvent.TOOL_TYPE_UNKNOWN, resultReceiver, reason);
}
@GuardedBy("ImfLock.class")
- private boolean showCurrentInputLocked(IBinder windowToken, int flags, int lastClickToolType,
+ private boolean showCurrentInputLocked(IBinder windowToken,
+ @Nullable ImeTracker.Token statsToken, int flags, int lastClickToolType,
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);
+ }
+
mShowRequested = true;
if (mAccessibilityRequestingNoSoftKeyboard || mImeHiddenByDisplayPolicy) {
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_ACCESSIBILITY);
return false;
}
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_ACCESSIBILITY);
if ((flags & InputMethodManager.SHOW_FORCED) != 0) {
mShowExplicitlyRequested = true;
@@ -3371,8 +3414,10 @@
}
if (!mSystemReady) {
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_SYSTEM_READY);
return false;
}
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_SYSTEM_READY);
mBindingController.setCurrentMethodVisible();
final IInputMethodInvoker curMethod = getCurMethodLocked();
@@ -3380,6 +3425,9 @@
// create a placeholder token for IMS so that IMS cannot inject windows into client app.
Binder showInputToken = new Binder();
mShowRequestWindowMap.put(showInputToken, windowToken);
+ ImeTracker.get().onCancelled(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME);
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_HAS_IME);
+ mCurStatsToken = null;
final int showFlags = getImeShowFlagsLocked();
if (DEBUG) {
Slog.v(TAG, "Calling " + curMethod + ".showSoftInput(" + showInputToken
@@ -3391,23 +3439,34 @@
curMethod.updateEditorToolType(lastClickToolType);
}
// TODO(b/192412909): Check if we can always call onShowHideSoftInputRequested() or not.
- if (curMethod.showSoftInput(showInputToken, showFlags, resultReceiver)) {
+ if (curMethod.showSoftInput(showInputToken, statsToken, showFlags, resultReceiver)) {
onShowHideSoftInputRequested(true /* show */, windowToken, reason);
}
mInputShown = true;
return true;
+ } else {
+ ImeTracker.get().onCancelled(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME);
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_WAIT_IME);
+ mCurStatsToken = statsToken;
}
return false;
}
@Override
- public boolean hideSoftInput(IInputMethodClient client, IBinder windowToken, int flags,
- ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+ public boolean hideSoftInput(IInputMethodClient client, IBinder windowToken,
+ @Nullable ImeTracker.Token statsToken, int flags, ResultReceiver resultReceiver,
+ @SoftInputShowHideReason int reason) {
int uid = Binder.getCallingUid();
ImeTracing.getInstance().triggerManagerServiceDump(
"InputMethodManagerService#hideSoftInput");
synchronized (ImfLock.class) {
- if (!canInteractWithImeLocked(uid, client, "hideSoftInput")) {
+ if (!canInteractWithImeLocked(uid, client, "hideSoftInput", statsToken)) {
+ if (mInputShown) {
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_CLIENT_FOCUSED);
+ } else {
+ ImeTracker.get().onCancelled(statsToken,
+ ImeTracker.PHASE_SERVER_CLIENT_FOCUSED);
+ }
return false;
}
final long ident = Binder.clearCallingIdentity();
@@ -3415,7 +3474,7 @@
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.hideSoftInput");
if (DEBUG) Slog.v(TAG, "Client requesting input be hidden");
return InputMethodManagerService.this.hideCurrentInputLocked(windowToken,
- flags, resultReceiver, reason);
+ statsToken, flags, resultReceiver, reason);
} finally {
Binder.restoreCallingIdentity(ident);
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
@@ -3424,17 +3483,32 @@
}
@GuardedBy("ImfLock.class")
- boolean hideCurrentInputLocked(IBinder windowToken, int flags, ResultReceiver resultReceiver,
- @SoftInputShowHideReason int reason) {
+ boolean hideCurrentInputLocked(IBinder windowToken, @Nullable ImeTracker.Token statsToken,
+ int flags, 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().onRequestHide(statsToken, ImeTracker.ORIGIN_SERVER_HIDE_INPUT, reason);
+ }
+
if ((flags & InputMethodManager.HIDE_IMPLICIT_ONLY) != 0
&& (mShowExplicitlyRequested || mShowForced)) {
if (DEBUG) Slog.v(TAG, "Not hiding: explicit show not cancelled by non-explicit hide");
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_HIDE_IMPLICIT);
return false;
}
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_HIDE_IMPLICIT);
+
if (mShowForced && (flags & InputMethodManager.HIDE_NOT_ALWAYS) != 0) {
if (DEBUG) Slog.v(TAG, "Not hiding: forced show not cancelled by not-always hide");
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_HIDE_NOT_ALWAYS);
return false;
}
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_HIDE_NOT_ALWAYS);
// There is a chance that IMM#hideSoftInput() is called in a transient state where
// IMMS#InputShown is already updated to be true whereas IMMS#mImeWindowVis is still waiting
@@ -3445,8 +3519,8 @@
// IMMS#InputShown indicates that the software keyboard is shown.
// TODO: Clean up, IMMS#mInputShown, IMMS#mImeWindowVis and mShowRequested.
IInputMethodInvoker curMethod = getCurMethodLocked();
- final boolean shouldHideSoftInput = (curMethod != null) && (mInputShown
- || (mImeWindowVis & InputMethodService.IME_ACTIVE) != 0);
+ final boolean shouldHideSoftInput = (curMethod != null)
+ && (mInputShown || (mImeWindowVis & InputMethodService.IME_ACTIVE) != 0);
boolean res;
if (shouldHideSoftInput) {
final Binder hideInputToken = new Binder();
@@ -3455,17 +3529,20 @@
// delivered to the IME process as an IPC. Hence the inconsistency between
// IMMS#mInputShown and IMMS#mImeWindowVis should be resolved spontaneously in
// the final state.
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_SHOULD_HIDE);
if (DEBUG) {
Slog.v(TAG, "Calling " + curMethod + ".hideSoftInput(0, " + hideInputToken
+ ", " + resultReceiver + ") for reason: "
+ InputMethodDebug.softInputDisplayReasonToString(reason));
}
// TODO(b/192412909): Check if we can always call onShowHideSoftInputRequested() or not.
- if (curMethod.hideSoftInput(hideInputToken, 0 /* flags */, resultReceiver)) {
+ if (curMethod.hideSoftInput(hideInputToken, statsToken, 0 /* flags */,
+ resultReceiver)) {
onShowHideSoftInputRequested(false /* show */, windowToken, reason);
}
res = true;
} else {
+ ImeTracker.get().onCancelled(statsToken, ImeTracker.PHASE_SERVER_SHOULD_HIDE);
res = false;
}
mBindingController.setCurrentMethodNotVisible();
@@ -3473,6 +3550,9 @@
mShowRequested = false;
mShowExplicitlyRequested = false;
mShowForced = false;
+ // Cancel existing statsToken for show IME as we got a hide request.
+ ImeTracker.get().onCancelled(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME);
+ mCurStatsToken = null;
return res;
}
@@ -3630,8 +3710,8 @@
Slog.w(TAG, "If you need to impersonate a foreground user/profile from"
+ " a background user, use EditorInfo.targetInputMethodUser with"
+ " INTERACT_ACROSS_USERS_FULL permission.");
- hideCurrentInputLocked(
- mCurFocusedWindow, 0, null, SoftInputShowHideReason.HIDE_INVALID_USER);
+ hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
+ null /* resultReceiver */, SoftInputShowHideReason.HIDE_INVALID_USER);
return InputBindResult.INVALID_USER;
}
@@ -3687,7 +3767,7 @@
boolean didStart = false;
InputBindResult res = null;
- // We shows the IME when the system allows the IME focused target window to restore the
+ // We show the IME when the system allows the IME focused target window to restore the
// IME visibility (e.g. switching to the app task when last time the IME is visible).
// Note that we don't restore IME visibility for some cases (e.g. when the soft input
// state is ALWAYS_HIDDEN or STATE_HIDDEN with forward navigation).
@@ -3699,7 +3779,7 @@
res = startInputUncheckedLocked(cs, inputContext, remoteAccessibilityInputConnection,
editorInfo, startInputFlags, startInputReason, unverifiedTargetSdkVersion,
imeDispatcher);
- showCurrentInputLocked(windowToken, InputMethodManager.SHOW_IMPLICIT, null,
+ showCurrentInputImplicitLocked(windowToken,
SoftInputShowHideReason.SHOW_RESTORE_IME_VISIBILITY);
return res;
}
@@ -3712,8 +3792,8 @@
// be behind any soft input window, so hide the
// soft input window if it is shown.
if (DEBUG) Slog.v(TAG, "Unspecified window will hide input");
- hideCurrentInputLocked(
- mCurFocusedWindow, InputMethodManager.HIDE_NOT_ALWAYS, null,
+ hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */,
+ InputMethodManager.HIDE_NOT_ALWAYS, null /* resultReceiver */,
SoftInputShowHideReason.HIDE_UNSPECIFIED_WINDOW);
// If focused display changed, we should unbind current method
@@ -3742,10 +3822,7 @@
imeDispatcher);
didStart = true;
}
- showCurrentInputLocked(
- windowToken,
- InputMethodManager.SHOW_IMPLICIT,
- null,
+ showCurrentInputImplicitLocked(windowToken,
SoftInputShowHideReason.SHOW_AUTO_EDITOR_FORWARD_NAV);
}
break;
@@ -3758,14 +3835,16 @@
case LayoutParams.SOFT_INPUT_STATE_HIDDEN:
if ((softInputMode & LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0) {
if (DEBUG) Slog.v(TAG, "Window asks to hide input going forward");
- hideCurrentInputLocked(mCurFocusedWindow, 0, null,
+ hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
+ null /* resultReceiver */,
SoftInputShowHideReason.HIDE_STATE_HIDDEN_FORWARD_NAV);
}
break;
case LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN:
if (!sameWindowFocused) {
if (DEBUG) Slog.v(TAG, "Window asks to hide input");
- hideCurrentInputLocked(mCurFocusedWindow, 0, null,
+ hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
+ null /* resultReceiver */,
SoftInputShowHideReason.HIDE_ALWAYS_HIDDEN_STATE);
}
break;
@@ -3781,7 +3860,7 @@
imeDispatcher);
didStart = true;
}
- showCurrentInputLocked(windowToken, InputMethodManager.SHOW_IMPLICIT, null,
+ showCurrentInputImplicitLocked(windowToken,
SoftInputShowHideReason.SHOW_STATE_VISIBLE_FORWARD_NAV);
} else {
Slog.e(TAG, "SOFT_INPUT_STATE_VISIBLE is ignored because"
@@ -3802,7 +3881,7 @@
imeDispatcher);
didStart = true;
}
- showCurrentInputLocked(windowToken, InputMethodManager.SHOW_IMPLICIT, null,
+ showCurrentInputImplicitLocked(windowToken,
SoftInputShowHideReason.SHOW_STATE_ALWAYS_VISIBLE);
}
} else {
@@ -3824,7 +3903,8 @@
// an editor upon refocusing a window.
if (startInputByWinGainedFocus) {
if (DEBUG) Slog.v(TAG, "Same window without editor will hide input");
- hideCurrentInputLocked(mCurFocusedWindow, 0, null,
+ hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */,
+ 0 /* flags */, null /* resultReceiver */,
SoftInputShowHideReason.HIDE_SAME_WINDOW_FOCUSED_WITHOUT_EDITOR);
}
}
@@ -3838,7 +3918,8 @@
// 2) SOFT_INPUT_STATE_VISIBLE state without an editor
// 3) SOFT_INPUT_STATE_ALWAYS_VISIBLE state without an editor
if (DEBUG) Slog.v(TAG, "Window without editor will hide input");
- hideCurrentInputLocked(mCurFocusedWindow, 0, null,
+ hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
+ null /* resultReceiver */,
SoftInputShowHideReason.HIDE_WINDOW_GAINED_FOCUS_WITHOUT_EDITOR);
}
res = startInputUncheckedLocked(cs, inputContext,
@@ -3853,8 +3934,15 @@
}
@GuardedBy("ImfLock.class")
- private boolean canInteractWithImeLocked(
- int uid, IInputMethodClient client, String methodName) {
+ private void showCurrentInputImplicitLocked(@NonNull IBinder windowToken,
+ @SoftInputShowHideReason int reason) {
+ showCurrentInputLocked(windowToken, null /* statsToken */, InputMethodManager.SHOW_IMPLICIT,
+ null /* resultReceiver */, reason);
+ }
+
+ @GuardedBy("ImfLock.class")
+ private boolean canInteractWithImeLocked(int uid, IInputMethodClient client, String methodName,
+ @Nullable ImeTracker.Token statsToken) {
if (mCurClient == null || client == null
|| mCurClient.mClient.asBinder() != client.asBinder()) {
// We need to check if this is the current client with
@@ -3862,13 +3950,16 @@
// be made before input is started in it.
final ClientState cs = mClients.get(client.asBinder());
if (cs == null) {
+ ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_CLIENT_KNOWN);
throw new IllegalArgumentException("unknown client " + client.asBinder());
}
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_CLIENT_KNOWN);
if (!isImeClientFocused(mCurFocusedWindow, cs)) {
Slog.w(TAG, String.format("Ignoring %s of uid %d : %s", methodName, uid, client));
return false;
}
}
+ ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_CLIENT_FOCUSED);
return true;
}
@@ -4221,7 +4312,7 @@
final int curTokenDisplayId;
synchronized (ImfLock.class) {
if (!canInteractWithImeLocked(callingUid, client,
- "getInputMethodWindowVisibleHeight")) {
+ "getInputMethodWindowVisibleHeight", null /* statsToken */)) {
if (!mLoggedDeniedGetInputMethodWindowVisibleHeightForUid.get(callingUid)) {
EventLog.writeEvent(0x534e4554, "204906124", callingUid, "");
mLoggedDeniedGetInputMethodWindowVisibleHeightForUid.put(callingUid, true);
@@ -4444,7 +4535,8 @@
int uid = Binder.getCallingUid();
synchronized (ImfLock.class) {
- if (!canInteractWithImeLocked(uid, client, "addVirtualStylusIdForTestSession")) {
+ if (!canInteractWithImeLocked(uid, client, "addVirtualStylusIdForTestSession",
+ null /* statsToken */)) {
return;
}
final long ident = Binder.clearCallingIdentity();
@@ -4471,7 +4563,8 @@
int uid = Binder.getCallingUid();
synchronized (ImfLock.class) {
- if (!canInteractWithImeLocked(uid, client, "setStylusWindowIdleTimeoutForTest")) {
+ if (!canInteractWithImeLocked(uid, client, "setStylusWindowIdleTimeoutForTest",
+ null /* statsToken */)) {
return;
}
final long ident = Binder.clearCallingIdentity();
@@ -4656,7 +4749,8 @@
}
@BinderThread
- private void applyImeVisibility(IBinder token, IBinder windowToken, boolean setVisible) {
+ private void applyImeVisibility(IBinder token, IBinder windowToken, boolean setVisible,
+ @Nullable ImeTracker.Token statsToken) {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.applyImeVisibility");
synchronized (ImfLock.class) {
if (!calledWithValidTokenLocked(token)) {
@@ -4664,13 +4758,22 @@
}
if (!setVisible) {
if (mCurClient != null) {
+ ImeTracker.get().onProgress(statsToken,
+ ImeTracker.PHASE_SERVER_APPLY_IME_VISIBILITY);
+
mWindowManagerInternal.hideIme(
mHideRequestWindowMap.get(windowToken),
- mCurClient.mSelfReportedDisplayId);
+ mCurClient.mSelfReportedDisplayId, statsToken);
+ } else {
+ ImeTracker.get().onFailed(statsToken,
+ ImeTracker.PHASE_SERVER_APPLY_IME_VISIBILITY);
}
} else {
+ ImeTracker.get().onProgress(statsToken,
+ ImeTracker.PHASE_SERVER_APPLY_IME_VISIBILITY);
// Send to window manager to show IME after IME layout finishes.
- mWindowManagerInternal.showImePostLayout(mShowRequestWindowMap.get(windowToken));
+ mWindowManagerInternal.showImePostLayout(mShowRequestWindowMap.get(windowToken),
+ statsToken);
}
}
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
@@ -4737,7 +4840,8 @@
}
final long ident = Binder.clearCallingIdentity();
try {
- hideCurrentInputLocked(mLastImeTargetWindow, flags, null, reason);
+ hideCurrentInputLocked(mLastImeTargetWindow, null /* statsToken */, flags,
+ null /* resultReceiver */, reason);
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -4754,7 +4858,8 @@
}
final long ident = Binder.clearCallingIdentity();
try {
- showCurrentInputLocked(mLastImeTargetWindow, flags, null,
+ showCurrentInputLocked(mLastImeTargetWindow, null /* statsToken */, flags,
+ null /* resultReceiver */,
SoftInputShowHideReason.SHOW_SOFT_INPUT_FROM_IME);
} finally {
Binder.restoreCallingIdentity(ident);
@@ -4845,7 +4950,8 @@
case MSG_HIDE_CURRENT_INPUT_METHOD:
synchronized (ImfLock.class) {
final @SoftInputShowHideReason int reason = (int) msg.obj;
- hideCurrentInputLocked(mCurFocusedWindow, 0, null, reason);
+ hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
+ null /* resultReceiver */, reason);
}
return true;
@@ -5297,6 +5403,7 @@
mCurrentSubtype = getCurrentInputMethodSubtypeLocked();
}
}
+ notifyInputMethodSubtypeChangedLocked(mSettings.getCurrentUserId(), imi, mCurrentSubtype);
if (!setSubtypeOnly) {
// Set InputMethod here
@@ -6332,7 +6439,8 @@
final String nextIme;
final List<InputMethodInfo> nextEnabledImes;
if (userId == mSettings.getCurrentUserId()) {
- hideCurrentInputLocked(mCurFocusedWindow, 0, null,
+ hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */,
+ 0 /* flags */, null /* resultReceiver */,
SoftInputShowHideReason.HIDE_RESET_SHELL_COMMAND);
mBindingController.unbindCurrentMethod();
// Reset the current IME
@@ -6597,8 +6705,9 @@
@BinderThread
@Override
- public void applyImeVisibilityAsync(IBinder windowToken, boolean setVisible) {
- mImms.applyImeVisibility(mToken, windowToken, setVisible);
+ public void applyImeVisibilityAsync(IBinder windowToken, boolean setVisible,
+ @Nullable ImeTracker.Token statsToken) {
+ mImms.applyImeVisibility(mToken, windowToken, setVisible, statsToken);
}
@BinderThread
diff --git a/services/core/java/com/android/server/location/gnss/GnssConfiguration.java b/services/core/java/com/android/server/location/gnss/GnssConfiguration.java
index 1435016..b8abd98 100644
--- a/services/core/java/com/android/server/location/gnss/GnssConfiguration.java
+++ b/services/core/java/com/android/server/location/gnss/GnssConfiguration.java
@@ -76,6 +76,8 @@
"ENABLE_PSDS_PERIODIC_DOWNLOAD";
private static final String CONFIG_ENABLE_ACTIVE_SIM_EMERGENCY_SUPL =
"ENABLE_ACTIVE_SIM_EMERGENCY_SUPL";
+ private static final String CONFIG_ENABLE_NI_SUPL_MESSAGE_INJECTION =
+ "ENABLE_NI_SUPL_MESSAGE_INJECTION";
static final String CONFIG_LONGTERM_PSDS_SERVER_1 = "LONGTERM_PSDS_SERVER_1";
static final String CONFIG_LONGTERM_PSDS_SERVER_2 = "LONGTERM_PSDS_SERVER_2";
static final String CONFIG_LONGTERM_PSDS_SERVER_3 = "LONGTERM_PSDS_SERVER_3";
@@ -218,6 +220,14 @@
}
/**
+ * Returns true if NI SUPL message injection is enabled; Returns false otherwise.
+ * Default false if not set.
+ */
+ boolean isNiSuplMessageInjectionEnabled() {
+ return getBooleanConfig(CONFIG_ENABLE_NI_SUPL_MESSAGE_INJECTION, false);
+ }
+
+ /**
* Returns true if a long-term PSDS server is configured.
*/
boolean isLongTermPsdsServerConfigured() {
diff --git a/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java b/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java
index 6f637b8..6f6b1c9 100644
--- a/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java
+++ b/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java
@@ -84,6 +84,7 @@
import android.os.WorkSource;
import android.os.WorkSource.WorkChain;
import android.provider.Settings;
+import android.provider.Telephony.Sms.Intents;
import android.telephony.CarrierConfigManager;
import android.telephony.CellIdentity;
import android.telephony.CellIdentityGsm;
@@ -95,6 +96,7 @@
import android.telephony.CellInfoLte;
import android.telephony.CellInfoNr;
import android.telephony.CellInfoWcdma;
+import android.telephony.SmsMessage;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
@@ -107,6 +109,7 @@
import com.android.internal.location.GpsNetInitiatedHandler;
import com.android.internal.location.GpsNetInitiatedHandler.GpsNiNotification;
import com.android.internal.util.FrameworkStatsLog;
+import com.android.internal.util.HexDump;
import com.android.server.FgThread;
import com.android.server.location.gnss.GnssSatelliteBlocklistHelper.GnssSatelliteBlocklistCallback;
import com.android.server.location.gnss.NtpTimeHelper.InjectNtpTimeCallback;
@@ -523,23 +526,31 @@
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED);
intentFilter.addAction(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED);
- mContext.registerReceiver(new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- String action = intent.getAction();
- if (DEBUG) Log.d(TAG, "receive broadcast intent, action: " + action);
- if (action == null) {
- return;
- }
+ mContext.registerReceiver(mIntentReceiver, intentFilter, null, mHandler);
- switch (action) {
- case CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED:
- case TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED:
- subscriptionOrCarrierConfigChanged();
- break;
- }
+ if (mNetworkConnectivityHandler.isNativeAgpsRilSupported()
+ && mGnssConfiguration.isNiSuplMessageInjectionEnabled()) {
+ // Listen to WAP PUSH NI SUPL message.
+ // See User Plane Location Protocol Candidate Version 3.0,
+ // OMA-TS-ULP-V3_0-20110920-C, Section 8.3 OMA Push.
+ intentFilter = new IntentFilter();
+ intentFilter.addAction(Intents.WAP_PUSH_RECEIVED_ACTION);
+ try {
+ intentFilter.addDataType("application/vnd.omaloc-supl-init");
+ } catch (IntentFilter.MalformedMimeTypeException e) {
+ Log.w(TAG, "Malformed SUPL init mime type");
}
- }, intentFilter, null, mHandler);
+ mContext.registerReceiver(mIntentReceiver, intentFilter, null, mHandler);
+
+ // Listen to MT SMS NI SUPL message.
+ // See User Plane Location Protocol Candidate Version 3.0,
+ // OMA-TS-ULP-V3_0-20110920-C, Section 8.4 MT SMS.
+ intentFilter = new IntentFilter();
+ intentFilter.addAction(Intents.DATA_SMS_RECEIVED_ACTION);
+ intentFilter.addDataScheme("sms");
+ intentFilter.addDataAuthority("localhost", "7275");
+ mContext.registerReceiver(mIntentReceiver, intentFilter, null, mHandler);
+ }
mNetworkConnectivityHandler.registerNetworkCallbacks();
@@ -560,6 +571,80 @@
updateEnabled();
}
+ private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (DEBUG) Log.d(TAG, "receive broadcast intent, action: " + action);
+ if (action == null) {
+ return;
+ }
+
+ switch (action) {
+ case CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED:
+ case TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED:
+ subscriptionOrCarrierConfigChanged();
+ break;
+ case Intents.WAP_PUSH_RECEIVED_ACTION:
+ case Intents.DATA_SMS_RECEIVED_ACTION:
+ injectSuplInit(intent);
+ break;
+ }
+ }
+ };
+
+ private void injectSuplInit(Intent intent) {
+ if (!isNfwLocationAccessAllowed()) {
+ Log.w(TAG, "Reject SUPL INIT as no NFW location access");
+ return;
+ }
+
+ int slotIndex = intent.getIntExtra(SubscriptionManager.EXTRA_SLOT_INDEX,
+ SubscriptionManager.INVALID_SIM_SLOT_INDEX);
+ if (slotIndex == SubscriptionManager.INVALID_SIM_SLOT_INDEX) {
+ Log.e(TAG, "Invalid slot index");
+ return;
+ }
+
+ byte[] suplInit = null;
+ String action = intent.getAction();
+ if (action.equals(Intents.DATA_SMS_RECEIVED_ACTION)) {
+ SmsMessage[] messages = Intents.getMessagesFromIntent(intent);
+ if (messages == null) {
+ Log.e(TAG, "Message does not exist in the intent");
+ return;
+ }
+ for (SmsMessage message : messages) {
+ suplInit = message.getUserData();
+ injectSuplInit(suplInit, slotIndex);
+ }
+ } else if (action.equals(Intents.WAP_PUSH_RECEIVED_ACTION)) {
+ suplInit = intent.getByteArrayExtra("data");
+ injectSuplInit(suplInit, slotIndex);
+ }
+ }
+
+ private void injectSuplInit(byte[] suplInit, int slotIndex) {
+ if (suplInit != null) {
+ if (DEBUG) {
+ Log.d(TAG, "suplInit = "
+ + HexDump.toHexString(suplInit) + " slotIndex = " + slotIndex);
+ }
+ mGnssNative.injectNiSuplMessageData(suplInit, suplInit.length , slotIndex);
+ }
+ }
+
+ private boolean isNfwLocationAccessAllowed() {
+ if (mGnssNative.isInEmergencySession()) {
+ return true;
+ }
+ if (mGnssVisibilityControl != null
+ && mGnssVisibilityControl.hasLocationPermissionEnabledProxyApps()) {
+ return true;
+ }
+ return false;
+ }
+
/**
* Implements {@link InjectNtpTimeCallback#injectTime}
*/
diff --git a/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java b/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java
index 02bdfd5..a7fffe2 100644
--- a/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java
+++ b/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java
@@ -762,6 +762,10 @@
return APN_INVALID;
}
+ protected boolean isNativeAgpsRilSupported() {
+ return native_is_agps_ril_supported();
+ }
+
// AGPS support
private native void native_agps_data_conn_open(long networkHandle, String apn, int apnIpType);
diff --git a/services/core/java/com/android/server/location/gnss/GnssVisibilityControl.java b/services/core/java/com/android/server/location/gnss/GnssVisibilityControl.java
index 631dbbf..4e5e5f8 100644
--- a/services/core/java/com/android/server/location/gnss/GnssVisibilityControl.java
+++ b/services/core/java/com/android/server/location/gnss/GnssVisibilityControl.java
@@ -437,6 +437,10 @@
return locationPermissionEnabledProxyApps;
}
+ public boolean hasLocationPermissionEnabledProxyApps() {
+ return getLocationPermissionEnabledProxyApps().length > 0;
+ }
+
private void handleNfwNotification(NfwNotification nfwNotification) {
if (DEBUG) Log.d(TAG, "Non-framework location access notification: " + nfwNotification);
diff --git a/services/core/java/com/android/server/location/gnss/hal/GnssNative.java b/services/core/java/com/android/server/location/gnss/hal/GnssNative.java
index 2d015a5d..edb2e5b 100644
--- a/services/core/java/com/android/server/location/gnss/hal/GnssNative.java
+++ b/services/core/java/com/android/server/location/gnss/hal/GnssNative.java
@@ -989,6 +989,14 @@
mGnssHal.injectPsdsData(data, length, psdsType);
}
+ /**
+ * Injects NI SUPL message data into the GNSS HAL.
+ */
+ public void injectNiSuplMessageData(byte[] data, int length, int slotIndex) {
+ Preconditions.checkState(mRegistered);
+ mGnssHal.injectNiSuplMessageData(data, length, slotIndex);
+ }
+
@NativeEntryPoint
void reportGnssServiceDied() {
// Not necessary to clear (and restore) binder identity since it runs on another thread.
@@ -1278,7 +1286,7 @@
}
@NativeEntryPoint
- boolean isInEmergencySession() {
+ public boolean isInEmergencySession() {
return Binder.withCleanCallingIdentity(
() -> mEmergencyHelper.isInEmergency(
TimeUnit.SECONDS.toMillis(mConfiguration.getEsExtensionSec())));
@@ -1507,6 +1515,10 @@
protected void injectPsdsData(byte[] data, int length, int psdsType) {
native_inject_psds_data(data, length, psdsType);
}
+
+ protected void injectNiSuplMessageData(byte[] data, int length, int slotIndex) {
+ native_inject_ni_supl_message_data(data, length, slotIndex);
+ }
}
// basic APIs
@@ -1650,6 +1662,9 @@
private static native void native_agps_set_ref_location_cellid(int type, int mcc, int mnc,
int lac, long cid, int tac, int pcid, int arfcn);
+ private static native void native_inject_ni_supl_message_data(byte[] data, int length,
+ int slotIndex);
+
// PSDS APIs
private static native boolean native_supports_psds();
diff --git a/services/core/java/com/android/server/pm/Computer.java b/services/core/java/com/android/server/pm/Computer.java
index bf00a33..5b8ee2b 100644
--- a/services/core/java/com/android/server/pm/Computer.java
+++ b/services/core/java/com/android/server/pm/Computer.java
@@ -125,6 +125,14 @@
ActivityInfo getActivityInfo(ComponentName component, long flags, int userId);
/**
+ * Similar to {@link Computer#getActivityInfo(android.content.ComponentName, long, int)} but
+ * only visible as internal service. This method bypass INTERACT_ACROSS_USERS or
+ * INTERACT_ACROSS_USERS_FULL permission checks and only to be used for intent resolution across
+ * chained cross profiles
+ */
+ ActivityInfo getActivityInfoCrossProfile(ComponentName component, long flags, int userId);
+
+ /**
* Important: The provided filterCallingUid is used exclusively to filter out activities
* that can be seen based on user state. It's typically the original caller uid prior
* to clearing. Because it can only be provided by trusted code, its value can be
diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java
index b285136..a8534b0 100644
--- a/services/core/java/com/android/server/pm/ComputerEngine.java
+++ b/services/core/java/com/android/server/pm/ComputerEngine.java
@@ -835,6 +835,24 @@
}
/**
+ * Similar to {@link Computer#getActivityInfo(android.content.ComponentName, long, int)} but
+ * only visible as internal service. This method bypass INTERACT_ACROSS_USERS or
+ * INTERACT_ACROSS_USERS_FULL permission checks and only to be used for intent resolution across
+ * chained cross profiles
+ * @param component application's component
+ * @param flags resolve info flags
+ * @param userId user id where activity resides
+ * @return ActivityInfo corresponding to requested component.
+ */
+ public final ActivityInfo getActivityInfoCrossProfile(ComponentName component,
+ @PackageManager.ResolveInfoFlagsBits long flags, int userId) {
+ if (!mUserManager.exists(userId)) return null;
+ flags = updateFlagsForComponent(flags, userId);
+
+ return getActivityInfoInternalBody(component, flags, Binder.getCallingUid(), userId);
+ }
+
+ /**
* Important: The provided filterCallingUid is used exclusively to filter out activities
* that can be seen based on user state. It's typically the original caller uid prior
* to clearing. Because it can only be provided by trusted code, its value can be
@@ -1711,7 +1729,7 @@
ComponentName forwardingActivityComponentName = new ComponentName(
androidApplication().packageName, className);
ActivityInfo forwardingActivityInfo =
- getActivityInfo(forwardingActivityComponentName, 0,
+ getActivityInfoCrossProfile(forwardingActivityComponentName, 0,
sourceUserId);
if (!targetIsProfile) {
forwardingActivityInfo.showUserIcon = targetUserId;
diff --git a/services/core/java/com/android/server/pm/CrossProfileIntentFilter.java b/services/core/java/com/android/server/pm/CrossProfileIntentFilter.java
index 798217f..04bd135 100644
--- a/services/core/java/com/android/server/pm/CrossProfileIntentFilter.java
+++ b/services/core/java/com/android/server/pm/CrossProfileIntentFilter.java
@@ -49,6 +49,15 @@
//flag to decide if intent needs to be resolved cross profile if pkgName is already defined
public static final int FLAG_IS_PACKAGE_FOR_FILTER = 0x00000008;
+ /*
+ This flag, denotes if further cross profile resolution is allowed, e.g. if profile#0 is linked
+ to profile#1 and profile#2 . When intent resolution from profile#1 is started we resolve it in
+ profile#1 and profile#0. The profile#0 is also linked to profile#2, we will only resolve in
+ profile#2 if CrossProfileIntentFilter between profile#1 and profile#0 have set flag
+ FLAG_ALLOW_CHAINED_RESOLUTION.
+ */
+ public static final int FLAG_ALLOW_CHAINED_RESOLUTION = 0x00000010;
+
private static final String TAG = "CrossProfileIntentFilter";
/**
diff --git a/services/core/java/com/android/server/pm/CrossProfileIntentResolverEngine.java b/services/core/java/com/android/server/pm/CrossProfileIntentResolverEngine.java
index 5ae4cab..4362956 100644
--- a/services/core/java/com/android/server/pm/CrossProfileIntentResolverEngine.java
+++ b/services/core/java/com/android/server/pm/CrossProfileIntentResolverEngine.java
@@ -36,14 +36,19 @@
import android.util.Pair;
import android.util.Slog;
import android.util.SparseArray;
+import android.util.SparseBooleanArray;
import com.android.server.LocalServices;
import com.android.server.pm.pkg.PackageStateInternal;
import com.android.server.pm.verify.domain.DomainVerificationManagerInternal;
import com.android.server.pm.verify.domain.DomainVerificationUtils;
+import java.util.ArrayDeque;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.List;
+import java.util.Queue;
+import java.util.Set;
import java.util.function.Function;
/**
@@ -115,73 +120,111 @@
Intent intent, String resolvedType, int userId, long flags, String pkgName,
boolean hasNonNegativePriorityResult,
Function<String, PackageStateInternal> pkgSettingFunction) {
-
+ Queue<Integer> pendingUsers = new ArrayDeque<>();
+ Set<Integer> visitedUserIds = new HashSet<>();
+ SparseBooleanArray hasNonNegativePriorityResultFromParent = new SparseBooleanArray();
+ visitedUserIds.add(userId);
+ pendingUsers.add(userId);
+ hasNonNegativePriorityResultFromParent.put(userId, hasNonNegativePriorityResult);
+ UserManagerInternal umInternal = LocalServices.getService(UserManagerInternal.class);
List<CrossProfileDomainInfo> crossProfileDomainInfos = new ArrayList<>();
+ while (!pendingUsers.isEmpty()) {
+ int currentUserId = pendingUsers.poll();
+ List<CrossProfileIntentFilter> matchingFilters =
+ computer.getMatchingCrossProfileIntentFilters(intent, resolvedType,
+ currentUserId);
- List<CrossProfileIntentFilter> matchingFilters =
- computer.getMatchingCrossProfileIntentFilters(intent, resolvedType, userId);
-
- if (matchingFilters == null || matchingFilters.isEmpty()) {
- /** if intent is web intent, checking if parent profile should handle the intent even
- if there is no matching filter. The configuration is based on user profile
- restriction android.os.UserManager#ALLOW_PARENT_PROFILE_APP_LINKING **/
- if (intent.hasWebURI()) {
- UserInfo parent = computer.getProfileParent(userId);
- if (parent != null) {
- CrossProfileDomainInfo generalizedCrossProfileDomainInfo = computer
- .getCrossProfileDomainPreferredLpr(intent, resolvedType, flags, userId,
- parent.id);
- if (generalizedCrossProfileDomainInfo != null) {
- crossProfileDomainInfos.add(generalizedCrossProfileDomainInfo);
+ if (matchingFilters == null || matchingFilters.isEmpty()) {
+ /** if intent is web intent, checking if parent profile should handle the intent
+ * even if there is no matching filter. The configuration is based on user profile
+ * restriction android.os.UserManager#ALLOW_PARENT_PROFILE_APP_LINKING **/
+ if (currentUserId == userId && intent.hasWebURI()) {
+ UserInfo parent = computer.getProfileParent(currentUserId);
+ if (parent != null) {
+ CrossProfileDomainInfo generalizedCrossProfileDomainInfo = computer
+ .getCrossProfileDomainPreferredLpr(intent, resolvedType, flags,
+ currentUserId, parent.id);
+ if (generalizedCrossProfileDomainInfo != null) {
+ crossProfileDomainInfos.add(generalizedCrossProfileDomainInfo);
+ }
}
}
+ continue;
}
- return crossProfileDomainInfos;
- }
- UserManagerInternal umInternal = LocalServices.getService(UserManagerInternal.class);
- UserInfo sourceUserInfo = umInternal.getUserInfo(userId);
+ UserInfo sourceUserInfo = umInternal.getUserInfo(currentUserId);
- // Grouping the CrossProfileIntentFilters based on targerId
- SparseArray<List<CrossProfileIntentFilter>> crossProfileIntentFiltersByUser =
- new SparseArray<>();
+ // Grouping the CrossProfileIntentFilters based on targerId
+ SparseArray<List<CrossProfileIntentFilter>> crossProfileIntentFiltersByUser =
+ new SparseArray<>();
- for (int index = 0; index < matchingFilters.size(); index++) {
- CrossProfileIntentFilter crossProfileIntentFilter = matchingFilters.get(index);
+ for (int index = 0; index < matchingFilters.size(); index++) {
+ CrossProfileIntentFilter crossProfileIntentFilter = matchingFilters.get(index);
- if (!crossProfileIntentFiltersByUser
- .contains(crossProfileIntentFilter.mTargetUserId)) {
- crossProfileIntentFiltersByUser.put(crossProfileIntentFilter.mTargetUserId,
- new ArrayList<>());
+ if (!crossProfileIntentFiltersByUser
+ .contains(crossProfileIntentFilter.mTargetUserId)) {
+ crossProfileIntentFiltersByUser.put(crossProfileIntentFilter.mTargetUserId,
+ new ArrayList<>());
+ }
+ crossProfileIntentFiltersByUser.get(crossProfileIntentFilter.mTargetUserId)
+ .add(crossProfileIntentFilter);
}
- crossProfileIntentFiltersByUser.get(crossProfileIntentFilter.mTargetUserId)
- .add(crossProfileIntentFilter);
- }
- /*
- For each target user, we would call their corresponding strategy
- {@link CrossProfileResolver} to resolve intent in corresponding user
- */
- for (int index = 0; index < crossProfileIntentFiltersByUser.size(); index++) {
+ /*
+ For each target user, we would call their corresponding strategy
+ {@link CrossProfileResolver} to resolve intent in corresponding user
+ */
+ for (int index = 0; index < crossProfileIntentFiltersByUser.size(); index++) {
- UserInfo targetUserInfo = umInternal.getUserInfo(crossProfileIntentFiltersByUser
- .keyAt(index));
+ int targetUserId = crossProfileIntentFiltersByUser.keyAt(index);
- // Choosing strategy based on source and target user
- CrossProfileResolver crossProfileResolver =
- chooseCrossProfileResolver(computer, sourceUserInfo, targetUserInfo);
+ //if user is already visited then skip resolution for particular user.
+ if (visitedUserIds.contains(targetUserId)) {
+ continue;
+ }
+
+ UserInfo targetUserInfo = umInternal.getUserInfo(targetUserId);
+
+ // Choosing strategy based on source and target user
+ CrossProfileResolver crossProfileResolver =
+ chooseCrossProfileResolver(computer, sourceUserInfo, targetUserInfo);
/*
If {@link CrossProfileResolver} is available for source,target pair we will call it to
get {@link CrossProfileDomainInfo}s from that user.
*/
- if (crossProfileResolver != null) {
- List<CrossProfileDomainInfo> crossProfileInfos = crossProfileResolver
- .resolveIntent(computer, intent, resolvedType, userId,
- crossProfileIntentFiltersByUser.keyAt(index), flags, pkgName,
- crossProfileIntentFiltersByUser.valueAt(index),
- hasNonNegativePriorityResult, pkgSettingFunction);
- crossProfileDomainInfos.addAll(crossProfileInfos);
+ if (crossProfileResolver != null) {
+ List<CrossProfileDomainInfo> crossProfileInfos = crossProfileResolver
+ .resolveIntent(computer, intent, resolvedType, currentUserId,
+ targetUserId, flags, pkgName,
+ crossProfileIntentFiltersByUser.valueAt(index),
+ hasNonNegativePriorityResultFromParent.get(currentUserId),
+ pkgSettingFunction);
+ crossProfileDomainInfos.addAll(crossProfileInfos);
+
+ hasNonNegativePriorityResultFromParent.put(targetUserId,
+ hasNonNegativePriority(crossProfileInfos));
+
+ /*
+ Adding target user to queue if flag
+ {@link CrossProfileIntentFilter#FLAG_ALLOW_CHAINED_RESOLUTION} is set for any
+ {@link CrossProfileIntentFilter}
+ */
+ boolean allowChainedResolution = false;
+ for (int filterIndex = 0; filterIndex < crossProfileIntentFiltersByUser
+ .valueAt(index).size(); filterIndex++) {
+ if ((CrossProfileIntentFilter
+ .FLAG_ALLOW_CHAINED_RESOLUTION & crossProfileIntentFiltersByUser
+ .valueAt(index).get(filterIndex).mFlags) != 0) {
+ allowChainedResolution = true;
+ break;
+ }
+ }
+ if (allowChainedResolution) {
+ pendingUsers.add(targetUserId);
+ }
+ visitedUserIds.add(targetUserId);
+ }
}
}
@@ -237,7 +280,7 @@
/**
* Returns true if we source user can reach target user for given intent. The source can
- * directly or indirectly reach to target. This will perform depth first search to check if
+ * directly or indirectly reach to target. This will perform breadth first search to check if
* source can reach target.
* @param computer {@link Computer} instance used for resolution by {@link ComponentResolverApi}
* @param intent request
@@ -251,13 +294,38 @@
@UserIdInt int targetUserId) {
if (sourceUserId == targetUserId) return true;
- List<CrossProfileIntentFilter> matches =
- computer.getMatchingCrossProfileIntentFilters(intent, resolvedType, sourceUserId);
- if (matches != null) {
- for (int index = 0; index < matches.size(); index++) {
- CrossProfileIntentFilter crossProfileIntentFilter = matches.get(index);
- if (crossProfileIntentFilter.mTargetUserId == targetUserId) {
- return true;
+ Queue<Integer> pendingUsers = new ArrayDeque<>();
+ Set<Integer> visitedUserIds = new HashSet<>();
+ visitedUserIds.add(sourceUserId);
+ pendingUsers.add(sourceUserId);
+
+ while (!pendingUsers.isEmpty()) {
+ int currentUserId = pendingUsers.poll();
+
+ List<CrossProfileIntentFilter> matches =
+ computer.getMatchingCrossProfileIntentFilters(intent, resolvedType,
+ currentUserId);
+ if (matches != null) {
+ for (int index = 0; index < matches.size(); index++) {
+ CrossProfileIntentFilter crossProfileIntentFilter = matches.get(index);
+ if (crossProfileIntentFilter.mTargetUserId == targetUserId) {
+ return true;
+ }
+ if (visitedUserIds.contains(crossProfileIntentFilter.mTargetUserId)) {
+ continue;
+ }
+
+ /*
+ If source cannot directly reach to target, we will add
+ CrossProfileIntentFilter.mTargetUserId user to queue to check if target user
+ can be reached via CrossProfileIntentFilter.mTargetUserId i.e. it can be
+ indirectly reached through chained/linked profiles.
+ */
+ if ((CrossProfileIntentFilter.FLAG_ALLOW_CHAINED_RESOLUTION
+ & crossProfileIntentFilter.mFlags) != 0) {
+ pendingUsers.add(crossProfileIntentFilter.mTargetUserId);
+ visitedUserIds.add(crossProfileIntentFilter.mTargetUserId);
+ }
}
}
}
@@ -605,4 +673,14 @@
return resolveInfoList;
}
+
+ /**
+ * @param crossProfileDomainInfos list of cross profile domain info in descending priority order
+ * @return if the list contains a resolve info with non-negative priority
+ */
+ private boolean hasNonNegativePriority(List<CrossProfileDomainInfo> crossProfileDomainInfos) {
+ return crossProfileDomainInfos.size() > 0
+ && crossProfileDomainInfos.get(0).mResolveInfo != null
+ && crossProfileDomainInfos.get(0).mResolveInfo.priority >= 0;
+ }
}
diff --git a/services/core/java/com/android/server/pm/DefaultCrossProfileIntentFiltersUtils.java b/services/core/java/com/android/server/pm/DefaultCrossProfileIntentFiltersUtils.java
index cac9323..ceaaefd 100644
--- a/services/core/java/com/android/server/pm/DefaultCrossProfileIntentFiltersUtils.java
+++ b/services/core/java/com/android/server/pm/DefaultCrossProfileIntentFiltersUtils.java
@@ -319,4 +319,135 @@
HOME,
MOBILE_NETWORK_SETTINGS);
}
+
+ /**
+ * Clone profile's DefaultCrossProfileIntentFilter
+ */
+
+ /*
+ Allowing media capture from clone to parent profile as clone profile would not have camera
+ */
+ private static final DefaultCrossProfileIntentFilter CLONE_TO_PARENT_MEDIA_CAPTURE =
+ new DefaultCrossProfileIntentFilter.Builder(
+ DefaultCrossProfileIntentFilter.Direction.TO_PARENT,
+ /* flags= */ 0x00000018, // 0x00000018 means FLAG_IS_PACKAGE_FOR_FILTER
+ // and FLAG_ALLOW_CHAINED_RESOLUTION set
+ /* letsPersonalDataIntoProfile= */ false)
+ .addAction(MediaStore.ACTION_IMAGE_CAPTURE)
+ .addAction(MediaStore.ACTION_IMAGE_CAPTURE_SECURE)
+ .addAction(MediaStore.ACTION_VIDEO_CAPTURE)
+ .addAction(MediaStore.Audio.Media.RECORD_SOUND_ACTION)
+ .addAction(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA)
+ .addAction(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE)
+ .addAction(MediaStore.INTENT_ACTION_VIDEO_CAMERA)
+ .addCategory(Intent.CATEGORY_DEFAULT)
+ .build();
+
+ /*
+ Allowing send action from clone to parent profile to share content from clone apps to parent
+ apps
+ */
+ private static final DefaultCrossProfileIntentFilter CLONE_TO_PARENT_SEND_ACTION =
+ new DefaultCrossProfileIntentFilter.Builder(
+ DefaultCrossProfileIntentFilter.Direction.TO_PARENT,
+ /* flags= */ 0x00000018, // 0x00000018 means FLAG_IS_PACKAGE_FOR_FILTER
+ // and FLAG_ALLOW_CHAINED_RESOLUTION set
+ /* letsPersonalDataIntoProfile= */ false)
+ .addAction(Intent.ACTION_SEND)
+ .addAction(Intent.ACTION_SEND_MULTIPLE)
+ .addAction(Intent.ACTION_SENDTO)
+ .addDataType("*/*")
+ .build();
+
+ /*
+ Allowing send action from parent to clone profile to share content from parent apps to clone
+ apps
+ */
+ private static final DefaultCrossProfileIntentFilter PARENT_TO_CLONE_SEND_ACTION =
+ new DefaultCrossProfileIntentFilter.Builder(
+ DefaultCrossProfileIntentFilter.Direction.TO_PROFILE,
+ /* flags= */ 0x00000018, // 0x00000018 means FLAG_IS_PACKAGE_FOR_FILTER
+ // and FLAG_ALLOW_CHAINED_RESOLUTION set
+ /* letsPersonalDataIntoProfile= */ false)
+ .addAction(Intent.ACTION_SEND)
+ .addAction(Intent.ACTION_SEND_MULTIPLE)
+ .addAction(Intent.ACTION_SENDTO)
+ .addDataType("*/*")
+ .build();
+
+ /*
+ Allowing view action from clone to parent profile to open any app-links or web links
+ */
+ private static final DefaultCrossProfileIntentFilter CLONE_TO_PARENT_VIEW_ACTION =
+ new DefaultCrossProfileIntentFilter.Builder(
+ DefaultCrossProfileIntentFilter.Direction.TO_PARENT,
+ /* flags= */ 0x00000018, // 0x00000018 means FLAG_IS_PACKAGE_FOR_FILTER
+ // and FLAG_ALLOW_CHAINED_RESOLUTION set
+ /* letsPersonalDataIntoProfile= */ false)
+ .addAction(Intent.ACTION_VIEW)
+ .addDataScheme("https")
+ .addDataScheme("http")
+ .build();
+
+ /*
+ Allowing view action from parent to clone profile to open any app-links or web links
+ */
+ private static final DefaultCrossProfileIntentFilter PARENT_TO_CLONE_VIEW_ACTION =
+ new DefaultCrossProfileIntentFilter.Builder(
+ DefaultCrossProfileIntentFilter.Direction.TO_PROFILE,
+ /* flags= */ 0x00000018, // 0x00000018 means FLAG_IS_PACKAGE_FOR_FILTER
+ // and FLAG_ALLOW_CHAINED_RESOLUTION set
+ /* letsPersonalDataIntoProfile= */ false)
+ .addAction(Intent.ACTION_VIEW)
+ .addDataScheme("https")
+ .addDataScheme("http")
+ .build();
+
+ /*
+ Allowing pick,insert and edit action from clone to parent profile to open picker or contacts
+ insert/edit.
+ */
+ private static final DefaultCrossProfileIntentFilter CLONE_TO_PARENT_PICK_INSERT_ACTION =
+ new DefaultCrossProfileIntentFilter.Builder(
+ DefaultCrossProfileIntentFilter.Direction.TO_PARENT,
+ /* flags= */ 0x00000018, // 0x00000018 means FLAG_IS_PACKAGE_FOR_FILTER
+ // and FLAG_ALLOW_CHAINED_RESOLUTION set
+ /* letsPersonalDataIntoProfile= */ false)
+ .addAction(Intent.ACTION_PICK)
+ .addAction(Intent.ACTION_GET_CONTENT)
+ .addAction(Intent.ACTION_EDIT)
+ .addAction(Intent.ACTION_INSERT)
+ .addAction(Intent.ACTION_INSERT_OR_EDIT)
+ .addDataType("*/*")
+ .build();
+
+ /*
+ Allowing pick,insert and edit action from parent to clone profile to open picker
+ */
+ private static final DefaultCrossProfileIntentFilter PARENT_TO_CLONE_PICK_INSERT_ACTION =
+ new DefaultCrossProfileIntentFilter.Builder(
+ DefaultCrossProfileIntentFilter.Direction.TO_PROFILE,
+ /* flags= */ 0x00000018, // 0x00000018 means FLAG_IS_PACKAGE_FOR_FILTER
+ // and FLAG_ALLOW_CHAINED_RESOLUTION set
+ /* letsPersonalDataIntoProfile= */ false)
+ .addAction(Intent.ACTION_PICK)
+ .addAction(Intent.ACTION_GET_CONTENT)
+ .addAction(Intent.ACTION_EDIT)
+ .addAction(Intent.ACTION_INSERT)
+ .addAction(Intent.ACTION_INSERT_OR_EDIT)
+ .addDataType("*/*")
+ .build();
+
+ public static List<DefaultCrossProfileIntentFilter> getDefaultCloneProfileFilters() {
+ return Arrays.asList(
+ PARENT_TO_CLONE_SEND_ACTION,
+ PARENT_TO_CLONE_VIEW_ACTION,
+ PARENT_TO_CLONE_PICK_INSERT_ACTION,
+ CLONE_TO_PARENT_MEDIA_CAPTURE,
+ CLONE_TO_PARENT_SEND_ACTION,
+ CLONE_TO_PARENT_VIEW_ACTION,
+ CLONE_TO_PARENT_PICK_INSERT_ACTION
+
+ );
+ }
}
diff --git a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
index dbd026e..27d74d5 100644
--- a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
+++ b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
@@ -148,7 +148,8 @@
UserManager.DISALLOW_SHARING_ADMIN_CONFIGURED_WIFI,
UserManager.DISALLOW_WIFI_DIRECT,
UserManager.DISALLOW_ADD_WIFI_CONFIG,
- UserManager.DISALLOW_CELLULAR_2G
+ UserManager.DISALLOW_CELLULAR_2G,
+ UserManager.DISALLOW_ULTRA_WIDEBAND_RADIO
});
public static final Set<String> DEPRECATED_USER_RESTRICTIONS = Sets.newArraySet(
@@ -197,7 +198,8 @@
UserManager.DISALLOW_WIFI_TETHERING,
UserManager.DISALLOW_WIFI_DIRECT,
UserManager.DISALLOW_ADD_WIFI_CONFIG,
- UserManager.DISALLOW_CELLULAR_2G
+ UserManager.DISALLOW_CELLULAR_2G,
+ UserManager.DISALLOW_ULTRA_WIDEBAND_RADIO
);
/**
@@ -237,7 +239,8 @@
UserManager.DISALLOW_WIFI_TETHERING,
UserManager.DISALLOW_WIFI_DIRECT,
UserManager.DISALLOW_ADD_WIFI_CONFIG,
- UserManager.DISALLOW_CELLULAR_2G
+ UserManager.DISALLOW_CELLULAR_2G,
+ UserManager.DISALLOW_ULTRA_WIDEBAND_RADIO
);
/**
diff --git a/services/core/java/com/android/server/pm/UserTypeFactory.java b/services/core/java/com/android/server/pm/UserTypeFactory.java
index c35fe17..b8950e1 100644
--- a/services/core/java/com/android/server/pm/UserTypeFactory.java
+++ b/services/core/java/com/android/server/pm/UserTypeFactory.java
@@ -126,11 +126,13 @@
.setCrossProfileIntentFilterAccessControl(
CrossProfileIntentFilter.ACCESS_LEVEL_SYSTEM)
.setIsCredentialSharableWithParent(true)
+ .setDefaultCrossProfileIntentFilters(getDefaultCloneCrossProfileIntentFilter())
.setDefaultUserProperties(new UserProperties.Builder()
.setStartWithParent(true)
.setShowInLauncher(UserProperties.SHOW_IN_LAUNCHER_WITH_PARENT)
.setShowInSettings(UserProperties.SHOW_IN_SETTINGS_WITH_PARENT)
- .setInheritDevicePolicy(UserProperties.INHERIT_DEVICE_POLICY_FROM_PARENT));
+ .setInheritDevicePolicy(UserProperties.INHERIT_DEVICE_POLICY_FROM_PARENT)
+ .setUseParentsContacts(true));
}
/**
@@ -310,6 +312,10 @@
return DefaultCrossProfileIntentFiltersUtils.getDefaultManagedProfileFilters();
}
+ private static List<DefaultCrossProfileIntentFilter> getDefaultCloneCrossProfileIntentFilter() {
+ return DefaultCrossProfileIntentFiltersUtils.getDefaultCloneProfileFilters();
+ }
+
/**
* Reads the given xml parser to obtain device user-type customization, and updates the given
* map of {@link UserTypeDetails.Builder}s accordingly.
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 8588267..28f86ed 100644
--- a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
+++ b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
@@ -616,6 +616,10 @@
grantPermissionsToSystemPackage(pm, getDefaultCaptivePortalLoginPackage(), userId,
NOTIFICATION_PERMISSIONS);
+ // Dock Manager
+ grantPermissionsToSystemPackage(pm, getDefaultDockManagerPackage(), userId,
+ NOTIFICATION_PERMISSIONS);
+
// Camera
grantPermissionsToSystemPackage(pm,
getDefaultSystemHandlerActivityPackage(pm, MediaStore.ACTION_IMAGE_CAPTURE, userId),
@@ -933,6 +937,10 @@
return mContext.getString(R.string.config_defaultCaptivePortalLoginPackageName);
}
+ private String getDefaultDockManagerPackage() {
+ return mContext.getString(R.string.config_defaultDockManagerPackageName);
+ }
+
@SafeVarargs
private final void grantPermissionToEachSystemPackage(PackageManagerWrapper pm,
ArrayList<String> packages, int userId, Set<String>... permissions) {
diff --git a/services/core/java/com/android/server/power/hint/HintManagerService.java b/services/core/java/com/android/server/power/hint/HintManagerService.java
index dfa1281..0d13831 100644
--- a/services/core/java/com/android/server/power/hint/HintManagerService.java
+++ b/services/core/java/com/android/server/power/hint/HintManagerService.java
@@ -24,6 +24,7 @@
import android.os.IBinder;
import android.os.IHintManager;
import android.os.IHintSession;
+import android.os.PerformanceHintManager;
import android.os.Process;
import android.os.RemoteException;
import android.util.ArrayMap;
@@ -147,6 +148,8 @@
private static native void nativeReportActualWorkDuration(
long halPtr, long[] actualDurationNanos, long[] timeStampNanos);
+ private static native void nativeSendHint(long halPtr, int hint);
+
private static native long nativeGetHintSessionPreferredRate();
/** Wrapper for HintManager.nativeInit */
@@ -186,6 +189,11 @@
timeStampNanos);
}
+ /** Wrapper for HintManager.sendHint */
+ public void halSendHint(long halPtr, int hint) {
+ nativeSendHint(halPtr, hint);
+ }
+
/** Wrapper for HintManager.nativeGetHintSessionPreferredRate */
public long halGetHintSessionPreferredRate() {
return nativeGetHintSessionPreferredRate();
@@ -475,6 +483,18 @@
}
}
+ @Override
+ public void sendHint(@PerformanceHintManager.Session.Hint int hint) {
+ synchronized (mLock) {
+ if (mHalSessionPtr == 0 || !updateHintAllowed()) {
+ return;
+ }
+ Preconditions.checkArgument(hint >= 0, "the hint ID the hint value should be"
+ + " greater than zero.");
+ mNativeWrapper.halSendHint(mHalSessionPtr, hint);
+ }
+ }
+
private void onProcStateChanged() {
updateHintAllowed();
}
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 07b87dc..8f8b57f 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -1747,6 +1747,7 @@
}
prevDc.mClosingApps.remove(this);
+ prevDc.getDisplayPolicy().removeRelaunchingApp(this);
if (prevDc.mFocusedApp == this) {
prevDc.setFocusedApp(null);
@@ -3130,8 +3131,8 @@
}
// Check to see if PiP is supported for the display this container is on.
- if (mDisplayContent != null && !mDisplayContent.mDwpcHelper.isWindowingModeSupported(
- WINDOWING_MODE_PINNED)) {
+ if (mDisplayContent != null && !mDisplayContent.mDwpcHelper.isEnteringPipAllowed(
+ getUid())) {
Slog.w(TAG, "Display " + mDisplayContent.getDisplayId()
+ " doesn't support enter picture-in-picture mode. caller = " + caller);
return false;
@@ -3959,6 +3960,9 @@
void startRelaunching() {
if (mPendingRelaunchCount == 0) {
mRelaunchStartTime = SystemClock.elapsedRealtime();
+ if (mVisibleRequested) {
+ mDisplayContent.getDisplayPolicy().addRelaunchingApp(this);
+ }
}
clearAllDrawn();
@@ -3972,7 +3976,7 @@
mPendingRelaunchCount--;
if (mPendingRelaunchCount == 0 && !isClientVisible()) {
// Don't count if the client won't report drawn.
- mRelaunchStartTime = 0;
+ finishOrAbortReplacingWindow();
}
} else {
// Update keyguard flags upon finishing relaunch.
@@ -3993,7 +3997,12 @@
return;
}
mPendingRelaunchCount = 0;
+ finishOrAbortReplacingWindow();
+ }
+
+ void finishOrAbortReplacingWindow() {
mRelaunchStartTime = 0;
+ mDisplayContent.getDisplayPolicy().removeRelaunchingApp(this);
}
/**
@@ -5102,6 +5111,9 @@
mTaskSupervisor.onProcessActivityStateChanged(app, false /* forceBatch */);
}
logAppCompatState();
+ if (!visible) {
+ finishOrAbortReplacingWindow();
+ }
}
/**
@@ -6887,11 +6899,8 @@
* {@link android.view.Display#INVALID_DISPLAY} if not attached.
*/
int getDisplayId() {
- final Task rootTask = getRootTask();
- if (rootTask == null) {
- return INVALID_DISPLAY;
- }
- return rootTask.getDisplayId();
+ return task != null && task.mDisplayContent != null
+ ? task.mDisplayContent.mDisplayId : INVALID_DISPLAY;
}
final boolean isDestroyable() {
@@ -7892,8 +7901,8 @@
}
@Override
- float getSizeCompatScale() {
- return hasSizeCompatBounds() ? mSizeCompatScale : super.getSizeCompatScale();
+ float getCompatScale() {
+ return hasSizeCompatBounds() ? mSizeCompatScale : super.getCompatScale();
}
@Override
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index 3c457e1..2f70eda 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -27,6 +27,7 @@
import static android.app.ActivityManager.START_FLAG_NATIVE_DEBUGGING;
import static android.app.ActivityManager.START_FLAG_TRACK_ALLOCATION;
import static android.app.ActivityManager.START_TASK_TO_FRONT;
+import static android.app.ActivityOptions.ANIM_REMOTE_ANIMATION;
import static android.app.ITaskStackListener.FORCED_RESIZEABLE_REASON_SECONDARY_DISPLAY;
import static android.app.ITaskStackListener.FORCED_RESIZEABLE_REASON_SPLIT_SCREEN;
import static android.app.WaitResult.INVALID_DELAY;
@@ -2577,6 +2578,10 @@
// Apply options to prevent pendingOptions be taken when scheduling
// activity lifecycle transaction to make sure the override pending app
// transition will be applied immediately.
+ if (activityOptions.getAnimationType() == ANIM_REMOTE_ANIMATION) {
+ targetActivity.mPendingRemoteAnimation =
+ activityOptions.getRemoteAnimationAdapter();
+ }
targetActivity.applyOptionsAnimation();
if (activityOptions != null && activityOptions.getLaunchCookie() != null) {
targetActivity.mLaunchCookie = activityOptions.getLaunchCookie();
diff --git a/services/core/java/com/android/server/wm/DisplayArea.java b/services/core/java/com/android/server/wm/DisplayArea.java
index b84b2d8..bedeabe 100644
--- a/services/core/java/com/android/server/wm/DisplayArea.java
+++ b/services/core/java/com/android/server/wm/DisplayArea.java
@@ -369,6 +369,55 @@
}
@Override
+ ActivityRecord getActivity(Predicate<ActivityRecord> callback, boolean traverseTopToBottom,
+ ActivityRecord boundary) {
+ if (mType == Type.ABOVE_TASKS || mType == Type.BELOW_TASKS) {
+ return null;
+ }
+ return super.getActivity(callback, traverseTopToBottom, boundary);
+ }
+
+ @Override
+ Task getTask(Predicate<Task> callback, boolean traverseTopToBottom) {
+ if (mType == Type.ABOVE_TASKS || mType == Type.BELOW_TASKS) {
+ return null;
+ }
+ return super.getTask(callback, traverseTopToBottom);
+ }
+
+ @Override
+ boolean forAllActivities(Predicate<ActivityRecord> callback, boolean traverseTopToBottom) {
+ if (mType == Type.ABOVE_TASKS || mType == Type.BELOW_TASKS) {
+ return false;
+ }
+ return super.forAllActivities(callback, traverseTopToBottom);
+ }
+
+ @Override
+ boolean forAllRootTasks(Predicate<Task> callback, boolean traverseTopToBottom) {
+ if (mType == Type.ABOVE_TASKS || mType == Type.BELOW_TASKS) {
+ return false;
+ }
+ return super.forAllRootTasks(callback, traverseTopToBottom);
+ }
+
+ @Override
+ boolean forAllTasks(Predicate<Task> callback) {
+ if (mType == Type.ABOVE_TASKS || mType == Type.BELOW_TASKS) {
+ return false;
+ }
+ return super.forAllTasks(callback);
+ }
+
+ @Override
+ boolean forAllLeafTasks(Predicate<Task> callback) {
+ if (mType == Type.ABOVE_TASKS || mType == Type.BELOW_TASKS) {
+ return false;
+ }
+ return super.forAllLeafTasks(callback);
+ }
+
+ @Override
void forAllDisplayAreas(Consumer<DisplayArea> callback) {
super.forAllDisplayAreas(callback);
callback.accept(this);
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 8b34443..12efe0d 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -230,6 +230,7 @@
import android.view.WindowManager;
import android.view.WindowManager.DisplayImePolicy;
import android.view.WindowManagerPolicyConstants.PointerEventListener;
+import android.view.inputmethod.ImeTracker;
import android.window.DisplayWindowPolicyController;
import android.window.IDisplayAreaOrganizer;
import android.window.ScreenCapture;
@@ -454,7 +455,7 @@
/**
* Compat metrics computed based on {@link #mDisplayMetrics}.
- * @see #updateDisplayAndOrientation(int, Configuration)
+ * @see #updateDisplayAndOrientation(Configuration)
*/
private final DisplayMetrics mCompatDisplayMetrics = new DisplayMetrics();
@@ -5031,7 +5032,7 @@
* layer has been assigned since), to facilitate assigning the layer from the IME target, or
* fall back if there is no target.
* - the container doesn't always participate in window traversal, according to
- * {@link #skipImeWindowsDuringTraversal()}
+ * {@link #skipImeWindowsDuringTraversal(DisplayContent)}
*/
private static class ImeContainer extends DisplayArea.Tokens {
boolean mNeedsLayer = false;
@@ -6712,25 +6713,35 @@
mRemoteInsetsController.insetsControlChanged(stateController.getRawInsetsState(),
stateController.getControlsForDispatch(this));
} catch (RemoteException e) {
- Slog.w(TAG, "Failed to deliver inset state change", e);
+ Slog.w(TAG, "Failed to deliver inset control state change", e);
}
}
@Override
- public void showInsets(@WindowInsets.Type.InsetsType int types, boolean fromIme) {
+ public void showInsets(@WindowInsets.Type.InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
try {
- mRemoteInsetsController.showInsets(types, fromIme);
+ ImeTracker.get().onProgress(statsToken,
+ ImeTracker.PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_SHOW_INSETS);
+ mRemoteInsetsController.showInsets(types, fromIme, statsToken);
} catch (RemoteException e) {
Slog.w(TAG, "Failed to deliver showInsets", e);
+ ImeTracker.get().onFailed(statsToken,
+ ImeTracker.PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_SHOW_INSETS);
}
}
@Override
- public void hideInsets(@InsetsType int types, boolean fromIme) {
+ public void hideInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
try {
- mRemoteInsetsController.hideInsets(types, fromIme);
+ ImeTracker.get().onProgress(statsToken,
+ ImeTracker.PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_HIDE_INSETS);
+ mRemoteInsetsController.hideInsets(types, fromIme, statsToken);
} catch (RemoteException e) {
- Slog.w(TAG, "Failed to deliver showInsets", e);
+ Slog.w(TAG, "Failed to deliver hideInsets", e);
+ ImeTracker.get().onFailed(statsToken,
+ ImeTracker.PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_HIDE_INSETS);
}
}
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index a1efd2d..1fef3c2 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -270,6 +270,12 @@
private final ArraySet<WindowState> mInsetsSourceWindowsExceptIme = new ArraySet<>();
+ /** Apps which are controlling the appearance of system bars */
+ private final ArraySet<ActivityRecord> mSystemBarColorApps = new ArraySet<>();
+
+ /** Apps which are relaunching and were controlling the appearance of system bars */
+ private final ArraySet<ActivityRecord> mRelaunchingSystemBarColorApps = new ArraySet<>();
+
private boolean mIsFreeformWindowOverlappingWithNavBar;
private boolean mLastImmersiveMode;
@@ -1449,6 +1455,7 @@
mStatusBarBackgroundWindows.clear();
mStatusBarColorCheckedBounds.setEmpty();
mStatusBarBackgroundCheckedBounds.setEmpty();
+ mSystemBarColorApps.clear();
mAllowLockscreenWhenOn = false;
mShowingDream = false;
@@ -1525,6 +1532,7 @@
win.mAttrs.insetsFlags.appearance & APPEARANCE_LIGHT_STATUS_BARS,
new Rect(win.getFrame())));
mStatusBarColorCheckedBounds.union(sTmpRect);
+ addSystemBarColorApp(win);
}
}
@@ -1537,6 +1545,7 @@
if (isOverlappingWithNavBar) {
if (mNavBarColorWindowCandidate == null) {
mNavBarColorWindowCandidate = win;
+ addSystemBarColorApp(win);
}
if (mNavBarBackgroundWindow == null) {
mNavBarBackgroundWindow = win;
@@ -1555,9 +1564,11 @@
}
} else if (win.isDimming()) {
if (mStatusBar != null) {
- addStatusBarAppearanceRegionsForDimmingWindow(
+ if (addStatusBarAppearanceRegionsForDimmingWindow(
win.mAttrs.insetsFlags.appearance & APPEARANCE_LIGHT_STATUS_BARS,
- mStatusBar.getFrame(), win.getBounds(), win.getFrame());
+ mStatusBar.getFrame(), win.getBounds(), win.getFrame())) {
+ addSystemBarColorApp(win);
+ }
}
if (isOverlappingWithNavBar && mNavBarColorWindowCandidate == null) {
mNavBarColorWindowCandidate = win;
@@ -1565,18 +1576,21 @@
}
}
- private void addStatusBarAppearanceRegionsForDimmingWindow(int appearance, Rect statusBarFrame,
- Rect winBounds, Rect winFrame) {
+ /**
+ * Returns true if mStatusBarAppearanceRegionList is changed.
+ */
+ private boolean addStatusBarAppearanceRegionsForDimmingWindow(
+ int appearance, Rect statusBarFrame, Rect winBounds, Rect winFrame) {
if (!sTmpRect.setIntersect(winBounds, statusBarFrame)) {
- return;
+ return false;
}
if (mStatusBarColorCheckedBounds.contains(sTmpRect)) {
- return;
+ return false;
}
if (appearance == 0 || !sTmpRect2.setIntersect(winFrame, statusBarFrame)) {
mStatusBarAppearanceRegionList.add(new AppearanceRegion(0, new Rect(winBounds)));
mStatusBarColorCheckedBounds.union(sTmpRect);
- return;
+ return true;
}
// A dimming window can divide status bar into different appearance regions (up to 3).
// +---------+-------------+---------+
@@ -1605,6 +1619,14 @@
// We don't have vertical status bar yet, so we don't handle the other orientation.
}
mStatusBarColorCheckedBounds.union(sTmpRect);
+ return true;
+ }
+
+ private void addSystemBarColorApp(WindowState win) {
+ final ActivityRecord app = win.mActivityRecord;
+ if (app != null) {
+ mSystemBarColorApps.add(app);
+ }
}
/**
@@ -2049,7 +2071,8 @@
// Don't show status bar when swiping on already visible navigation bar.
// But restore the position of navigation bar if it has been moved by the control
// target.
- controlTarget.showInsets(Type.navigationBars(), false);
+ controlTarget.showInsets(Type.navigationBars(), false /* fromIme */,
+ null /* statsToken */);
return;
}
@@ -2057,10 +2080,12 @@
// Show transient bars if they are hidden; restore position if they are visible.
mDisplayContent.getInsetsPolicy().showTransient(SHOW_TYPES_FOR_SWIPE,
isGestureOnSystemBar);
- controlTarget.showInsets(restorePositionTypes, false);
+ controlTarget.showInsets(restorePositionTypes, false /* fromIme */,
+ null /* statsToken */);
} else {
// Restore visibilities and positions of system bars.
- controlTarget.showInsets(Type.statusBars() | Type.navigationBars(), false);
+ controlTarget.showInsets(Type.statusBars() | Type.navigationBars(),
+ false /* fromIme */, null /* statsToken */);
// To further allow the pull-down-from-the-top gesture to pull down the notification
// shade as a consistent motion, we reroute the touch events here from the currently
// touched window to the status bar after making it visible.
@@ -2086,6 +2111,25 @@
return mDisplayContent.getInsetsPolicy();
}
+ /**
+ * Called when an app has started replacing its main window.
+ */
+ void addRelaunchingApp(ActivityRecord app) {
+ if (mSystemBarColorApps.contains(app)) {
+ mRelaunchingSystemBarColorApps.add(app);
+ }
+ }
+
+ /**
+ * Called when an app has finished replacing its main window or aborted.
+ */
+ void removeRelaunchingApp(ActivityRecord app) {
+ final boolean removed = mRelaunchingSystemBarColorApps.remove(app);
+ if (removed & mRelaunchingSystemBarColorApps.isEmpty()) {
+ updateSystemBarAttributes();
+ }
+ }
+
void resetSystemBarAttributes() {
mLastDisableFlags = 0;
updateSystemBarAttributes();
@@ -2128,6 +2172,11 @@
final int displayId = getDisplayId();
final int disableFlags = win.getDisableFlags();
final int opaqueAppearance = updateSystemBarsLw(win, disableFlags);
+ if (!mRelaunchingSystemBarColorApps.isEmpty()) {
+ // The appearance of system bars might change while relaunching apps. We don't report
+ // the intermediate state to system UI. Otherwise, it might trigger redundant effects.
+ return;
+ }
final WindowState navColorWin = chooseNavigationColorWindowLw(mNavBarColorWindowCandidate,
mDisplayContent.mInputMethodWindow, mNavigationBarPosition);
final boolean isNavbarColorManagedByIme =
@@ -2590,6 +2639,14 @@
pw.print(prefix); pw.print("mTopFullscreenOpaqueWindowState=");
pw.println(mTopFullscreenOpaqueWindowState);
}
+ if (!mSystemBarColorApps.isEmpty()) {
+ pw.print(prefix); pw.print("mSystemBarColorApps=");
+ pw.println(mSystemBarColorApps);
+ }
+ if (!mRelaunchingSystemBarColorApps.isEmpty()) {
+ pw.print(prefix); pw.print("mRelaunchingSystemBarColorApps=");
+ pw.println(mRelaunchingSystemBarColorApps);
+ }
if (mNavBarColorWindowCandidate != null) {
pw.print(prefix); pw.print("mNavBarColorWindowCandidate=");
pw.println(mNavBarColorWindowCandidate);
diff --git a/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java b/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java
index 5d49042..6f821b5 100644
--- a/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java
+++ b/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java
@@ -162,6 +162,17 @@
return mDisplayWindowPolicyController.canShowTasksInRecents();
}
+ /**
+ * @see DisplayWindowPolicyController#isEnteringPipAllowed(int)
+ */
+ public final boolean isEnteringPipAllowed(int uid) {
+ if (mDisplayWindowPolicyController == null) {
+ return true;
+ }
+ return mDisplayWindowPolicyController.isEnteringPipAllowed(uid);
+ }
+
+
void dump(String prefix, PrintWriter pw) {
if (mDisplayWindowPolicyController != null) {
pw.println();
diff --git a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
index 554791a..7fd093f 100644
--- a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
@@ -33,9 +33,11 @@
import android.os.Trace;
import android.util.proto.ProtoOutputStream;
import android.view.InsetsSource;
+import android.view.InsetsSourceConsumer;
import android.view.InsetsSourceControl;
import android.view.InsetsState;
import android.view.WindowInsets;
+import android.view.inputmethod.ImeTracker;
import android.window.TaskSnapshot;
import com.android.internal.annotations.VisibleForTesting;
@@ -49,6 +51,9 @@
*/
final class ImeInsetsSourceProvider extends WindowContainerInsetsSourceProvider {
+ /** The token tracking the current IME request or {@code null} otherwise. */
+ @Nullable
+ private ImeTracker.Token mImeRequesterStatsToken;
private InsetsControlTarget mImeRequester;
private Runnable mShowImeRunner;
private boolean mIsImeLayoutDrawn;
@@ -162,14 +167,20 @@
}
/**
- * Called from {@link WindowManagerInternal#showImePostLayout} when {@link InputMethodService}
- * requests to show IME on {@param imeTarget}.
+ * Called from {@link WindowManagerInternal#showImePostLayout}
+ * when {@link android.inputmethodservice.InputMethodService} requests to show IME
+ * on {@param imeTarget}.
*
- * @param imeTarget imeTarget on which IME request is coming from.
+ * @param imeTarget imeTarget on which IME show request is coming from.
+ * @param statsToken the token tracking the current IME show request or {@code null} otherwise.
*/
- void scheduleShowImePostLayout(InsetsControlTarget imeTarget) {
+ void scheduleShowImePostLayout(InsetsControlTarget imeTarget,
+ @Nullable ImeTracker.Token statsToken) {
boolean targetChanged = isTargetChangedWithinActivity(imeTarget);
mImeRequester = imeTarget;
+ // There was still a stats token, so that request presumably failed.
+ ImeTracker.get().onFailed(mImeRequesterStatsToken, ImeTracker.PHASE_WM_SHOW_IME_RUNNER);
+ mImeRequesterStatsToken = statsToken;
if (targetChanged) {
// target changed, check if new target can show IME.
ProtoLog.d(WM_DEBUG_IME, "IME target changed within ActivityRecord");
@@ -183,15 +194,20 @@
ProtoLog.d(WM_DEBUG_IME, "Schedule IME show for %s", mImeRequester.getWindow() == null
? mImeRequester : mImeRequester.getWindow().getName());
mShowImeRunner = () -> {
+ ImeTracker.get().onProgress(mImeRequesterStatsToken,
+ ImeTracker.PHASE_WM_SHOW_IME_RUNNER);
ProtoLog.d(WM_DEBUG_IME, "Run showImeRunner");
// Target should still be the same.
if (isReadyToShowIme()) {
+ ImeTracker.get().onProgress(mImeRequesterStatsToken,
+ ImeTracker.PHASE_WM_SHOW_IME_READY);
final InsetsControlTarget target = mDisplayContent.getImeTarget(IME_TARGET_CONTROL);
ProtoLog.i(WM_DEBUG_IME, "call showInsets(ime) on %s",
target.getWindow() != null ? target.getWindow().getName() : "");
setImeShowing(true);
- target.showInsets(WindowInsets.Type.ime(), true /* fromIme */);
+ target.showInsets(WindowInsets.Type.ime(), true /* fromIme */,
+ mImeRequesterStatsToken);
Trace.asyncTraceEnd(TRACE_TAG_WINDOW_MANAGER, "WMS.showImePostLayout", 0);
if (target != mImeRequester && mImeRequester != null) {
ProtoLog.w(WM_DEBUG_IME,
@@ -199,7 +215,12 @@
(mImeRequester.getWindow() != null
? mImeRequester.getWindow().getName() : ""));
}
+ } else {
+ ImeTracker.get().onFailed(mImeRequesterStatsToken,
+ ImeTracker.PHASE_WM_SHOW_IME_READY);
}
+ // Clear token here so we don't report an error in abortShowImePostLayout().
+ mImeRequesterStatsToken = null;
abortShowImePostLayout();
};
mDisplayContent.mWmService.requestTraversal();
@@ -234,6 +255,8 @@
mImeRequester = null;
mIsImeLayoutDrawn = false;
mShowImeRunner = null;
+ ImeTracker.get().onCancelled(mImeRequesterStatsToken, ImeTracker.PHASE_WM_SHOW_IME_RUNNER);
+ mImeRequesterStatsToken = null;
}
@VisibleForTesting
diff --git a/services/core/java/com/android/server/wm/InsetsControlTarget.java b/services/core/java/com/android/server/wm/InsetsControlTarget.java
index d35b7c3..8ecbc17 100644
--- a/services/core/java/com/android/server/wm/InsetsControlTarget.java
+++ b/services/core/java/com/android/server/wm/InsetsControlTarget.java
@@ -16,9 +16,11 @@
package com.android.server.wm;
+import android.annotation.Nullable;
import android.inputmethodservice.InputMethodService;
import android.view.WindowInsets;
import android.view.WindowInsets.Type.InsetsType;
+import android.view.inputmethod.ImeTracker;
/**
* Generalization of an object that can control insets state.
@@ -57,8 +59,10 @@
*
* @param types to specify which types of insets source window should be shown.
* @param fromIme {@code true} if IME show request originated from {@link InputMethodService}.
+ * @param statsToken the token tracking the current IME show request or {@code null} otherwise.
*/
- default void showInsets(@InsetsType int types, boolean fromIme) {
+ default void showInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
}
/**
@@ -66,8 +70,10 @@
*
* @param types to specify which types of insets source window should be hidden.
* @param fromIme {@code true} if IME hide request originated from {@link InputMethodService}.
+ * @param statsToken the token tracking the current IME hide request or {@code null} otherwise.
*/
- default void hideInsets(@InsetsType int types, boolean fromIme) {
+ default void hideInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
}
/**
diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java
index b9fa80c..f66fa0f 100644
--- a/services/core/java/com/android/server/wm/InsetsPolicy.java
+++ b/services/core/java/com/android/server/wm/InsetsPolicy.java
@@ -799,7 +799,7 @@
show ? ANIMATION_TYPE_SHOW : ANIMATION_TYPE_HIDE, show
? LAYOUT_INSETS_DURING_ANIMATION_SHOWN
: LAYOUT_INSETS_DURING_ANIMATION_HIDDEN,
- null /* translator */);
+ null /* translator */, null /* statsToken */);
SurfaceAnimationThread.getHandler().post(
() -> mListener.onReady(mAnimationControl, typesReady));
}
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index 74a236b..2dbccae 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -580,9 +580,8 @@
// Rounded corners should be displayed above the taskbar.
bounds.bottom =
Math.min(bounds.bottom, getTaskbarInsetsSource(mainWindow).getFrame().top);
- if (mActivityRecord.inSizeCompatMode()
- && mActivityRecord.getSizeCompatScale() < 1.0f) {
- bounds.scale(1.0f / mActivityRecord.getSizeCompatScale());
+ if (mActivityRecord.inSizeCompatMode() && mActivityRecord.getCompatScale() < 1.0f) {
+ bounds.scale(1.0f / mActivityRecord.getCompatScale());
}
}
diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java
index c206a15..bab3a05 100644
--- a/services/core/java/com/android/server/wm/WindowManagerInternal.java
+++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java
@@ -43,6 +43,7 @@
import android.view.SurfaceControlViewHost;
import android.view.WindowInfo;
import android.view.WindowManager.DisplayImePolicy;
+import android.view.inputmethod.ImeTracker;
import com.android.internal.policy.KeyInterceptionInfo;
import com.android.server.input.InputManagerService;
@@ -729,16 +730,20 @@
* Show IME on imeTargetWindow once IME has finished layout.
*
* @param imeTargetWindowToken token of the (IME target) window on which IME should be shown.
+ * @param statsToken the token tracking the current IME show request or {@code null} otherwise.
*/
- public abstract void showImePostLayout(IBinder imeTargetWindowToken);
+ public abstract void showImePostLayout(IBinder imeTargetWindowToken,
+ @Nullable ImeTracker.Token statsToken);
/**
* Hide IME using imeTargetWindow when requested.
*
* @param imeTargetWindowToken token of the (IME target) window on which IME should be hidden.
* @param displayId the id of the display the IME is on.
+ * @param statsToken the token tracking the current IME hide request or {@code null} otherwise.
*/
- public abstract void hideIme(IBinder imeTargetWindowToken, int displayId);
+ public abstract void hideIme(IBinder imeTargetWindowToken, int displayId,
+ @Nullable ImeTracker.Token statsToken);
/**
* Tell window manager about a package that should be running with a restricted range of
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index c56a22e..3419207 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -292,6 +292,7 @@
import android.view.WindowManagerPolicyConstants.PointerEventListener;
import android.view.displayhash.DisplayHash;
import android.view.displayhash.VerifiedDisplayHash;
+import android.view.inputmethod.ImeTracker;
import android.window.ClientWindowFrames;
import android.window.ITaskFpsCallback;
import android.window.ScreenCapture;
@@ -425,7 +426,7 @@
* @see #ENABLE_SHELL_TRANSITIONS
*/
public static final boolean sEnableShellTransitions =
- SystemProperties.getBoolean(ENABLE_SHELL_TRANSITIONS, false);
+ SystemProperties.getBoolean(ENABLE_SHELL_TRANSITIONS, true);
/**
* Allows a fullscreen windowing mode activity to launch in its desired orientation directly
@@ -1842,7 +1843,7 @@
// Make this invalid which indicates a null attached frame.
outAttachedFrame.set(0, 0, -1, -1);
}
- outSizeCompatScale[0] = win.getSizeCompatScaleForClient();
+ outSizeCompatScale[0] = win.getCompatScaleForClient();
}
Binder.restoreCallingIdentity(origId);
@@ -8014,7 +8015,8 @@
}
@Override
- public void showImePostLayout(IBinder imeTargetWindowToken) {
+ public void showImePostLayout(IBinder imeTargetWindowToken,
+ @Nullable ImeTracker.Token statsToken) {
synchronized (mGlobalLock) {
InputTarget imeTarget = getInputTargetFromWindowTokenLocked(imeTargetWindowToken);
if (imeTarget == null) {
@@ -8023,17 +8025,18 @@
Trace.asyncTraceBegin(TRACE_TAG_WINDOW_MANAGER, "WMS.showImePostLayout", 0);
final InsetsControlTarget controlTarget = imeTarget.getImeControlTarget();
imeTarget = controlTarget.getWindow();
- // If InsetsControlTarget doesn't have a window, its using remoteControlTarget which
- // is controlled by default display
+ // If InsetsControlTarget doesn't have a window, it's using remoteControlTarget
+ // which is controlled by default display
final DisplayContent dc = imeTarget != null
? imeTarget.getDisplayContent() : getDefaultDisplayContentLocked();
dc.getInsetsStateController().getImeSourceProvider()
- .scheduleShowImePostLayout(controlTarget);
+ .scheduleShowImePostLayout(controlTarget, statsToken);
}
}
@Override
- public void hideIme(IBinder imeTargetWindowToken, int displayId) {
+ public void hideIme(IBinder imeTargetWindowToken, int displayId,
+ @Nullable ImeTracker.Token statsToken) {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "WMS.hideIme");
synchronized (mGlobalLock) {
WindowState imeTarget = mWindowMap.get(imeTargetWindowToken);
@@ -8049,10 +8052,15 @@
dc.getInsetsStateController().getImeSourceProvider().abortShowImePostLayout();
}
if (dc != null && dc.getImeTarget(IME_TARGET_CONTROL) != null) {
+ ImeTracker.get().onProgress(statsToken,
+ ImeTracker.PHASE_WM_HAS_IME_INSETS_CONTROL_TARGET);
ProtoLog.d(WM_DEBUG_IME, "hideIme Control target: %s ",
dc.getImeTarget(IME_TARGET_CONTROL));
- dc.getImeTarget(IME_TARGET_CONTROL).hideInsets(
- WindowInsets.Type.ime(), true /* fromIme */);
+ dc.getImeTarget(IME_TARGET_CONTROL).hideInsets(WindowInsets.Type.ime(),
+ true /* fromIme */, statsToken);
+ } else {
+ ImeTracker.get().onFailed(statsToken,
+ ImeTracker.PHASE_WM_HAS_IME_INSETS_CONTROL_TARGET);
}
if (dc != null) {
dc.getInsetsStateController().getImeSourceProvider().setImeShowing(false);
@@ -8895,7 +8903,7 @@
outInsetsState.set(state, true /* copySources */);
if (WindowState.hasCompatScale(attrs, token, overrideScale)) {
final float compatScale = token != null && token.hasSizeCompatBounds()
- ? token.getSizeCompatScale() * overrideScale
+ ? token.getCompatScale() * overrideScale
: overrideScale;
outInsetsState.scale(1f / compatScale);
}
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 17e6499..5c5c703 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -249,6 +249,7 @@
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
+import android.view.inputmethod.ImeTracker;
import android.window.ClientWindowFrames;
import android.window.OnBackInvokedCallbackInfo;
@@ -475,7 +476,7 @@
// Current transformation being applied.
float mGlobalScale = 1f;
float mInvGlobalScale = 1f;
- float mSizeCompatScale = 1f;
+ float mCompatScale = 1f;
final float mOverrideScale;
float mHScale = 1f, mVScale = 1f;
float mLastHScale = 1f, mLastVScale = 1f;
@@ -1256,21 +1257,21 @@
void updateGlobalScale() {
if (hasCompatScale()) {
- mSizeCompatScale = (mOverrideScale == 1f || mToken.hasSizeCompatBounds())
- ? mToken.getSizeCompatScale()
+ mCompatScale = (mOverrideScale == 1f || mToken.hasSizeCompatBounds())
+ ? mToken.getCompatScale()
: 1f;
- mGlobalScale = mSizeCompatScale * mOverrideScale;
+ mGlobalScale = mCompatScale * mOverrideScale;
mInvGlobalScale = 1f / mGlobalScale;
return;
}
- mGlobalScale = mInvGlobalScale = mSizeCompatScale = 1f;
+ mGlobalScale = mInvGlobalScale = mCompatScale = 1f;
}
- float getSizeCompatScaleForClient() {
- // If the size compat scale is because of the size compat bounds, we only scale down its
- // coordinates at the server side without letting the client know.
- return mToken.hasSizeCompatBounds() ? 1f : mSizeCompatScale;
+ float getCompatScaleForClient() {
+ // If this window in the size compat mode. The scaling is fully controlled at the server
+ // side. The client doesn't need to take it into account.
+ return mToken.hasSizeCompatBounds() ? 1f : mCompatScale;
}
/**
@@ -3868,7 +3869,7 @@
}
}
- outFrames.sizeCompatScale = getSizeCompatScaleForClient();
+ outFrames.compatScale = getCompatScaleForClient();
// Note: in the cases where the window is tied to an activity, we should not send a
// configuration update when the window has requested to be hidden. Doing so can lead to
@@ -4018,7 +4019,7 @@
mClient.insetsControlChanged(getCompatInsetsState(),
stateController.getControlsForDispatch(this));
} catch (RemoteException e) {
- Slog.w(TAG, "Failed to deliver inset state change to w=" + this, e);
+ Slog.w(TAG, "Failed to deliver inset control state change to w=" + this, e);
}
}
@@ -4028,20 +4029,30 @@
}
@Override
- public void showInsets(@InsetsType int types, boolean fromIme) {
+ public void showInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
try {
- mClient.showInsets(types, fromIme);
+ ImeTracker.get().onProgress(statsToken,
+ ImeTracker.PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_SHOW_INSETS);
+ mClient.showInsets(types, fromIme, statsToken);
} catch (RemoteException e) {
Slog.w(TAG, "Failed to deliver showInsets", e);
+ ImeTracker.get().onFailed(statsToken,
+ ImeTracker.PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_SHOW_INSETS);
}
}
@Override
- public void hideInsets(@InsetsType int types, boolean fromIme) {
+ public void hideInsets(@InsetsType int types, boolean fromIme,
+ @Nullable ImeTracker.Token statsToken) {
try {
- mClient.hideInsets(types, fromIme);
+ ImeTracker.get().onProgress(statsToken,
+ ImeTracker.PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_HIDE_INSETS);
+ mClient.hideInsets(types, fromIme, statsToken);
} catch (RemoteException e) {
- Slog.w(TAG, "Failed to deliver showInsets", e);
+ Slog.w(TAG, "Failed to deliver hideInsets", e);
+ ImeTracker.get().onFailed(statsToken,
+ ImeTracker.PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_HIDE_INSETS);
}
}
@@ -6018,7 +6029,7 @@
final long duration =
SystemClock.elapsedRealtime() - mActivityRecord.mRelaunchStartTime;
Slog.i(TAG, "finishDrawing of relaunch: " + this + " " + duration + "ms");
- mActivityRecord.mRelaunchStartTime = 0;
+ mActivityRecord.finishOrAbortReplacingWindow();
}
if (mActivityRecord != null && mAttrs.type == TYPE_APPLICATION_STARTING) {
mWmService.mAtmService.mTaskSupervisor.getActivityMetricsLogger()
diff --git a/services/core/java/com/android/server/wm/WindowToken.java b/services/core/java/com/android/server/wm/WindowToken.java
index 7c481f5..f2527b6 100644
--- a/services/core/java/com/android/server/wm/WindowToken.java
+++ b/services/core/java/com/android/server/wm/WindowToken.java
@@ -258,7 +258,7 @@
* @return The scale for applications running in compatibility mode. Multiply the size in the
* application by this scale will be the size in the screen.
*/
- float getSizeCompatScale() {
+ float getCompatScale() {
return mDisplayContent.mCompatibleScreenScale;
}
diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp
index 3c78819..57b977c 100644
--- a/services/core/jni/Android.bp
+++ b/services/core/jni/Android.bp
@@ -170,7 +170,7 @@
"android.hardware.power@1.1",
"android.hardware.power@1.2",
"android.hardware.power@1.3",
- "android.hardware.power-V3-cpp",
+ "android.hardware.power-V4-cpp",
"android.hardware.power.stats@1.0",
"android.hardware.power.stats-V1-ndk",
"android.hardware.thermal@1.0",
diff --git a/services/core/jni/com_android_server_hint_HintManagerService.cpp b/services/core/jni/com_android_server_hint_HintManagerService.cpp
index 000cb83..d975760 100644
--- a/services/core/jni/com_android_server_hint_HintManagerService.cpp
+++ b/services/core/jni/com_android_server_hint_HintManagerService.cpp
@@ -34,6 +34,7 @@
#include "jni.h"
using android::hardware::power::IPowerHintSession;
+using android::hardware::power::SessionHint;
using android::hardware::power::WorkDuration;
using android::base::StringPrintf;
@@ -81,6 +82,11 @@
appSession->reportActualWorkDuration(actualDurations);
}
+static void sendHint(int64_t session_ptr, SessionHint hint) {
+ sp<IPowerHintSession> appSession = reinterpret_cast<IPowerHintSession*>(session_ptr);
+ appSession->sendHint(hint);
+}
+
static int64_t getHintSessionPreferredRate() {
int64_t rate = -1;
auto result = gPowerHalController.getHintSessionPreferredRate();
@@ -139,6 +145,10 @@
reportActualWorkDuration(session_ptr, actualList);
}
+static void nativeSendHint(JNIEnv* env, jclass /* clazz */, jlong session_ptr, jint hint) {
+ sendHint(session_ptr, static_cast<SessionHint>(hint));
+}
+
static jlong nativeGetHintSessionPreferredRate(JNIEnv* /* env */, jclass /* clazz */) {
return static_cast<jlong>(getHintSessionPreferredRate());
}
@@ -153,6 +163,7 @@
{"nativeCloseHintSession", "(J)V", (void*)nativeCloseHintSession},
{"nativeUpdateTargetWorkDuration", "(JJ)V", (void*)nativeUpdateTargetWorkDuration},
{"nativeReportActualWorkDuration", "(J[J[J)V", (void*)nativeReportActualWorkDuration},
+ {"nativeSendHint", "(JI)V", (void*)nativeSendHint},
{"nativeGetHintSessionPreferredRate", "()J", (void*)nativeGetHintSessionPreferredRate},
};
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 0d87237..3197124 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -450,6 +450,10 @@
dump += StringPrintf(INDENT "Pointer Capture: %s, seq=%" PRIu32 "\n",
mLocked.pointerCaptureRequest.enable ? "Enabled" : "Disabled",
mLocked.pointerCaptureRequest.seq);
+ auto pointerController = mLocked.pointerController.lock();
+ if (pointerController != nullptr) {
+ pointerController->dump(dump);
+ }
}
dump += "\n";
diff --git a/services/core/jni/com_android_server_location_GnssLocationProvider.cpp b/services/core/jni/com_android_server_location_GnssLocationProvider.cpp
index 9fa23c2..e1de05c 100644
--- a/services/core/jni/com_android_server_location_GnssLocationProvider.cpp
+++ b/services/core/jni/com_android_server_location_GnssLocationProvider.cpp
@@ -482,6 +482,17 @@
agnssRilIface->setSetId(type, setid_string);
}
+static void android_location_gnss_hal_GnssNative_inject_ni_supl_message_data(JNIEnv* env, jclass,
+ jbyteArray data,
+ jint length,
+ jint slotIndex) {
+ if (agnssRilIface == nullptr) {
+ ALOGE("%s: IAGnssRil interface not available.", __func__);
+ return;
+ }
+ agnssRilIface->injectNiSuplMessageData(data, length, slotIndex);
+}
+
static jint android_location_gnss_hal_GnssNative_read_nmea(JNIEnv* env, jclass,
jbyteArray nmeaArray, jint buffer_size) {
return gnssHal->readNmea(nmeaArray, buffer_size);
@@ -974,6 +985,8 @@
android_location_gnss_hal_GnssNative_agps_set_reference_location_cellid)},
{"native_set_agps_server", "(ILjava/lang/String;I)V",
reinterpret_cast<void*>(android_location_gnss_hal_GnssNative_set_agps_server)},
+ {"native_inject_ni_supl_message_data", "([BII)V",
+ reinterpret_cast<void*>(android_location_gnss_hal_GnssNative_inject_ni_supl_message_data)},
{"native_send_ni_response", "(II)V",
reinterpret_cast<void*>(android_location_gnss_hal_GnssNative_send_ni_response)},
{"native_get_internal_state", "()Ljava/lang/String;",
diff --git a/services/core/jni/gnss/AGnssRil.cpp b/services/core/jni/gnss/AGnssRil.cpp
index 34e4976..c7a1af7 100644
--- a/services/core/jni/gnss/AGnssRil.cpp
+++ b/services/core/jni/gnss/AGnssRil.cpp
@@ -84,8 +84,19 @@
networkAttributes.capabilities = static_cast<int32_t>(capabilities),
networkAttributes.apn = jniApn.c_str();
- auto result = mIAGnssRil->updateNetworkState(networkAttributes);
- return checkAidlStatus(result, "IAGnssRilAidl updateNetworkState() failed.");
+ auto status = mIAGnssRil->updateNetworkState(networkAttributes);
+ return checkAidlStatus(status, "IAGnssRilAidl updateNetworkState() failed.");
+}
+
+jboolean AGnssRil::injectNiSuplMessageData(const jbyteArray& msgData, jint length, jint slotIndex) {
+ JNIEnv* env = getJniEnv();
+ jbyte* bytes = reinterpret_cast<jbyte*>(env->GetPrimitiveArrayCritical(msgData, 0));
+ auto status = mIAGnssRil->injectNiSuplMessageData(std::vector<uint8_t>((const uint8_t*)bytes,
+ (const uint8_t*)bytes +
+ length),
+ static_cast<int>(slotIndex));
+ env->ReleasePrimitiveArrayCritical(msgData, bytes, JNI_ABORT);
+ return checkAidlStatus(status, "IAGnssRil injectNiSuplMessageData() failed.");
}
// Implementation of AGnssRil_V1_0
@@ -151,6 +162,11 @@
return checkHidlReturn(result, "IAGnssRil_V1_0 updateNetworkState() failed.");
}
+jboolean AGnssRil_V1_0::injectNiSuplMessageData(const jbyteArray&, jint, jint) {
+ ALOGI("IAGnssRil_V1_0 interface does not support injectNiSuplMessageData.");
+ return JNI_FALSE;
+}
+
// Implementation of AGnssRil_V2_0
AGnssRil_V2_0::AGnssRil_V2_0(const sp<IAGnssRil_V2_0>& iAGnssRil)
diff --git a/services/core/jni/gnss/AGnssRil.h b/services/core/jni/gnss/AGnssRil.h
index ce14a77d..b7e0282 100644
--- a/services/core/jni/gnss/AGnssRil.h
+++ b/services/core/jni/gnss/AGnssRil.h
@@ -43,6 +43,8 @@
virtual jboolean updateNetworkState(jboolean connected, jint type, jboolean roaming,
jboolean available, const jstring& apn, jlong networkHandle,
jshort capabilities) = 0;
+ virtual jboolean injectNiSuplMessageData(const jbyteArray& msgData, jint length,
+ jint slotIndex) = 0;
};
class AGnssRil : public AGnssRilInterface {
@@ -55,6 +57,8 @@
jboolean updateNetworkState(jboolean connected, jint type, jboolean roaming, jboolean available,
const jstring& apn, jlong networkHandle,
jshort capabilities) override;
+ jboolean injectNiSuplMessageData(const jbyteArray& msgData, jint length,
+ jint slotIndex) override;
private:
const sp<android::hardware::gnss::IAGnssRil> mIAGnssRil;
@@ -70,6 +74,7 @@
jboolean updateNetworkState(jboolean connected, jint type, jboolean roaming, jboolean available,
const jstring& apn, jlong networkHandle,
jshort capabilities) override;
+ jboolean injectNiSuplMessageData(const jbyteArray&, jint, jint) override;
private:
const sp<android::hardware::gnss::V1_0::IAGnssRil> mAGnssRil_V1_0;
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 316c736..89cbf53 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -646,6 +646,15 @@
@EnabledAfter(targetSdkVersion = Build.VERSION_CODES.Q)
private static final long USE_SET_LOCATION_ENABLED = 117835097L;
+ /**
+ * Forces wipeDataNoLock to attempt removing the user or throw an error as
+ * opposed to trying to factory reset the device first and only then falling back to user
+ * removal.
+ */
+ @ChangeId
+ @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public static final long EXPLICIT_WIPE_BEHAVIOUR = 242193913L;
+
// Only add to the end of the list. Do not change or rearrange these values, that will break
// historical data. Do not use negative numbers or zero, logger only handles positive
// integers.
@@ -6699,8 +6708,8 @@
}
@Override
- public void wipeDataWithReason(int flags, String wipeReasonForUser,
- boolean calledOnParentInstance) {
+ public void wipeDataWithReason(int flags, @NonNull String wipeReasonForUser,
+ boolean calledOnParentInstance, boolean factoryReset) {
if (!mHasFeature && !hasCallingOrSelfPermission(permission.MASTER_CLEAR)) {
return;
}
@@ -6782,7 +6791,8 @@
"DevicePolicyManager.wipeDataWithReason() from %s, organization-owned? %s",
adminName, calledByProfileOwnerOnOrgOwnedDevice);
- wipeDataNoLock(adminComp, flags, internalReason, wipeReasonForUser, userId);
+ wipeDataNoLock(adminComp, flags, internalReason, wipeReasonForUser, userId,
+ calledOnParentInstance, factoryReset);
}
private String getGenericWipeReason(
@@ -6844,8 +6854,13 @@
Slogf.i(LOG_TAG, "Cleaning up device-wide policies done.");
}
+ /**
+ * @param factoryReset null: legacy behaviour, false: attempt to remove user, true: attempt to
+ * factory reset
+ */
private void wipeDataNoLock(ComponentName admin, int flags, String internalReason,
- String wipeReasonForUser, int userId) {
+ @NonNull String wipeReasonForUser, int userId, boolean calledOnParentInstance,
+ @Nullable Boolean factoryReset) {
wtfIfInLock();
mInjector.binderWithCleanCallingIdentity(() -> {
@@ -6863,7 +6878,37 @@
+ " restriction is set for user " + userId);
}
- if (userId == UserHandle.USER_SYSTEM) {
+ boolean isSystemUser = userId == UserHandle.USER_SYSTEM;
+ boolean wipeDevice;
+ if (factoryReset == null || !CompatChanges.isChangeEnabled(EXPLICIT_WIPE_BEHAVIOUR)) {
+ // Legacy mode
+ wipeDevice = isSystemUser;
+ } else {
+ // Explicit behaviour
+ if (factoryReset) {
+ // TODO(b/254031494) Replace with new factory reset permission checks
+ boolean hasPermission = isDeviceOwnerUserId(userId)
+ || (isOrganizationOwnedDeviceWithManagedProfile()
+ && calledOnParentInstance);
+ Preconditions.checkState(hasPermission,
+ "Admin %s does not have permission to factory reset the device.",
+ userId);
+ wipeDevice = true;
+ } else {
+ Preconditions.checkCallAuthorization(!isSystemUser,
+ "User %s is a system user and cannot be removed", userId);
+ boolean isLastNonHeadlessUser = getUserInfo(userId).isFull()
+ && mUserManager.getAliveUsers().stream()
+ .filter((it) -> it.getUserHandle().getIdentifier() != userId)
+ .noneMatch(UserInfo::isFull);
+ Preconditions.checkState(!isLastNonHeadlessUser,
+ "Removing user %s would leave the device without any active users. "
+ + "Consider factory resetting the device instead.",
+ userId);
+ wipeDevice = false;
+ }
+ }
+ if (wipeDevice) {
forceWipeDeviceNoLock(
(flags & WIPE_EXTERNAL_STORAGE) != 0,
internalReason,
@@ -7131,7 +7176,7 @@
}
@Override
- public void reportFailedPasswordAttempt(int userHandle) {
+ public void reportFailedPasswordAttempt(int userHandle, boolean parent) {
Preconditions.checkArgumentNonnegative(userHandle, "Invalid userId");
final CallerIdentity caller = getCallerIdentity();
@@ -7153,7 +7198,7 @@
saveSettingsLocked(userHandle);
if (mHasFeature) {
strictestAdmin = getAdminWithMinimumFailedPasswordsForWipeLocked(
- userHandle, /* parent */ false);
+ userHandle, /* parent= */ false);
int max = strictestAdmin != null
? strictestAdmin.maximumFailedPasswordsForWipe : 0;
if (max > 0 && policy.mFailedPasswordAttempts >= max) {
@@ -7183,10 +7228,13 @@
// IMPORTANT: Call without holding the lock to prevent deadlock.
try {
wipeDataNoLock(strictestAdmin.info.getComponent(),
- /*flags=*/ 0,
- /*reason=*/ "reportFailedPasswordAttempt()",
+ /* flags= */ 0,
+ /* reason= */ "reportFailedPasswordAttempt()",
getFailedPasswordAttemptWipeMessage(),
- userId);
+ userId,
+ /* calledOnParentInstance= */ parent,
+ // factoryReset=null to enable U- behaviour
+ /* factoryReset= */ null);
} catch (SecurityException e) {
Slogf.w(LOG_TAG, "Failed to wipe user " + userId
+ " after max failed password attempts reached.", e);
@@ -7195,7 +7243,7 @@
if (mInjector.securityLogIsLoggingEnabled()) {
SecurityLog.writeEvent(SecurityLog.TAG_KEYGUARD_DISMISS_AUTH_ATTEMPT,
- /*result*/ 0, /*method strength*/ 1);
+ /* result= */ 0, /* method strength= */ 1);
}
}
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
index 3fbc400..640bde3 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
@@ -244,7 +244,7 @@
.setCurrentMethodVisible();
}
verify(mMockInputMethod, times(showSoftInput ? 1 : 0))
- .showSoftInput(any(), anyInt(), any());
+ .showSoftInput(any(), any(), anyInt(), any());
}
protected void verifyHideSoftInput(boolean setNotVisible, boolean hideSoftInput)
@@ -254,6 +254,6 @@
.setCurrentMethodNotVisible();
}
verify(mMockInputMethod, times(hideSoftInput ? 1 : 0))
- .hideSoftInput(any(), anyInt(), any());
+ .hideSoftInput(any(), any(), anyInt(), any());
}
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUidStateTrackerTest.java b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUidStateTrackerTest.java
index e1713b0..98e895a 100644
--- a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUidStateTrackerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUidStateTrackerTest.java
@@ -22,6 +22,7 @@
import static android.app.AppOpsManager.OP_CAMERA;
import static android.app.AppOpsManager.OP_COARSE_LOCATION;
import static android.app.AppOpsManager.OP_FINE_LOCATION;
+import static android.app.AppOpsManager.OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO;
import static android.app.AppOpsManager.OP_RECORD_AUDIO;
import static android.app.AppOpsManager.OP_WIFI_SCAN;
import static android.app.AppOpsManager.UID_STATE_BACKGROUND;
@@ -127,6 +128,8 @@
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_CAMERA, MODE_FOREGROUND));
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_COARSE_LOCATION, MODE_FOREGROUND));
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_FINE_LOCATION, MODE_FOREGROUND));
+ assertEquals(MODE_IGNORED,
+ mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
}
@Test
@@ -137,6 +140,8 @@
.update();
assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_RECORD_AUDIO, MODE_FOREGROUND));
+ assertEquals(MODE_ALLOWED,
+ mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_CAMERA, MODE_FOREGROUND));
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_COARSE_LOCATION, MODE_FOREGROUND));
@@ -151,6 +156,8 @@
.update();
assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_RECORD_AUDIO, MODE_FOREGROUND));
+ assertEquals(MODE_ALLOWED,
+ mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_CAMERA, MODE_FOREGROUND));
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_COARSE_LOCATION, MODE_FOREGROUND));
@@ -169,6 +176,8 @@
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_RECORD_AUDIO, MODE_FOREGROUND));
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_COARSE_LOCATION, MODE_FOREGROUND));
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_FINE_LOCATION, MODE_FOREGROUND));
+ assertEquals(MODE_IGNORED,
+ mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
}
@Test
@@ -183,6 +192,8 @@
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_RECORD_AUDIO, MODE_FOREGROUND));
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_COARSE_LOCATION, MODE_FOREGROUND));
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_FINE_LOCATION, MODE_FOREGROUND));
+ assertEquals(MODE_IGNORED,
+ mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
}
@Test
@@ -197,6 +208,8 @@
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_RECORD_AUDIO, MODE_FOREGROUND));
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_CAMERA, MODE_FOREGROUND));
+ assertEquals(MODE_IGNORED,
+ mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
}
@Test
@@ -211,6 +224,8 @@
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_RECORD_AUDIO, MODE_FOREGROUND));
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_CAMERA, MODE_FOREGROUND));
+ assertEquals(MODE_IGNORED,
+ mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
}
@Test
@@ -314,6 +329,8 @@
.update();
assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_RECORD_AUDIO, MODE_FOREGROUND));
+ assertEquals(MODE_ALLOWED,
+ mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
}
@Test
@@ -328,6 +345,8 @@
.update();
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_RECORD_AUDIO, MODE_FOREGROUND));
+ assertEquals(MODE_IGNORED,
+ mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
}
@Test
@@ -403,6 +422,8 @@
assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_CAMERA, MODE_FOREGROUND));
assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_COARSE_LOCATION, MODE_FOREGROUND));
assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_FINE_LOCATION, MODE_FOREGROUND));
+ assertEquals(MODE_ALLOWED,
+ mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
}
@Test
@@ -418,6 +439,8 @@
assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_CAMERA, MODE_FOREGROUND));
assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_COARSE_LOCATION, MODE_FOREGROUND));
assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_FINE_LOCATION, MODE_FOREGROUND));
+ assertEquals(MODE_ALLOWED,
+ mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
}
@Test
@@ -433,6 +456,8 @@
assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_CAMERA, MODE_FOREGROUND));
assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_COARSE_LOCATION, MODE_FOREGROUND));
assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_FINE_LOCATION, MODE_FOREGROUND));
+ assertEquals(MODE_ALLOWED,
+ mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
}
@Test
diff --git a/services/tests/servicestests/res/xml/usertypes_test_profile.xml b/services/tests/servicestests/res/xml/usertypes_test_profile.xml
index b27f49d..1a6dae37 100644
--- a/services/tests/servicestests/res/xml/usertypes_test_profile.xml
+++ b/services/tests/servicestests/res/xml/usertypes_test_profile.xml
@@ -33,6 +33,7 @@
<user-properties
showInLauncher='2020'
startWithParent='false'
+ useParentsContacts='false'
/>
</profile-type>
<profile-type name='custom.test.1' max-allowed-per-parent='14' />
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
index 4c939f0..0262f56 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
@@ -82,6 +82,7 @@
/* blockedActivities= */ new ArraySet<>(),
VirtualDeviceParams.ACTIVITY_POLICY_DEFAULT_ALLOWED,
/* activityListener= */ null,
+ /* pipBlockedCallback= */ null,
/* activityBlockedCallback= */ null,
/* secureWindowCallback= */ null,
/* deviceProfile= */ DEVICE_PROFILE_APP_STREAMING);
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
index ddb3049..8e669f0 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
@@ -140,7 +140,7 @@
import android.security.keystore.AttestationUtils;
import android.telephony.TelephonyManager;
import android.telephony.data.ApnSetting;
-import android.test.MoreAsserts; // TODO(b/171932723): replace by Truth
+import android.test.MoreAsserts;
import android.util.ArraySet;
import android.util.Log;
import android.util.Pair;
@@ -5087,7 +5087,7 @@
}
@Test
- public void testWipeDataDeviceOwner() throws Exception {
+ public void testWipeDevice_DeviceOwner() throws Exception {
setDeviceOwner();
when(getServices().userManager.getUserRestrictionSource(
UserManager.DISALLOW_FACTORY_RESET,
@@ -5096,7 +5096,7 @@
when(mContext.getResources().getString(R.string.work_profile_deleted_description_dpm_wipe)).
thenReturn("Just a test string.");
- dpm.wipeData(0);
+ dpm.wipeDevice(0);
verifyRebootWipeUserData(/* wipeEuicc= */ false);
}
@@ -5111,13 +5111,13 @@
when(mContext.getResources().getString(R.string.work_profile_deleted_description_dpm_wipe)).
thenReturn("Just a test string.");
- dpm.wipeData(WIPE_EUICC);
+ dpm.wipeDevice(WIPE_EUICC);
verifyRebootWipeUserData(/* wipeEuicc= */ true);
}
@Test
- public void testWipeDataDeviceOwnerDisallowed() throws Exception {
+ public void testWipeDevice_DeviceOwnerDisallowed() throws Exception {
setDeviceOwner();
when(getServices().userManager.getUserRestrictionSource(
UserManager.DISALLOW_FACTORY_RESET,
@@ -5128,7 +5128,7 @@
// The DO is not allowed to wipe the device if the user restriction was set
// by the system
assertExpectException(SecurityException.class, /* messageRegex= */ null,
- () -> dpm.wipeData(0));
+ () -> dpm.wipeDevice(0));
}
@Test
@@ -7986,7 +7986,7 @@
}
@Test
- public void testWipeData_financeDo_success() throws Exception {
+ public void testWipeDevice_financeDo_success() throws Exception {
setDeviceOwner();
dpm.setDeviceOwnerType(admin1, DEVICE_OWNER_TYPE_FINANCED);
when(getServices().userManager.getUserRestrictionSource(
@@ -7997,7 +7997,7 @@
.getString(R.string.work_profile_deleted_description_dpm_wipe))
.thenReturn("Test string");
- dpm.wipeData(0);
+ dpm.wipeDevice(0);
verifyRebootWipeUserData(/* wipeEuicc= */ false);
}
diff --git a/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java b/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java
index fa5b6b2..657bda6 100644
--- a/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java
@@ -22,8 +22,6 @@
import static com.android.server.display.DisplayAdapter.DISPLAY_DEVICE_EVENT_ADDED;
import static com.android.server.display.DisplayAdapter.DISPLAY_DEVICE_EVENT_CHANGED;
import static com.android.server.display.DisplayAdapter.DISPLAY_DEVICE_EVENT_REMOVED;
-import static com.android.server.display.LogicalDisplay.DISPLAY_PHASE_DISABLED;
-import static com.android.server.display.LogicalDisplay.DISPLAY_PHASE_ENABLED;
import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_ADDED;
import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_REMOVED;
@@ -54,8 +52,6 @@
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
-import com.android.server.display.layout.Layout;
-
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -87,7 +83,6 @@
@Mock Resources mResourcesMock;
@Mock IPowerManager mIPowerManagerMock;
@Mock IThermalService mIThermalServiceMock;
- @Mock DeviceStateToLayoutMap mDeviceStateToLayoutMapMock;
@Captor ArgumentCaptor<LogicalDisplay> mDisplayCaptor;
@@ -133,13 +128,11 @@
when(mResourcesMock.getIntArray(
com.android.internal.R.array.config_deviceStatesOnWhichToSleep))
.thenReturn(new int[]{0});
- when(mDeviceStateToLayoutMapMock.get(-1)).thenReturn(new Layout());
mLooper = new TestLooper();
mHandler = new Handler(mLooper.getLooper());
mLogicalDisplayMapper = new LogicalDisplayMapper(mContextMock, mDisplayDeviceRepo,
- mListenerMock, new DisplayManagerService.SyncRoot(), mHandler,
- mDeviceStateToLayoutMapMock);
+ mListenerMock, new DisplayManagerService.SyncRoot(), mHandler);
}
@@ -510,58 +503,6 @@
/* isBootCompleted= */true));
}
- @Test
- public void testDeviceStateLocked() {
- DisplayDevice device1 = createDisplayDevice(Display.TYPE_INTERNAL, 600, 800,
- DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY);
- DisplayDevice device2 = createDisplayDevice(Display.TYPE_INTERNAL, 600, 800,
- DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY);
-
- Layout layout = new Layout();
- layout.createDisplayLocked(device1.getDisplayDeviceInfoLocked().address, true, true);
- layout.createDisplayLocked(device2.getDisplayDeviceInfoLocked().address, false, false);
- when(mDeviceStateToLayoutMapMock.get(0)).thenReturn(layout);
-
- layout = new Layout();
- layout.createDisplayLocked(device1.getDisplayDeviceInfoLocked().address, false, false);
- layout.createDisplayLocked(device2.getDisplayDeviceInfoLocked().address, true, true);
- when(mDeviceStateToLayoutMapMock.get(1)).thenReturn(layout);
- when(mDeviceStateToLayoutMapMock.get(2)).thenReturn(layout);
-
- LogicalDisplay display1 = add(device1);
- assertEquals(info(display1).address, info(device1).address);
- assertEquals(DEFAULT_DISPLAY, id(display1));
-
- LogicalDisplay display2 = add(device2);
- assertEquals(info(display2).address, info(device2).address);
- // We can only have one default display
- assertEquals(DEFAULT_DISPLAY, id(display1));
-
- mLogicalDisplayMapper.setDeviceStateLocked(0, false);
- mLooper.moveTimeForward(1000);
- mLooper.dispatchAll();
- assertEquals(DISPLAY_PHASE_ENABLED,
- mLogicalDisplayMapper.getDisplayLocked(device1).getPhase());
- assertEquals(DISPLAY_PHASE_DISABLED,
- mLogicalDisplayMapper.getDisplayLocked(device2).getPhase());
-
- mLogicalDisplayMapper.setDeviceStateLocked(1, false);
- mLooper.moveTimeForward(1000);
- mLooper.dispatchAll();
- assertEquals(DISPLAY_PHASE_DISABLED,
- mLogicalDisplayMapper.getDisplayLocked(device1).getPhase());
- assertEquals(DISPLAY_PHASE_ENABLED,
- mLogicalDisplayMapper.getDisplayLocked(device2).getPhase());
-
- mLogicalDisplayMapper.setDeviceStateLocked(2, false);
- mLooper.moveTimeForward(1000);
- mLooper.dispatchAll();
- assertEquals(DISPLAY_PHASE_DISABLED,
- mLogicalDisplayMapper.getDisplayLocked(device1).getPhase());
- assertEquals(DISPLAY_PHASE_ENABLED,
- mLogicalDisplayMapper.getDisplayLocked(device2).getPhase());
- }
-
/////////////////
// Helper Methods
/////////////////
diff --git a/services/tests/servicestests/src/com/android/server/job/BackgroundRestrictionsTest.java b/services/tests/servicestests/src/com/android/server/job/BackgroundRestrictionsTest.java
index 9c8e72c..f5029ec 100644
--- a/services/tests/servicestests/src/com/android/server/job/BackgroundRestrictionsTest.java
+++ b/services/tests/servicestests/src/com/android/server/job/BackgroundRestrictionsTest.java
@@ -71,7 +71,7 @@
private static final String TEST_APP_PACKAGE = "com.android.servicestests.apps.jobtestapp";
private static final String TEST_APP_ACTIVITY = TEST_APP_PACKAGE + ".TestJobActivity";
private static final long POLL_INTERVAL = 500;
- private static final long DEFAULT_WAIT_TIMEOUT = 5000;
+ private static final long DEFAULT_WAIT_TIMEOUT = 10_000;
private Context mContext;
private AppOpsManager mAppOpsManager;
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 808130a..164161e 100644
--- a/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
+++ b/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
@@ -427,6 +427,35 @@
}
@Test
+ public void testEstimatedNetworkBytes() throws Exception {
+ assertPersistedEquals(new JobInfo.Builder(0, mComponent)
+ .setPersisted(true)
+ .setRequiredNetwork(new NetworkRequest.Builder().build())
+ .setEstimatedNetworkBytes(
+ JobInfo.NETWORK_BYTES_UNKNOWN, JobInfo.NETWORK_BYTES_UNKNOWN)
+ .build());
+ assertPersistedEquals(new JobInfo.Builder(0, mComponent)
+ .setPersisted(true)
+ .setRequiredNetwork(new NetworkRequest.Builder().build())
+ .setEstimatedNetworkBytes(5, 15)
+ .build());
+ }
+
+ @Test
+ public void testMinimumNetworkChunkBytes() throws Exception {
+ assertPersistedEquals(new JobInfo.Builder(0, mComponent)
+ .setPersisted(true)
+ .setRequiredNetwork(new NetworkRequest.Builder().build())
+ .setMinimumNetworkChunkBytes(JobInfo.NETWORK_BYTES_UNKNOWN)
+ .build());
+ assertPersistedEquals(new JobInfo.Builder(0, mComponent)
+ .setPersisted(true)
+ .setRequiredNetwork(new NetworkRequest.Builder().build())
+ .setMinimumNetworkChunkBytes(42)
+ .build());
+ }
+
+ @Test
public void testPersistedIdleConstraint() throws Exception {
JobInfo.Builder b = new Builder(8, mComponent)
.setRequiresDeviceIdle(true)
@@ -541,6 +570,15 @@
first.getNetworkType(), second.getNetworkType());
assertEquals("Invalid network.",
first.getRequiredNetwork(), second.getRequiredNetwork());
+ assertEquals("Download bytes don't match",
+ first.getEstimatedNetworkDownloadBytes(),
+ second.getEstimatedNetworkDownloadBytes());
+ assertEquals("Upload bytes don't match",
+ first.getEstimatedNetworkUploadBytes(),
+ second.getEstimatedNetworkUploadBytes());
+ assertEquals("Minimum chunk bytes don't match",
+ first.getMinimumNetworkChunkBytes(),
+ second.getMinimumNetworkChunkBytes());
assertEquals("Invalid deadline constraint.",
first.hasLateConstraint(),
second.hasLateConstraint());
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
index 5ece871..1f952c4 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
@@ -61,11 +61,13 @@
.setStartWithParent(false)
.setShowInSettings(45)
.setInheritDevicePolicy(67)
+ .setUseParentsContacts(false)
.build();
final UserProperties actualProps = new UserProperties(defaultProps);
actualProps.setShowInLauncher(14);
actualProps.setShowInSettings(32);
actualProps.setInheritDevicePolicy(51);
+ actualProps.setUseParentsContacts(true);
// Write the properties to xml.
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
@@ -152,11 +154,14 @@
// Items requiring hasManagePermission - put them here using hasManagePermission.
assertEqualGetterOrThrows(orig::getShowInSettings, copy::getShowInSettings,
hasManagePermission);
+ assertEqualGetterOrThrows(orig::getUseParentsContacts,
+ copy::getUseParentsContacts, hasManagePermission);
// Items requiring hasQueryPermission - put them here using hasQueryPermission.
// Items with no permission requirements.
assertEqualGetterOrThrows(orig::getShowInLauncher, copy::getShowInLauncher, true);
+
}
/**
@@ -196,7 +201,7 @@
assertThat(expected.getShowInLauncher()).isEqualTo(actual.getShowInLauncher());
assertThat(expected.getStartWithParent()).isEqualTo(actual.getStartWithParent());
assertThat(expected.getShowInSettings()).isEqualTo(actual.getShowInSettings());
- assertThat(expected.getInheritDevicePolicy()).isEqualTo(
- actual.getInheritDevicePolicy());
+ assertThat(expected.getInheritDevicePolicy()).isEqualTo(actual.getInheritDevicePolicy());
+ assertThat(expected.getUseParentsContacts()).isEqualTo(actual.getUseParentsContacts());
}
}
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java
index 5f48004..6fe33ee 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java
@@ -83,7 +83,8 @@
/* flags= */0,
/* letsPersonalDataIntoProfile= */false).build());
final UserProperties.Builder userProps = new UserProperties.Builder()
- .setShowInLauncher(17);
+ .setShowInLauncher(17)
+ .setUseParentsContacts(true);
final UserTypeDetails type = new UserTypeDetails.Builder()
.setName("a.name")
.setEnabled(1)
@@ -140,6 +141,7 @@
}
assertEquals(17, type.getDefaultUserPropertiesReference().getShowInLauncher());
+ assertTrue(type.getDefaultUserPropertiesReference().getUseParentsContacts());
assertEquals(23, type.getBadgeLabel(0));
assertEquals(24, type.getBadgeLabel(1));
@@ -182,6 +184,7 @@
final UserProperties props = type.getDefaultUserPropertiesReference();
assertNotNull(props);
assertFalse(props.getStartWithParent());
+ assertFalse(props.getUseParentsContacts());
assertEquals(UserProperties.SHOW_IN_LAUNCHER_WITH_PARENT, props.getShowInLauncher());
assertFalse(type.hasBadge());
@@ -263,7 +266,8 @@
final Bundle restrictions = makeRestrictionsBundle("no_config_vpn", "no_config_tethering");
final UserProperties.Builder props = new UserProperties.Builder()
.setShowInLauncher(19)
- .setStartWithParent(true);
+ .setStartWithParent(true)
+ .setUseParentsContacts(true);
final ArrayMap<String, UserTypeDetails.Builder> builders = new ArrayMap<>();
builders.put(userTypeAosp1, new UserTypeDetails.Builder()
.setName(userTypeAosp1)
@@ -289,7 +293,9 @@
assertEquals(Resources.ID_NULL, aospType.getIconBadge());
assertTrue(UserRestrictionsUtils.areEqual(restrictions, aospType.getDefaultRestrictions()));
assertEquals(19, aospType.getDefaultUserPropertiesReference().getShowInLauncher());
- assertEquals(true, aospType.getDefaultUserPropertiesReference().getStartWithParent());
+ assertTrue(aospType.getDefaultUserPropertiesReference().getStartWithParent());
+ assertTrue(aospType.getDefaultUserPropertiesReference()
+ .getUseParentsContacts());
// userTypeAosp2 should be modified.
aospType = builders.get(userTypeAosp2).createUserTypeDetails();
@@ -319,7 +325,9 @@
makeRestrictionsBundle("no_remove_user", "no_bluetooth"),
aospType.getDefaultRestrictions()));
assertEquals(2020, aospType.getDefaultUserPropertiesReference().getShowInLauncher());
- assertEquals(false, aospType.getDefaultUserPropertiesReference().getStartWithParent());
+ assertFalse(aospType.getDefaultUserPropertiesReference().getStartWithParent());
+ assertFalse(aospType.getDefaultUserPropertiesReference()
+ .getUseParentsContacts());
// userTypeOem1 should be created.
UserTypeDetails.Builder customType = builders.get(userTypeOem1);
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 a3c45b7..2e7e583 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
@@ -19,6 +19,7 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;
import static org.testng.Assert.assertThrows;
@@ -164,6 +165,14 @@
@Test
public void testCloneUser() throws Exception {
+
+ // Get the default properties for clone user type.
+ final UserTypeDetails userTypeDetails =
+ UserTypeFactory.getUserTypes().get(UserManager.USER_TYPE_PROFILE_CLONE);
+ assertWithMessage("No %s type on device", UserManager.USER_TYPE_PROFILE_CLONE)
+ .that(userTypeDetails).isNotNull();
+ final UserProperties typeProps = userTypeDetails.getDefaultUserPropertiesReference();
+
// Test that only one clone user can be created
final int primaryUserId = mUserManager.getPrimaryUser().id;
UserInfo userInfo = createProfileForUser("Clone user1",
@@ -187,6 +196,16 @@
.collect(Collectors.toList());
assertThat(cloneUsers.size()).isEqualTo(1);
+ // Check that the new clone user has the expected properties (relative to the defaults)
+ // provided that the test caller has the necessary permissions.
+ UserProperties cloneUserProperties =
+ mUserManager.getUserProperties(UserHandle.of(userInfo.id));
+ assertThat(typeProps.getUseParentsContacts())
+ .isEqualTo(cloneUserProperties.getUseParentsContacts());
+ assertThat(typeProps.getShowInLauncher())
+ .isEqualTo(cloneUserProperties.getShowInLauncher());
+ assertThrows(SecurityException.class, cloneUserProperties::getStartWithParent);
+
// Verify clone user parent
assertThat(mUserManager.getProfileParent(primaryUserId)).isNull();
UserInfo parentProfileInfo = mUserManager.getProfileParent(userInfo.id);
@@ -600,6 +619,7 @@
// provided that the test caller has the necessary permissions.
assertThat(userProps.getShowInLauncher()).isEqualTo(typeProps.getShowInLauncher());
assertThat(userProps.getShowInSettings()).isEqualTo(typeProps.getShowInSettings());
+ assertFalse(userProps.getUseParentsContacts());
assertThrows(SecurityException.class, userProps::getStartWithParent);
assertThrows(SecurityException.class, userProps::getInheritDevicePolicy);
}
diff --git a/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java
index 397770b..dcbdcdc 100644
--- a/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java
@@ -42,6 +42,7 @@
import android.os.Binder;
import android.os.IBinder;
import android.os.IHintSession;
+import android.os.PerformanceHintManager;
import android.os.Process;
import com.android.server.FgThread;
@@ -250,6 +251,32 @@
}
@Test
+ public void testSendHint() throws Exception {
+ HintManagerService service = createService();
+ IBinder token = new Binder();
+
+ AppHintSession a = (AppHintSession) service.getBinderServiceInstance()
+ .createHintSession(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION);
+
+ a.sendHint(PerformanceHintManager.Session.CPU_LOAD_RESET);
+ verify(mNativeWrapperMock, times(1)).halSendHint(anyLong(),
+ eq(PerformanceHintManager.Session.CPU_LOAD_RESET));
+
+ assertThrows(IllegalArgumentException.class, () -> {
+ a.sendHint(-1);
+ });
+
+ reset(mNativeWrapperMock);
+ // Set session to background, then the duration would not be updated.
+ service.mUidObserver.onUidStateChanged(
+ a.mUid, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
+ FgThread.getHandler().runWithScissors(() -> { }, 500);
+ assertFalse(a.updateHintAllowed());
+ a.sendHint(PerformanceHintManager.Session.CPU_LOAD_RESET);
+ verify(mNativeWrapperMock, never()).halSendHint(anyLong(), anyInt());
+ }
+
+ @Test
public void testDoHintInBackground() throws Exception {
HintManagerService service = createService();
IBinder token = new Binder();
diff --git a/services/tests/servicestests/test-apps/JobTestApp/src/com/android/servicestests/apps/jobtestapp/TestJobService.java b/services/tests/servicestests/test-apps/JobTestApp/src/com/android/servicestests/apps/jobtestapp/TestJobService.java
index 3e79407..b8585f2 100644
--- a/services/tests/servicestests/test-apps/JobTestApp/src/com/android/servicestests/apps/jobtestapp/TestJobService.java
+++ b/services/tests/servicestests/test-apps/JobTestApp/src/com/android/servicestests/apps/jobtestapp/TestJobService.java
@@ -34,7 +34,8 @@
public boolean onStartJob(JobParameters params) {
Log.i(TAG, "Test job executing: " + params.getJobId());
Intent reportJobStartIntent = new Intent(ACTION_JOB_STARTED);
- reportJobStartIntent.putExtra(JOB_PARAMS_EXTRA_KEY, params);
+ reportJobStartIntent.putExtra(JOB_PARAMS_EXTRA_KEY, params)
+ .addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
sendBroadcast(reportJobStartIntent);
return true;
}
@@ -43,7 +44,8 @@
public boolean onStopJob(JobParameters params) {
Log.i(TAG, "Test job stopped executing: " + params.getJobId());
Intent reportJobStopIntent = new Intent(ACTION_JOB_STOPPED);
- reportJobStopIntent.putExtra(JOB_PARAMS_EXTRA_KEY, params);
+ reportJobStopIntent.putExtra(JOB_PARAMS_EXTRA_KEY, params)
+ .addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
sendBroadcast(reportJobStopIntent);
// Deadline constraint is dropped on reschedule, so it's more reliable to use a new job.
return false;
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index 3a8e1cc..bd8da4e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -2284,8 +2284,7 @@
doReturn(false).when(mAtm).shouldDisableNonVrUiLocked();
spyOn(mDisplayContent.mDwpcHelper);
- doReturn(false).when(mDisplayContent.mDwpcHelper).isWindowingModeSupported(
- WINDOWING_MODE_PINNED);
+ doReturn(false).when(mDisplayContent.mDwpcHelper).isEnteringPipAllowed(anyInt());
assertFalse(activity.checkEnterPictureInPictureState("TEST", false /* beforeStopping */));
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
index 52af8ad..d99946f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
@@ -45,6 +45,7 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
@@ -375,10 +376,10 @@
displayPolicy.setCanSystemBarsBeShownByUser(false);
displayPolicy.requestTransientBars(windowState, true);
- verify(controlTarget, never()).showInsets(anyInt(), anyBoolean());
+ verify(controlTarget, never()).showInsets(anyInt(), anyBoolean(), any() /* statsToken */);
displayPolicy.setCanSystemBarsBeShownByUser(true);
displayPolicy.requestTransientBars(windowState, true);
- verify(controlTarget).showInsets(anyInt(), anyBoolean());
+ verify(controlTarget).showInsets(anyInt(), anyBoolean(), any() /* statsToken */);
}
}
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 21197ba..db1d15a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowPolicyControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowPolicyControllerTests.java
@@ -246,5 +246,10 @@
public boolean canShowTasksInRecents() {
return true;
}
+
+ @Override
+ public boolean isEnteringPipAllowed(int uid) {
+ return true;
+ }
}
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java
index ac3d0f0..75c5b6e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java
@@ -213,7 +213,7 @@
assertThat(newTaskBounds).isEqualTo(newDagBounds);
// Activity config bounds is unchanged, size compat bounds is (860x[860x860/1200=616])
- assertThat(mFirstActivity.getSizeCompatScale()).isLessThan(1f);
+ assertThat(mFirstActivity.getCompatScale()).isLessThan(1f);
assertThat(activityConfigBounds.width()).isEqualTo(activityBounds.width());
assertThat(activityConfigBounds.height()).isEqualTo(activityBounds.height());
assertThat(activitySizeCompatBounds.height()).isEqualTo(newTaskBounds.height());
diff --git a/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java b/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java
index eb8b89d..a26cad9 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java
@@ -55,7 +55,7 @@
mDisplayContent.setImeControlTarget(popup);
mDisplayContent.setImeLayeringTarget(appWin);
popup.mAttrs.format = PixelFormat.TRANSPARENT;
- mImeProvider.scheduleShowImePostLayout(appWin);
+ mImeProvider.scheduleShowImePostLayout(appWin, null /* statsToken */);
assertTrue(mImeProvider.isReadyToShowIme());
}
@@ -64,7 +64,7 @@
WindowState target = createWindow(null, TYPE_APPLICATION, "app");
mDisplayContent.setImeLayeringTarget(target);
mDisplayContent.updateImeInputAndControlTarget(target);
- mImeProvider.scheduleShowImePostLayout(target);
+ mImeProvider.scheduleShowImePostLayout(target, null /* statsToken */);
assertTrue(mImeProvider.isReadyToShowIme());
}
@@ -78,7 +78,7 @@
mDisplayContent.setImeLayeringTarget(target);
mDisplayContent.setImeControlTarget(target);
- mImeProvider.scheduleShowImePostLayout(target);
+ mImeProvider.scheduleShowImePostLayout(target, null /* statsToken */);
assertFalse(mImeProvider.isImeShowing());
mImeProvider.checkShowImePostLayout();
assertTrue(mImeProvider.isImeShowing());
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 e5842b4..e65610f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -90,7 +90,6 @@
import android.provider.DeviceConfig.Properties;
import android.view.InsetsFrameProvider;
import android.view.InsetsSource;
-import android.view.InsetsVisibilities;
import android.view.WindowManager;
import androidx.test.filters.MediumTest;
@@ -3209,7 +3208,7 @@
/** Asserts that the size of activity is larger than its parent so it is scaling. */
private void assertScaled() {
assertTrue(mActivity.inSizeCompatMode());
- assertNotEquals(1f, mActivity.getSizeCompatScale(), 0.0001f /* delta */);
+ assertNotEquals(1f, mActivity.getCompatScale(), 0.0001f /* delta */);
}
/** Asserts that the activity is best fitted in the parent. */
diff --git a/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java b/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java
index bb5aceb..6e72bf3 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java
@@ -16,6 +16,7 @@
package com.android.server.wm;
+import android.annotation.Nullable;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
@@ -26,6 +27,7 @@
import android.view.InsetsSourceControl;
import android.view.InsetsState;
import android.view.ScrollCaptureResponse;
+import android.view.inputmethod.ImeTracker;
import android.window.ClientWindowFrames;
import com.android.internal.os.IResultReceiver;
@@ -117,10 +119,12 @@
}
@Override
- public void showInsets(int types, boolean fromIme) throws RemoteException {
+ public void showInsets(int types, boolean fromIme, @Nullable ImeTracker.Token statsToken)
+ throws RemoteException {
}
@Override
- public void hideInsets(int types, boolean fromIme) throws RemoteException {
+ public void hideInsets(int types, boolean fromIme, @Nullable ImeTracker.Token statsToken)
+ throws RemoteException {
}
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
index 0139f6a..6bd3412 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
@@ -1000,7 +1000,7 @@
mDisplayContent.setImeLayeringTarget(app);
mDisplayContent.setImeInputTarget(app);
assertTrue(mDisplayContent.shouldImeAttachedToApp());
- controller.getImeSourceProvider().scheduleShowImePostLayout(app);
+ controller.getImeSourceProvider().scheduleShowImePostLayout(app, null /* statsToken */);
controller.getImeSourceProvider().getSource().setVisible(true);
controller.updateAboveInsetsState(false);
@@ -1037,7 +1037,7 @@
mDisplayContent.setImeLayeringTarget(app);
mDisplayContent.setImeInputTarget(app);
assertTrue(mDisplayContent.shouldImeAttachedToApp());
- controller.getImeSourceProvider().scheduleShowImePostLayout(app);
+ controller.getImeSourceProvider().scheduleShowImePostLayout(app, null /* statsToken */);
controller.getImeSourceProvider().getSource().setVisible(true);
controller.updateAboveInsetsState(false);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index eca7cbb..ab042d1 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -100,6 +100,7 @@
import android.view.View;
import android.view.WindowManager;
import android.view.WindowManager.DisplayImePolicy;
+import android.view.inputmethod.ImeTracker;
import android.window.ITransitionPlayer;
import android.window.ScreenCapture;
import android.window.StartingWindowInfo;
@@ -848,11 +849,13 @@
}
@Override
- public void showInsets(int i, boolean b) throws RemoteException {
+ public void showInsets(int i, boolean b, @Nullable ImeTracker.Token t)
+ throws RemoteException {
}
@Override
- public void hideInsets(int i, boolean b) throws RemoteException {
+ public void hideInsets(int i, boolean b, @Nullable ImeTracker.Token t)
+ throws RemoteException {
}
@Override
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index e099e69..d473c6a 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -17339,7 +17339,7 @@
* Subsequent attempts will return the same error until the request is made on the default
* data subscription.
*/
- public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA_SUB = 14;
+ public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA_SUBSCRIPTION = 14;
/**
* Purchase premium capability was successful and is waiting for the network to setup the
@@ -17369,7 +17369,7 @@
PURCHASE_PREMIUM_CAPABILITY_RESULT_FEATURE_NOT_SUPPORTED,
PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_NOT_AVAILABLE,
PURCHASE_PREMIUM_CAPABILITY_RESULT_ENTITLEMENT_CHECK_FAILED,
- PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA_SUB,
+ PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA_SUBSCRIPTION,
PURCHASE_PREMIUM_CAPABILITY_RESULT_PENDING_NETWORK_SETUP})
public @interface PurchasePremiumCapabilityResult {}
@@ -17409,8 +17409,8 @@
return "NETWORK_NOT_AVAILABLE";
case PURCHASE_PREMIUM_CAPABILITY_RESULT_ENTITLEMENT_CHECK_FAILED:
return "ENTITLEMENT_CHECK_FAILED";
- case PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA_SUB:
- return "NOT_DEFAULT_DATA_SUB";
+ case PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA_SUBSCRIPTION:
+ return "NOT_DEFAULT_DATA_SUBSCRIPTION";
case PURCHASE_PREMIUM_CAPABILITY_RESULT_PENDING_NETWORK_SETUP:
return "PENDING_NETWORK_SETUP";
default:
diff --git a/telephony/java/android/telephony/data/IQualifiedNetworksService.aidl b/telephony/java/android/telephony/data/IQualifiedNetworksService.aidl
index ba2b62d..8e27077 100644
--- a/telephony/java/android/telephony/data/IQualifiedNetworksService.aidl
+++ b/telephony/java/android/telephony/data/IQualifiedNetworksService.aidl
@@ -27,4 +27,5 @@
oneway void createNetworkAvailabilityProvider(int slotId, IQualifiedNetworksServiceCallback callback);
oneway void removeNetworkAvailabilityProvider(int slotId);
oneway void reportThrottleStatusChanged(int slotId, in List<ThrottleStatus> statuses);
+ oneway void reportEmergencyDataNetworkPreferredTransportChanged (int slotId, int transportType);
}
diff --git a/telephony/java/android/telephony/data/QualifiedNetworksService.java b/telephony/java/android/telephony/data/QualifiedNetworksService.java
index fb97336..56f0f9f 100644
--- a/telephony/java/android/telephony/data/QualifiedNetworksService.java
+++ b/telephony/java/android/telephony/data/QualifiedNetworksService.java
@@ -68,6 +68,7 @@
private static final int QNS_REMOVE_ALL_NETWORK_AVAILABILITY_PROVIDERS = 3;
private static final int QNS_UPDATE_QUALIFIED_NETWORKS = 4;
private static final int QNS_APN_THROTTLE_STATUS_CHANGED = 5;
+ private static final int QNS_EMERGENCY_DATA_NETWORK_PREFERRED_TRANSPORT_CHANGED = 6;
private final HandlerThread mHandlerThread;
@@ -193,6 +194,20 @@
}
/**
+ * The framework calls this method when the preferred transport type used to set up
+ * emergency data network is changed.
+ *
+ * This method is meant to be overridden.
+ *
+ * @param transportType transport type changed to be preferred
+ */
+ public void reportEmergencyDataNetworkPreferredTransportChanged(
+ @AccessNetworkConstants.TransportType int transportType) {
+ Log.d(TAG, "reportEmergencyDataNetworkPreferredTransportChanged: "
+ + AccessNetworkConstants.transportTypeToString(transportType));
+ }
+
+ /**
* Called when the qualified networks provider is removed. The extended class should
* implement this method to perform cleanup works.
*/
@@ -237,6 +252,13 @@
}
break;
+ case QNS_EMERGENCY_DATA_NETWORK_PREFERRED_TRANSPORT_CHANGED:
+ if (provider != null) {
+ int transportType = (int) message.arg2;
+ provider.reportEmergencyDataNetworkPreferredTransportChanged(transportType);
+ }
+ break;
+
case QNS_REMOVE_NETWORK_AVAILABILITY_PROVIDER:
if (provider != null) {
provider.close();
@@ -332,6 +354,14 @@
mHandler.obtainMessage(QNS_APN_THROTTLE_STATUS_CHANGED, slotIndex, 0, statuses)
.sendToTarget();
}
+
+ @Override
+ public void reportEmergencyDataNetworkPreferredTransportChanged(int slotIndex,
+ @AccessNetworkConstants.TransportType int transportType) {
+ mHandler.obtainMessage(
+ QNS_EMERGENCY_DATA_NETWORK_PREFERRED_TRANSPORT_CHANGED,
+ slotIndex, transportType).sendToTarget();
+ }
}
private void log(String s) {
diff --git a/tests/TouchLatency/Android.bp b/tests/TouchLatency/Android.bp
index 3a9e240..4ef1ead 100644
--- a/tests/TouchLatency/Android.bp
+++ b/tests/TouchLatency/Android.bp
@@ -12,6 +12,7 @@
manifest: "app/src/main/AndroidManifest.xml",
// omit gradle 'build' dir
srcs: ["app/src/main/java/**/*.java"],
+ static_libs: ["com.google.android.material_material"],
resource_dirs: ["app/src/main/res"],
aaptflags: ["--auto-add-overlay"],
sdk_version: "current",
diff --git a/tests/TouchLatency/app/build.gradle b/tests/TouchLatency/app/build.gradle
index f5ae6f4..129baab 100644
--- a/tests/TouchLatency/app/build.gradle
+++ b/tests/TouchLatency/app/build.gradle
@@ -6,7 +6,7 @@
defaultConfig {
applicationId "com.prefabulated.touchlatency"
- minSdkVersion 28
+ minSdkVersion 30
targetSdkVersion 33
versionCode 1
versionName "1.0"
@@ -17,4 +17,9 @@
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
+
+ dependencies {
+ implementation 'androidx.appcompat:appcompat:1.5.1'
+ implementation 'com.google.android.material:material:1.6.0'
+ }
}
diff --git a/tests/TouchLatency/app/src/main/java/com/prefabulated/touchlatency/TouchLatencyActivity.java b/tests/TouchLatency/app/src/main/java/com/prefabulated/touchlatency/TouchLatencyActivity.java
index 6ab3b3e..2e93c87 100644
--- a/tests/TouchLatency/app/src/main/java/com/prefabulated/touchlatency/TouchLatencyActivity.java
+++ b/tests/TouchLatency/app/src/main/java/com/prefabulated/touchlatency/TouchLatencyActivity.java
@@ -16,7 +16,6 @@
package com.prefabulated.touchlatency;
-import android.app.Activity;
import android.app.ActivityOptions;
import android.content.Intent;
import android.hardware.display.DisplayManager;
@@ -30,25 +29,49 @@
import android.view.Window;
import android.view.WindowManager;
-public class TouchLatencyActivity extends Activity {
- private Mode mDisplayModes[];
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.google.android.material.slider.RangeSlider;
+import com.google.android.material.slider.RangeSlider.OnChangeListener;
+
+public class TouchLatencyActivity extends AppCompatActivity {
+ private static final int REFRESH_RATE_SLIDER_MIN = 20;
+ private static final int REFRESH_RATE_SLIDER_STEP = 5;
+
+ private Menu mMenu;
+ private Mode[] mDisplayModes;
private int mCurrentModeIndex;
+ private float mSliderPreferredRefreshRate;
private DisplayManager mDisplayManager;
+
private final DisplayManager.DisplayListener mDisplayListener =
new DisplayManager.DisplayListener() {
@Override
public void onDisplayAdded(int i) {
- invalidateOptionsMenu();
+ updateOptionsMenu();
}
@Override
public void onDisplayRemoved(int i) {
- invalidateOptionsMenu();
+ updateOptionsMenu();
}
@Override
public void onDisplayChanged(int i) {
- invalidateOptionsMenu();
+ updateOptionsMenu();
+ }
+ };
+
+ private final RangeSlider.OnChangeListener mRefreshRateSliderListener = new OnChangeListener() {
+ @Override
+ public void onValueChange(@NonNull RangeSlider slider, float value, boolean fromUser) {
+ if (value == mSliderPreferredRefreshRate) return;
+
+ mSliderPreferredRefreshRate = value;
+ WindowManager.LayoutParams w = getWindow().getAttributes();
+ w.preferredRefreshRate = mSliderPreferredRefreshRate;
+ getWindow().setAttributes(w);
}
};
@@ -75,17 +98,23 @@
Trace.endSection();
}
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- Trace.beginSection("TouchLatencyActivity onCreateOptionsMenu");
- // Inflate the menu; this adds items to the action bar if it is present.
- getMenuInflater().inflate(R.menu.menu_touch_latency, menu);
+ public void updateOptionsMenu() {
if (mDisplayModes.length > 1) {
- MenuItem menuItem = menu.findItem(R.id.display_mode);
+ MenuItem menuItem = mMenu.findItem(R.id.display_mode);
Mode currentMode = getWindowManager().getDefaultDisplay().getMode();
updateDisplayMode(menuItem, currentMode);
}
- updateMultiDisplayMenu(menu.findItem(R.id.multi_display));
+ updateRefreshRateMenu(mMenu.findItem(R.id.frame_rate));
+ updateMultiDisplayMenu(mMenu.findItem(R.id.multi_display));
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ Trace.beginSection("TouchLatencyActivity onCreateOptionsMenu");
+ mMenu = menu;
+ // Inflate the menu; this adds items to the action bar if it is present.
+ getMenuInflater().inflate(R.menu.menu_touch_latency, mMenu);
+ updateOptionsMenu();
Trace.endSection();
return true;
}
@@ -96,6 +125,32 @@
menuItem.setVisible(true);
}
+ private float getHighestRefreshRate() {
+ float maxRefreshRate = 0;
+ for (Display.Mode mode : getDisplay().getSupportedModes()) {
+ if (sameSizeMode(mode) && mode.getRefreshRate() > maxRefreshRate) {
+ maxRefreshRate = mode.getRefreshRate();
+ }
+ }
+ return maxRefreshRate;
+ }
+
+ private void updateRefreshRateMenu(MenuItem item) {
+ item.setActionView(R.layout.refresh_rate_layout);
+ RangeSlider slider = item.getActionView().findViewById(R.id.slider_from_layout);
+ slider.addOnChangeListener(mRefreshRateSliderListener);
+
+ float highestRefreshRate = getHighestRefreshRate();
+ slider.setValueFrom(REFRESH_RATE_SLIDER_MIN);
+ slider.setValueTo(highestRefreshRate);
+ slider.setStepSize(REFRESH_RATE_SLIDER_STEP);
+ if (mSliderPreferredRefreshRate < REFRESH_RATE_SLIDER_MIN
+ || mSliderPreferredRefreshRate > highestRefreshRate) {
+ mSliderPreferredRefreshRate = highestRefreshRate;
+ }
+ slider.setValues(mSliderPreferredRefreshRate);
+ }
+
private void updateMultiDisplayMenu(MenuItem item) {
item.setVisible(mDisplayManager.getDisplays().length > 1);
}
@@ -105,6 +160,12 @@
mDisplayManager.registerDisplayListener(mDisplayListener, new Handler());
}
+ private boolean sameSizeMode(Display.Mode mode) {
+ Mode currentMode = mDisplayModes[mCurrentModeIndex];
+ return currentMode.getPhysicalHeight() == mode.getPhysicalHeight()
+ && currentMode.getPhysicalWidth() == mode.getPhysicalWidth();
+ }
+
public void changeDisplayMode(MenuItem item) {
Window w = getWindow();
WindowManager.LayoutParams params = w.getAttributes();
@@ -112,10 +173,7 @@
int modeIndex = (mCurrentModeIndex + 1) % mDisplayModes.length;
while (modeIndex != mCurrentModeIndex) {
// skip modes with different resolutions
- Mode currentMode = mDisplayModes[mCurrentModeIndex];
- Mode nextMode = mDisplayModes[modeIndex];
- if (currentMode.getPhysicalHeight() == nextMode.getPhysicalHeight()
- && currentMode.getPhysicalWidth() == nextMode.getPhysicalWidth()) {
+ if (sameSizeMode(mDisplayModes[modeIndex])) {
break;
}
modeIndex = (modeIndex + 1) % mDisplayModes.length;
diff --git a/tests/TouchLatency/app/src/main/res/layout/refresh_rate_layout.xml b/tests/TouchLatency/app/src/main/res/layout/refresh_rate_layout.xml
new file mode 100644
index 0000000..bb9ce60
--- /dev/null
+++ b/tests/TouchLatency/app/src/main/res/layout/refresh_rate_layout.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <com.google.android.material.slider.RangeSlider
+ android:id="@+id/slider_from_layout"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:tickColor="@color/cardview_light_background"
+ app:trackColor="@color/cardview_light_background"
+ app:thumbColor="@color/cardview_dark_background"
+ android:visibility="visible"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/tests/TouchLatency/app/src/main/res/menu/menu_touch_latency.xml b/tests/TouchLatency/app/src/main/res/menu/menu_touch_latency.xml
index abc7fd5..7169021 100644
--- a/tests/TouchLatency/app/src/main/res/menu/menu_touch_latency.xml
+++ b/tests/TouchLatency/app/src/main/res/menu/menu_touch_latency.xml
@@ -14,21 +14,25 @@
limitations under the License.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" tools:context=".TouchLatencyActivity">
<item
android:id="@+id/action_settings"
android:orderInCategory="101"
- android:showAsAction="always"
- android:title="@string/mode"/>
+ android:title="@string/mode"
+ app:showAsAction="always" />
+ <item
+ android:id="@+id/frame_rate"
+ android:title="@string/frame_rate"
+ app:showAsAction="collapseActionView" />
<item
android:id="@+id/display_mode"
- android:showAsAction="ifRoom"
android:title="@string/display_mode"
- android:visible="false"/>
-
+ android:visible="false"
+ app:showAsAction="always" />
<item
android:id="@+id/multi_display"
- android:showAsAction="ifRoom"
android:title="@string/multi_display"
- android:visible="false"/>
+ android:visible="false"
+ app:showAsAction="ifRoom" />
</menu>
diff --git a/tests/TouchLatency/app/src/main/res/values/strings.xml b/tests/TouchLatency/app/src/main/res/values/strings.xml
index 5ee86d8..cad2df7 100644
--- a/tests/TouchLatency/app/src/main/res/values/strings.xml
+++ b/tests/TouchLatency/app/src/main/res/values/strings.xml
@@ -18,5 +18,6 @@
<string name="mode">Touch</string>
<string name="display_mode">Mode</string>
+ <string name="frame_rate">Frame Rate</string>
<string name="multi_display">multi-display</string>
</resources>
diff --git a/tests/TouchLatency/app/src/main/res/values/styles.xml b/tests/TouchLatency/app/src/main/res/values/styles.xml
index 22da7c1..b23a87e 100644
--- a/tests/TouchLatency/app/src/main/res/values/styles.xml
+++ b/tests/TouchLatency/app/src/main/res/values/styles.xml
@@ -16,7 +16,7 @@
<resources>
<!-- Base application theme. -->
- <style name="AppTheme" parent="@android:style/Theme.Material.Light.DarkActionBar">
+ <style name="AppTheme" parent="Theme.MaterialComponents.Light.DarkActionBar">
<!-- Customize your theme here. -->
</style>
diff --git a/tests/TouchLatency/gradle.properties b/tests/TouchLatency/gradle.properties
index 1d3591c..ccd5dda 100644
--- a/tests/TouchLatency/gradle.properties
+++ b/tests/TouchLatency/gradle.properties
@@ -15,4 +15,5 @@
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
-# org.gradle.parallel=true
\ No newline at end of file
+# org.gradle.parallel=true
+android.useAndroidX=true
\ No newline at end of file
diff --git a/tests/utils/testutils/java/com/android/internal/util/test/BroadcastInterceptingContext.java b/tests/utils/testutils/java/com/android/internal/util/test/BroadcastInterceptingContext.java
index 3da8b46..133c176 100644
--- a/tests/utils/testutils/java/com/android/internal/util/test/BroadcastInterceptingContext.java
+++ b/tests/utils/testutils/java/com/android/internal/util/test/BroadcastInterceptingContext.java
@@ -147,12 +147,39 @@
@Override
public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
- return registerReceiver(receiver, filter, null, null);
+ return registerReceiver(receiver, filter, null, null, 0);
+ }
+
+ /**
+ * Registers the specified {@code receiver} to listen for broadcasts that match the {@code
+ * filter} in the current process.
+ *
+ * <p>Since this method only listens for broadcasts in the current process, the provided {@code
+ * flags} are ignored; this method is primarily intended to allow receivers that register with
+ * flags to register in the current process during tests.
+ */
+ @Override
+ public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter, int flags) {
+ return registerReceiver(receiver, filter, null, null, flags);
}
@Override
public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter,
String broadcastPermission, Handler scheduler) {
+ return registerReceiver(receiver, filter, broadcastPermission, scheduler, 0);
+ }
+
+ /**
+ * Registers the specified {@code receiver} to listen for broadcasts that match the {@code
+ * filter} to run in the context of the specified {@code scheduler} in the current process.
+ *
+ * <p>Since this method only listens for broadcasts in the current process, the provided {@code
+ * flags} are ignored; this method is primarily intended to allow receivers that register with
+ * flags to register in the current process during tests.
+ */
+ @Override
+ public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter,
+ String broadcastPermission, Handler scheduler, int flags) {
synchronized (mInterceptors) {
mInterceptors.add(new BroadcastInterceptor(receiver, filter, scheduler));
}
diff --git a/tools/aapt2/Resources.proto b/tools/aapt2/Resources.proto
index 2a450ba..1d7fd1d 100644
--- a/tools/aapt2/Resources.proto
+++ b/tools/aapt2/Resources.proto
@@ -46,6 +46,13 @@
string version = 2;
}
+// References to non local resources
+message DynamicRefTable {
+ PackageId package_id = 1;
+ string package_name = 2;
+}
+
+
// Top level message representing a resource table.
message ResourceTable {
// The string pool containing source paths referenced throughout the resource table. This does
@@ -60,6 +67,8 @@
// The version fingerprints of the tools that built the resource table.
repeated ToolFingerprint tool_fingerprint = 4;
+
+ repeated DynamicRefTable dynamic_ref_table = 5;
}
// A package ID in the range [0x00, 0xff].
diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp
index 116dcd6..a8d2299 100644
--- a/tools/aapt2/cmd/Link.cpp
+++ b/tools/aapt2/cmd/Link.cpp
@@ -1085,6 +1085,10 @@
const auto localeconfig_entry =
ResolveTableEntry(context_, &final_table_, localeconfig_reference);
if (!localeconfig_entry) {
+ // If locale config is resolved from external symbols - skip validation.
+ if (context_->GetExternalSymbols()->FindByReference(*localeconfig_reference)) {
+ return true;
+ }
context_->GetDiagnostics()->Error(
android::DiagMessage(localeConfig->compiled_value->GetSource())
<< "no localeConfig entry");
diff --git a/tools/aapt2/cmd/Link_test.cpp b/tools/aapt2/cmd/Link_test.cpp
index 254f3a5..28fcc1a 100644
--- a/tools/aapt2/cmd/Link_test.cpp
+++ b/tools/aapt2/cmd/Link_test.cpp
@@ -840,6 +840,43 @@
ASSERT_TRUE(Link(link1_args, &diag));
}
+TEST_F(LinkTest, LocaleConfigVerificationExternalSymbol) {
+ StdErrDiagnostics diag;
+ const std::string base_files_dir = GetTestPath("base");
+ ASSERT_TRUE(CompileFile(GetTestPath("res/xml/locales_config.xml"), R"(
+ <locale-config xmlns:android="http://schemas.android.com/apk/res/android">
+ <locale android:name="en-US"/>
+ <locale android:name="pt"/>
+ <locale android:name="es-419"/>
+ <locale android:name="zh-Hans-SG"/>
+ </locale-config>)",
+ base_files_dir, &diag));
+ const std::string base_apk = GetTestPath("base.apk");
+ std::vector<std::string> link_args = {
+ "--manifest",
+ GetDefaultManifest("com.aapt2.app"),
+ "-o",
+ base_apk,
+ };
+ ASSERT_TRUE(Link(link_args, base_files_dir, &diag));
+
+ const std::string localeconfig_manifest = GetTestPath("localeconfig_manifest.xml");
+ const std::string out_apk = GetTestPath("out.apk");
+ WriteFile(localeconfig_manifest, android::base::StringPrintf(R"(
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.aapt2.app">
+
+ <application
+ android:localeConfig="@xml/locales_config">
+ </application>
+ </manifest>)"));
+ link_args = LinkCommandBuilder(this)
+ .SetManifestFile(localeconfig_manifest)
+ .AddParameter("-I", base_apk)
+ .Build(out_apk);
+ ASSERT_TRUE(Link(link_args, &diag));
+}
+
TEST_F(LinkTest, LocaleConfigWrongTag) {
StdErrDiagnostics diag;
const std::string compiled_files_dir = GetTestPath("compiled");
diff --git a/tools/aapt2/format/proto/ProtoDeserialize.cpp b/tools/aapt2/format/proto/ProtoDeserialize.cpp
index 6a1e8c1..e39f327 100644
--- a/tools/aapt2/format/proto/ProtoDeserialize.cpp
+++ b/tools/aapt2/format/proto/ProtoDeserialize.cpp
@@ -562,6 +562,11 @@
}
}
+ for (const pb::DynamicRefTable& dynamic_ref : pb_table.dynamic_ref_table()) {
+ out_table->included_packages_.insert(
+ {dynamic_ref.package_id().id(), dynamic_ref.package_name()});
+ }
+
// Deserialize the overlayable groups of the table
std::vector<std::shared_ptr<Overlayable>> overlayables;
for (const pb::Overlayable& pb_overlayable : pb_table.overlayable()) {
diff --git a/tools/aapt2/format/proto/ProtoSerialize.cpp b/tools/aapt2/format/proto/ProtoSerialize.cpp
index 163a60a..a6d58fd 100644
--- a/tools/aapt2/format/proto/ProtoSerialize.cpp
+++ b/tools/aapt2/format/proto/ProtoSerialize.cpp
@@ -345,7 +345,11 @@
pb::ToolFingerprint* pb_fingerprint = out_table->add_tool_fingerprint();
pb_fingerprint->set_tool(util::GetToolName());
pb_fingerprint->set_version(util::GetToolFingerprint());
-
+ for (auto it = table.included_packages_.begin(); it != table.included_packages_.end(); ++it) {
+ pb::DynamicRefTable* pb_dynamic_ref = out_table->add_dynamic_ref_table();
+ pb_dynamic_ref->mutable_package_id()->set_id(it->first);
+ pb_dynamic_ref->set_package_name(it->second);
+ }
std::vector<Overlayable*> overlayables;
auto table_view = table.GetPartitionedView();
for (const auto& package : table_view.packages) {
diff --git a/tools/aapt2/format/proto/ProtoSerialize_test.cpp b/tools/aapt2/format/proto/ProtoSerialize_test.cpp
index 692fa42..5adc5e6 100644
--- a/tools/aapt2/format/proto/ProtoSerialize_test.cpp
+++ b/tools/aapt2/format/proto/ProtoSerialize_test.cpp
@@ -1024,4 +1024,28 @@
EXPECT_THAT(*(custom_layout->path), Eq("res/layout/bar.xml"));
}
+TEST(ProtoSerializeTest, SerializeDynamicRef) {
+ std::unique_ptr<IAaptContext> context =
+ test::ContextBuilder().SetCompilationPackage("app").SetPackageId(0x7f).Build();
+ std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder().Build();
+ table->included_packages_.insert({20, "foobar"});
+ table->included_packages_.insert({30, "barfoo"});
+
+ ResourceTable new_table;
+ pb::ResourceTable pb_table;
+ MockFileCollection files;
+ std::string error;
+ SerializeTableToPb(*table, &pb_table, context->GetDiagnostics());
+ ASSERT_TRUE(DeserializeTableFromPb(pb_table, &files, &new_table, &error));
+ EXPECT_THAT(error, IsEmpty());
+
+ int result = new_table.included_packages_.size();
+ EXPECT_THAT(result, Eq(2));
+ auto it = new_table.included_packages_.begin();
+ EXPECT_THAT(it->first, Eq(20));
+ EXPECT_THAT(it->second, Eq("foobar"));
+ it++;
+ EXPECT_THAT(it->first, Eq(30));
+ EXPECT_THAT(it->second, Eq("barfoo"));
+}
} // namespace aapt