diff options
754 files changed, 21535 insertions, 10100 deletions
diff --git a/.clang-format b/.clang-format index 03af56d64062..d60d33c08f3f 100644 --- a/.clang-format +++ b/.clang-format @@ -9,5 +9,17 @@ CommentPragmas: NOLINT:.* ConstructorInitializerIndentWidth: 6 ContinuationIndentWidth: 8 IndentWidth: 4 +JavaImportGroups: +- android +- androidx +- com.android +- dalvik +- libcore +- com +- junit +- net +- org +- java +- javax PenaltyBreakBeforeFirstCallParameter: 100000 SpacesBeforeTrailingComments: 1 diff --git a/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl b/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl index d281da037fde..a3390b75b8bf 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 @@ import android.app.job.JobWorkItem; */ 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 @@ interface IJobCallback { */ @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 22ad252b9639..2bb82bd006de 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 @@ oneway interface IJobService { /** 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 dfdb29091ad9..74486861956e 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.Nullable; 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 @@ import java.util.List; */ @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 d184d44239ed..dabf7282a9e0 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 abstract class JobService extends Service { 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 @@ public abstract class JobService extends Service { * 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 3d43d20e7955..6c4b6863ae9e 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.Message; import android.os.RemoteException; import android.util.Log; +import com.android.internal.os.SomeArgs; + import java.lang.ref.WeakReference; /** @@ -51,6 +59,20 @@ public abstract class JobServiceEngine { * 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 @@ public abstract class JobServiceEngine { } @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 @@ public abstract class JobServiceEngine { @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 @@ public abstract class JobServiceEngine { 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 @@ public abstract class JobServiceEngine { 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 @@ public abstract class JobServiceEngine { 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 @@ public abstract class JobServiceEngine { 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 358f009e8797..e0d1a30e7ced 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -569,7 +569,7 @@ public class JobSchedulerService extends com.android.server.SystemService public static final long DEFAULT_RUNTIME_MIN_EJ_GUARANTEE_MS = 3 * MINUTE_IN_MILLIS; @VisibleForTesting static final long DEFAULT_RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS = 5 * MINUTE_IN_MILLIS; - static final boolean DEFAULT_PERSIST_IN_SPLIT_FILES = false; + static final boolean DEFAULT_PERSIST_IN_SPLIT_FILES = true; private static final boolean DEFAULT_USE_TARE_POLICY = false; /** @@ -1310,7 +1310,16 @@ public class JobSchedulerService extends com.android.server.SystemService 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 @@ public class JobSchedulerService extends com.android.server.SystemService 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 7c61a354016f..9aa6b1c298ef 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 android.app.job.JobInfo.getPriorityString; 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 final class JobServiceContext implements ServiceConnection { 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 final class JobServiceContext implements ServiceConnection { 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 @@ public final class JobServiceContext implements ServiceConnection { 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 @@ public final class JobServiceContext implements ServiceConnection { } } + 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 @@ public final class JobServiceContext implements ServiceConnection { } } + private void doUpdateTransferredNetworkBytes(JobCallback jobCallback, int jobId, + @Nullable JobWorkItem item, long downloadBytes, long uploadBytes) { + // TODO(255393346): Make sure apps call this appropriately and monitor for abuse + } + + private void doUpdateEstimatedNetworkBytes(JobCallback jobCallback, int jobId, + @Nullable JobWorkItem item, long downloadBytes, long uploadBytes) { + // TODO(255393346): Make sure apps call this appropriately and monitor for abuse + } + /** * We acquire/release a wakelock on onServiceConnected/unbindService. This mirrors the work * we intend to send to the client - we stop sending work when the service is unbound so until @@ -1036,7 +1090,16 @@ public final class JobServiceContext implements ServiceConnection { 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 2f94705f01c2..c2602f246ce4 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.GuardedBy; 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 @@ public final class JobStore { * 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 @@ public final class JobStore { 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 @@ public final class JobStore { 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 @@ public final class JobStore { * 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 @@ public final class JobStore { * 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 @@ public final class JobStore { 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 @@ public final class JobStore { } } 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 92716f475b88..f6410ffbd3dd 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 @@ public final class JobStatus { 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 @@ public final class JobStatus { } } - 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/cmds/idmap2/Android.bp b/cmds/idmap2/Android.bp index 4f8faca59e4c..7a08cbdcddd4 100644 --- a/cmds/idmap2/Android.bp +++ b/cmds/idmap2/Android.bp @@ -222,6 +222,7 @@ cc_test { }, data: [ "tests/data/**/*.apk", + "tests/data/**/*.png", ], compile_multilib: "first", test_options: { diff --git a/cmds/idmap2/idmap2d/Idmap2Service.cpp b/cmds/idmap2/idmap2d/Idmap2Service.cpp index 44311648da80..10947dc90a76 100644 --- a/cmds/idmap2/idmap2d/Idmap2Service.cpp +++ b/cmds/idmap2/idmap2d/Idmap2Service.cpp @@ -39,6 +39,7 @@ #include "idmap2/PrettyPrintVisitor.h" #include "idmap2/Result.h" #include "idmap2/SysTrace.h" +#include <fcntl.h> using android::base::StringPrintf; using android::binder::Status; @@ -238,6 +239,9 @@ Status Idmap2Service::createFabricatedOverlay( if (res.dataType == Res_value::TYPE_STRING) { builder.SetResourceValue(res.resourceName, res.dataType, res.stringData.value(), res.configuration.value_or(std::string())); + } else if (res.binaryData.has_value()) { + builder.SetResourceValue(res.resourceName, res.binaryData->get(), + res.configuration.value_or(std::string())); } else { builder.SetResourceValue(res.resourceName, res.dataType, res.data, res.configuration.value_or(std::string())); @@ -264,6 +268,7 @@ Status Idmap2Service::createFabricatedOverlay( file_name.c_str(), kMaxFileNameLength)); } } while (std::filesystem::exists(path)); + builder.setFrroPath(path); const uid_t uid = IPCThreadState::self()->getCallingUid(); if (!UidHasWriteAccessToPath(uid, path)) { diff --git a/cmds/idmap2/idmap2d/aidl/core/android/os/FabricatedOverlayInternalEntry.aidl b/cmds/idmap2/idmap2d/aidl/core/android/os/FabricatedOverlayInternalEntry.aidl index c773e112997d..3ad6d58e8253 100644 --- a/cmds/idmap2/idmap2d/aidl/core/android/os/FabricatedOverlayInternalEntry.aidl +++ b/cmds/idmap2/idmap2d/aidl/core/android/os/FabricatedOverlayInternalEntry.aidl @@ -24,5 +24,6 @@ parcelable FabricatedOverlayInternalEntry { int dataType; int data; @nullable @utf8InCpp String stringData; + @nullable ParcelFileDescriptor binaryData; @nullable @utf8InCpp String configuration; }
\ No newline at end of file diff --git a/cmds/idmap2/include/idmap2/FabricatedOverlay.h b/cmds/idmap2/include/idmap2/FabricatedOverlay.h index 05b0618131c9..9f57710edb0b 100644 --- a/cmds/idmap2/include/idmap2/FabricatedOverlay.h +++ b/cmds/idmap2/include/idmap2/FabricatedOverlay.h @@ -28,6 +28,7 @@ #include "idmap2/ResourceContainer.h" #include "idmap2/Result.h" +#include <binder/ParcelFileDescriptor.h> namespace android::idmap2 { @@ -45,6 +46,15 @@ struct FabricatedOverlay { const std::string& data_string_value, const std::string& configuration); + Builder& SetResourceValue(const std::string& resource_name, + std::optional<android::base::borrowed_fd>&& binary_value, + const std::string& configuration); + + inline Builder& setFrroPath(std::string frro_path) { + frro_path_ = std::move(frro_path); + return *this; + } + WARN_UNUSED Result<FabricatedOverlay> Build(); private: @@ -53,6 +63,7 @@ struct FabricatedOverlay { DataType data_type; DataValue data_value; std::string data_string_value; + std::optional<android::base::borrowed_fd> data_binary_value; std::string configuration; }; @@ -60,6 +71,7 @@ struct FabricatedOverlay { std::string name_; std::string target_package_name_; std::string target_overlayable_; + std::string frro_path_; std::vector<Entry> entries_; }; @@ -79,10 +91,14 @@ struct FabricatedOverlay { explicit FabricatedOverlay(pb::FabricatedOverlay&& overlay, std::string&& string_pool_data_, + std::vector<android::base::borrowed_fd> binary_files_, + off_t total_binary_bytes_, std::optional<uint32_t> crc_from_disk = {}); pb::FabricatedOverlay overlay_pb_; std::string string_pool_data_; + std::vector<android::base::borrowed_fd> binary_files_; + uint32_t total_binary_bytes_; std::optional<uint32_t> crc_from_disk_; mutable std::optional<SerializedData> data_; diff --git a/cmds/idmap2/include/idmap2/ResourceUtils.h b/cmds/idmap2/include/idmap2/ResourceUtils.h index af4dd8960cc3..2214a83bd2da 100644 --- a/cmds/idmap2/include/idmap2/ResourceUtils.h +++ b/cmds/idmap2/include/idmap2/ResourceUtils.h @@ -19,6 +19,7 @@ #include <optional> #include <string> +#include <android-base/unique_fd.h> #include "androidfw/AssetManager2.h" #include "idmap2/Result.h" @@ -41,6 +42,7 @@ struct TargetValue { DataType data_type; DataValue data_value; std::string data_string_value; + std::optional<android::base::borrowed_fd> data_binary_value; }; struct TargetValueWithConfig { diff --git a/cmds/idmap2/libidmap2/FabricatedOverlay.cpp b/cmds/idmap2/libidmap2/FabricatedOverlay.cpp index bde9b0be4361..d517e29f3369 100644 --- a/cmds/idmap2/libidmap2/FabricatedOverlay.cpp +++ b/cmds/idmap2/libidmap2/FabricatedOverlay.cpp @@ -16,6 +16,10 @@ #include "idmap2/FabricatedOverlay.h" +#include <sys/stat.h> // umask +#include <sys/types.h> // umask + +#include <android-base/file.h> #include <androidfw/ResourceUtils.h> #include <androidfw/StringPool.h> #include <google/protobuf/io/coded_stream.h> @@ -51,9 +55,13 @@ void Write32(std::ostream& stream, uint32_t value) { FabricatedOverlay::FabricatedOverlay(pb::FabricatedOverlay&& overlay, std::string&& string_pool_data, + std::vector<android::base::borrowed_fd> binary_files, + off_t total_binary_bytes, std::optional<uint32_t> crc_from_disk) : overlay_pb_(std::forward<pb::FabricatedOverlay>(overlay)), string_pool_data_(std::move(string_pool_data)), + binary_files_(std::move(binary_files)), + total_binary_bytes_(total_binary_bytes), crc_from_disk_(crc_from_disk) { } @@ -72,14 +80,23 @@ FabricatedOverlay::Builder& FabricatedOverlay::Builder::SetOverlayable(const std FabricatedOverlay::Builder& FabricatedOverlay::Builder::SetResourceValue( const std::string& resource_name, uint8_t data_type, uint32_t data_value, const std::string& configuration) { - entries_.emplace_back(Entry{resource_name, data_type, data_value, "", configuration}); + entries_.emplace_back( + Entry{resource_name, data_type, data_value, "", std::nullopt, configuration}); return *this; } FabricatedOverlay::Builder& FabricatedOverlay::Builder::SetResourceValue( const std::string& resource_name, uint8_t data_type, const std::string& data_string_value, const std::string& configuration) { - entries_.emplace_back(Entry{resource_name, data_type, 0, data_string_value, configuration}); + entries_.emplace_back( + Entry{resource_name, data_type, 0, data_string_value, std::nullopt, configuration}); + return *this; +} + +FabricatedOverlay::Builder& FabricatedOverlay::Builder::SetResourceValue( + const std::string& resource_name, std::optional<android::base::borrowed_fd>&& binary_value, + const std::string& configuration) { + entries_.emplace_back(Entry{resource_name, 0, 0, "", binary_value, configuration}); return *this; } @@ -135,7 +152,7 @@ Result<FabricatedOverlay> FabricatedOverlay::Builder::Build() { } value->second = TargetValue{res_entry.data_type, res_entry.data_value, - res_entry.data_string_value}; + res_entry.data_string_value, res_entry.data_binary_value}; } pb::FabricatedOverlay overlay_pb; @@ -144,6 +161,11 @@ Result<FabricatedOverlay> FabricatedOverlay::Builder::Build() { overlay_pb.set_target_package_name(target_package_name_); overlay_pb.set_target_overlayable(target_overlayable_); + std::vector<android::base::borrowed_fd> binary_files; + size_t total_binary_bytes = 0; + // 16 for the number of bytes in the frro file before the binary data + const size_t FRRO_HEADER_SIZE = 16; + for (auto& package : package_map) { auto package_pb = overlay_pb.add_packages(); package_pb->set_name(package.first); @@ -162,6 +184,20 @@ Result<FabricatedOverlay> FabricatedOverlay::Builder::Build() { if (value.second.data_type == Res_value::TYPE_STRING) { auto ref = string_pool.MakeRef(value.second.data_string_value); pb_value->set_data_value(ref.index()); + } else if (value.second.data_binary_value.has_value()) { + pb_value->set_data_type(Res_value::TYPE_STRING); + struct stat s; + if (fstat(value.second.data_binary_value->get(), &s) == -1) { + return Error("unable to get size of binary file: %d", errno); + } + std::string uri + = StringPrintf("frro:/%s?offset=%d&size=%d", frro_path_.c_str(), + static_cast<int> (FRRO_HEADER_SIZE + total_binary_bytes), + static_cast<int> (s.st_size)); + total_binary_bytes += s.st_size; + binary_files.emplace_back(value.second.data_binary_value->get()); + auto ref = string_pool.MakeRef(std::move(uri)); + pb_value->set_data_value(ref.index()); } else { pb_value->set_data_value(value.second.data_value); } @@ -169,10 +205,10 @@ Result<FabricatedOverlay> FabricatedOverlay::Builder::Build() { } } } - android::BigBuffer string_buffer(kBufferSize); android::StringPool::FlattenUtf8(&string_buffer, string_pool, nullptr); - return FabricatedOverlay(std::move(overlay_pb), string_buffer.to_string()); + return FabricatedOverlay(std::move(overlay_pb), string_buffer.to_string(), + std::move(binary_files), total_binary_bytes); } Result<FabricatedOverlay> FabricatedOverlay::FromBinaryStream(std::istream& stream) { @@ -190,7 +226,7 @@ Result<FabricatedOverlay> FabricatedOverlay::FromBinaryStream(std::istream& stre return Error("Failed to read fabricated overlay version."); } - if (version != 1 && version != 2) { + if (version < 1 || version > 3) { return Error("Invalid fabricated overlay version '%u'.", version); } @@ -201,7 +237,14 @@ Result<FabricatedOverlay> FabricatedOverlay::FromBinaryStream(std::istream& stre pb::FabricatedOverlay overlay{}; std::string sp_data; - if (version == 2) { + uint32_t total_binary_bytes; + if (version == 3) { + if (!Read32(stream, &total_binary_bytes)) { + return Error("Failed read total binary bytes."); + } + stream.seekg(total_binary_bytes, std::istream::cur); + } + if (version >= 2) { uint32_t sp_size; if (!Read32(stream, &sp_size)) { return Error("Failed read string pool size."); @@ -211,20 +254,15 @@ Result<FabricatedOverlay> FabricatedOverlay::FromBinaryStream(std::istream& stre return Error("Failed to read string pool."); } sp_data = buf; - - if (!overlay.ParseFromIstream(&stream)) { - return Error("Failed read fabricated overlay proto."); - } - } else { - if (!overlay.ParseFromIstream(&stream)) { - return Error("Failed read fabricated overlay proto."); - } + } + if (!overlay.ParseFromIstream(&stream)) { + return Error("Failed read fabricated overlay proto."); } // If the proto version is the latest version, then the contents of the proto must be the same // when the proto is re-serialized; otherwise, the crc must be calculated because migrating the // proto to the latest version will likely change the contents of the fabricated overlay. - return FabricatedOverlay(std::move(overlay), std::move(sp_data), + return FabricatedOverlay(std::move(overlay), std::move(sp_data), {}, total_binary_bytes, version == kFabricatedOverlayCurrentVersion ? std::optional<uint32_t>(crc) : std::nullopt); @@ -274,6 +312,14 @@ Result<Unit> FabricatedOverlay::ToBinaryStream(std::ostream& stream) const { Write32(stream, kFabricatedOverlayMagic); Write32(stream, kFabricatedOverlayCurrentVersion); Write32(stream, (*data)->pb_crc); + Write32(stream, total_binary_bytes_); + std::string file_contents; + for (const android::base::borrowed_fd fd : binary_files_) { + if (!ReadFdToString(fd, &file_contents)) { + return Error("Failed to read binary file data."); + } + stream.write(file_contents.data(), file_contents.length()); + } Write32(stream, (*data)->sp_data.length()); stream.write((*data)->sp_data.data(), (*data)->sp_data.length()); if (stream.bad()) { diff --git a/cmds/idmap2/tests/FabricatedOverlayTests.cpp b/cmds/idmap2/tests/FabricatedOverlayTests.cpp index e804c879ee82..e13a0eb5d488 100644 --- a/cmds/idmap2/tests/FabricatedOverlayTests.cpp +++ b/cmds/idmap2/tests/FabricatedOverlayTests.cpp @@ -17,6 +17,7 @@ #include <android-base/file.h> #include <gtest/gtest.h> #include <idmap2/FabricatedOverlay.h> +#include "TestHelpers.h" #include <fstream> #include <utility> @@ -41,6 +42,10 @@ TEST(FabricatedOverlayTests, OverlayInfo) { } TEST(FabricatedOverlayTests, SetResourceValue) { + auto path = GetTestDataPath() + "/overlay/res/drawable/android.png"; + auto fd = android::base::unique_fd(::open(path.c_str(), O_RDONLY | O_CLOEXEC)); + ASSERT_TRUE(fd > 0) << "errno " << errno << " for path " << path; + auto overlay = FabricatedOverlay::Builder("com.example.overlay", "SandTheme", "com.example.target") .SetResourceValue( @@ -54,6 +59,8 @@ TEST(FabricatedOverlayTests, SetResourceValue) { Res_value::TYPE_STRING, "foobar", "en-rUS-normal-xxhdpi-v21") + .SetResourceValue("com.example.target:drawable/dr1", fd, "port-xxhdpi-v7") + .setFrroPath("/foo/bar/biz.frro") .Build(); ASSERT_TRUE(overlay); auto container = FabricatedOverlayContainer::FromOverlay(std::move(*overlay)); @@ -67,19 +74,28 @@ TEST(FabricatedOverlayTests, SetResourceValue) { auto pairs = container->GetOverlayData(*info); ASSERT_TRUE(pairs); - ASSERT_EQ(4U, pairs->pairs.size()); + ASSERT_EQ(5U, pairs->pairs.size()); auto string_pool = ResStringPool(pairs->string_pool_data->data.get(), pairs->string_pool_data->data_length, false); auto& it = pairs->pairs[0]; - ASSERT_EQ("com.example.target:integer/int1", it.resource_name); + ASSERT_EQ("com.example.target:drawable/dr1", it.resource_name); auto entry = std::get_if<TargetValueWithConfig>(&it.value); ASSERT_NE(nullptr, entry); + ASSERT_EQ(std::string("frro://foo/bar/biz.frro?offset=16&size=8341"), + string_pool.string8At(entry->value.data_value).value_or("")); + ASSERT_EQ(Res_value::TYPE_STRING, entry->value.data_type); + ASSERT_EQ("port-xxhdpi-v7", entry->config); + + it = pairs->pairs[1]; + ASSERT_EQ("com.example.target:integer/int1", it.resource_name); + entry = std::get_if<TargetValueWithConfig>(&it.value); + ASSERT_NE(nullptr, entry); ASSERT_EQ(1U, entry->value.data_value); ASSERT_EQ(Res_value::TYPE_INT_DEC, entry->value.data_type); ASSERT_EQ("port", entry->config); - it = pairs->pairs[1]; + it = pairs->pairs[2]; ASSERT_EQ("com.example.target:string/int3", it.resource_name); entry = std::get_if<TargetValueWithConfig>(&it.value); ASSERT_NE(nullptr, entry); @@ -87,7 +103,7 @@ TEST(FabricatedOverlayTests, SetResourceValue) { ASSERT_EQ(Res_value::TYPE_REFERENCE, entry->value.data_type); ASSERT_EQ("xxhdpi-v7", entry->config); - it = pairs->pairs[2]; + it = pairs->pairs[3]; ASSERT_EQ("com.example.target:string/string1", it.resource_name); entry = std::get_if<TargetValueWithConfig>(&it.value); ASSERT_NE(nullptr, entry); @@ -95,7 +111,7 @@ TEST(FabricatedOverlayTests, SetResourceValue) { ASSERT_EQ(std::string("foobar"), string_pool.string8At(entry->value.data_value).value_or("")); ASSERT_EQ("en-rUS-normal-xxhdpi-v21", entry->config); - it = pairs->pairs[3]; + it = pairs->pairs[4]; ASSERT_EQ("com.example.target.split:integer/int2", it.resource_name); entry = std::get_if<TargetValueWithConfig>(&it.value); ASSERT_NE(nullptr, entry); diff --git a/cmds/idmap2/tests/IdmapTests.cpp b/cmds/idmap2/tests/IdmapTests.cpp index 7b7dc17deea4..b473f26b2230 100644 --- a/cmds/idmap2/tests/IdmapTests.cpp +++ b/cmds/idmap2/tests/IdmapTests.cpp @@ -260,11 +260,17 @@ TEST(IdmapTests, FabricatedOverlay) { auto target = TargetResourceContainer::FromPath(target_apk_path); ASSERT_TRUE(target); + auto path = GetTestDataPath() + "/overlay/res/drawable/android.png"; + auto fd = android::base::unique_fd(::open(path.c_str(), O_RDONLY | O_CLOEXEC)); + ASSERT_TRUE(fd > 0) << "errno " << errno << " for path " << path; + auto frro = FabricatedOverlay::Builder("com.example.overlay", "SandTheme", "test.target") .SetOverlayable("TestResources") .SetResourceValue("integer/int1", Res_value::TYPE_INT_DEC, 2U, "land-xxhdpi-v7") .SetResourceValue("string/str1", Res_value::TYPE_REFERENCE, 0x7f010000, "land") .SetResourceValue("string/str2", Res_value::TYPE_STRING, "foobar", "xxhdpi-v7") + .SetResourceValue("drawable/dr1", fd, "port-xxhdpi-v7") + .setFrroPath("/foo/bar/biz.frro") .Build(); ASSERT_TRUE(frro); @@ -293,14 +299,19 @@ TEST(IdmapTests, FabricatedOverlay) { auto string_pool_data = data->GetStringPoolData(); auto string_pool = ResStringPool(string_pool_data.data(), string_pool_data.size(), false); + std::u16string expected_uri = u"frro://foo/bar/biz.frro?offset=16&size=8341"; + uint32_t uri_index + = string_pool.indexOfString(expected_uri.data(), expected_uri.length()).value_or(-1); const auto& target_inline_entries = data->GetTargetInlineEntries(); - ASSERT_EQ(target_inline_entries.size(), 3U); - ASSERT_TARGET_INLINE_ENTRY(target_inline_entries[0], R::target::integer::int1, "land-xxhdpi-v7", + ASSERT_EQ(target_inline_entries.size(), 4U); + ASSERT_TARGET_INLINE_ENTRY(target_inline_entries[0], R::target::drawable::dr1, "port-xxhdpi-v7", + Res_value::TYPE_STRING, uri_index); + ASSERT_TARGET_INLINE_ENTRY(target_inline_entries[1], R::target::integer::int1, "land-xxhdpi-v7", Res_value::TYPE_INT_DEC, 2U); - ASSERT_TARGET_INLINE_ENTRY(target_inline_entries[1], R::target::string::str1, "land", + ASSERT_TARGET_INLINE_ENTRY(target_inline_entries[2], R::target::string::str1, "land", Res_value::TYPE_REFERENCE, 0x7f010000); - ASSERT_TARGET_INLINE_ENTRY(target_inline_entries[2], R::target::string::str2, "xxhdpi-v7", + ASSERT_TARGET_INLINE_ENTRY(target_inline_entries[3], R::target::string::str2, "xxhdpi-v7", Res_value::TYPE_STRING, (uint32_t) (string_pool.indexOfString(u"foobar", 6)).value_or(-1)); } diff --git a/cmds/idmap2/tests/R.h b/cmds/idmap2/tests/R.h index ad998b9a7a4c..80c062d92640 100644 --- a/cmds/idmap2/tests/R.h +++ b/cmds/idmap2/tests/R.h @@ -26,24 +26,27 @@ namespace android::idmap2 { // clang-format off namespace R::target { namespace integer { // NOLINT(runtime/indentation_namespace) - constexpr ResourceId int1 = 0x7f010000; + constexpr ResourceId int1 = 0x7f020000; + } + namespace drawable { + constexpr ResourceId dr1 = 0x7f010000; } namespace string { // NOLINT(runtime/indentation_namespace) - constexpr ResourceId not_overlayable = 0x7f020003; - constexpr ResourceId other = 0x7f020004; - constexpr ResourceId policy_actor = 0x7f020005; - constexpr ResourceId policy_config_signature = 0x7f020006; - constexpr ResourceId policy_odm = 0x7f020007; - constexpr ResourceId policy_oem = 0x7f020008; - constexpr ResourceId policy_product = 0x7f020009; - constexpr ResourceId policy_public = 0x7f02000a; - constexpr ResourceId policy_signature = 0x7f02000b; - constexpr ResourceId policy_system = 0x7f02000c; - constexpr ResourceId policy_system_vendor = 0x7f02000d; - constexpr ResourceId str1 = 0x7f02000e; - constexpr ResourceId str2 = 0x7f02000f; - constexpr ResourceId str3 = 0x7f020010; - constexpr ResourceId str4 = 0x7f020011; + constexpr ResourceId not_overlayable = 0x7f030003; + constexpr ResourceId other = 0x7f030004; + constexpr ResourceId policy_actor = 0x7f030005; + constexpr ResourceId policy_config_signature = 0x7f030006; + constexpr ResourceId policy_odm = 0x7f030007; + constexpr ResourceId policy_oem = 0x7f030008; + constexpr ResourceId policy_product = 0x7f030009; + constexpr ResourceId policy_public = 0x7f03000a; + constexpr ResourceId policy_signature = 0x7f03000b; + constexpr ResourceId policy_system = 0x7f03000c; + constexpr ResourceId policy_system_vendor = 0x7f03000d; + constexpr ResourceId str1 = 0x7f03000e; + constexpr ResourceId str2 = 0x7f03000f; + constexpr ResourceId str3 = 0x7f030010; + constexpr ResourceId str4 = 0x7f030011; } // namespace string } // namespace R::target diff --git a/cmds/idmap2/tests/RawPrintVisitorTests.cpp b/cmds/idmap2/tests/RawPrintVisitorTests.cpp index 7112eeb9ea0c..68164e26f352 100644 --- a/cmds/idmap2/tests/RawPrintVisitorTests.cpp +++ b/cmds/idmap2/tests/RawPrintVisitorTests.cpp @@ -79,22 +79,22 @@ TEST(RawPrintVisitorTests, CreateRawPrintVisitor) { ASSERT_CONTAINS_REGEX(ADDRESS "00000000 config count", stream.str()); ASSERT_CONTAINS_REGEX(ADDRESS "00000004 overlay entry count", stream.str()); ASSERT_CONTAINS_REGEX(ADDRESS "0000000a string pool index offset", stream.str()); - ASSERT_CONTAINS_REGEX(ADDRESS "7f010000 target id: integer/int1", stream.str()); + ASSERT_CONTAINS_REGEX(ADDRESS "7f020000 target id: integer/int1", stream.str()); ASSERT_CONTAINS_REGEX(ADDRESS "7f010000 overlay id: integer/int1", stream.str()); - ASSERT_CONTAINS_REGEX(ADDRESS "7f02000e target id: string/str1", stream.str()); + ASSERT_CONTAINS_REGEX(ADDRESS "7f03000e target id: string/str1", stream.str()); ASSERT_CONTAINS_REGEX(ADDRESS "7f02000b overlay id: string/str1", stream.str()); - ASSERT_CONTAINS_REGEX(ADDRESS "7f020010 target id: string/str3", stream.str()); + ASSERT_CONTAINS_REGEX(ADDRESS "7f030010 target id: string/str3", stream.str()); ASSERT_CONTAINS_REGEX(ADDRESS "7f02000c overlay id: string/str3", stream.str()); - ASSERT_CONTAINS_REGEX(ADDRESS "7f020011 target id: string/str4", stream.str()); + ASSERT_CONTAINS_REGEX(ADDRESS "7f030011 target id: string/str4", stream.str()); ASSERT_CONTAINS_REGEX(ADDRESS "7f02000d overlay id: string/str4", stream.str()); ASSERT_CONTAINS_REGEX(ADDRESS "7f010000 overlay id: integer/int1", stream.str()); - ASSERT_CONTAINS_REGEX(ADDRESS "7f010000 target id: integer/int1", stream.str()); + ASSERT_CONTAINS_REGEX(ADDRESS "7f020000 target id: integer/int1", stream.str()); ASSERT_CONTAINS_REGEX(ADDRESS "7f02000b overlay id: string/str1", stream.str()); - ASSERT_CONTAINS_REGEX(ADDRESS "7f02000e target id: string/str1", stream.str()); + ASSERT_CONTAINS_REGEX(ADDRESS "7f03000e target id: string/str1", stream.str()); ASSERT_CONTAINS_REGEX(ADDRESS "7f02000c overlay id: string/str3", stream.str()); - ASSERT_CONTAINS_REGEX(ADDRESS "7f020010 target id: string/str3", stream.str()); + ASSERT_CONTAINS_REGEX(ADDRESS "7f030010 target id: string/str3", stream.str()); ASSERT_CONTAINS_REGEX(ADDRESS "7f02000d overlay id: string/str4", stream.str()); - ASSERT_CONTAINS_REGEX(ADDRESS "7f020011 target id: string/str4", stream.str()); + ASSERT_CONTAINS_REGEX(ADDRESS "7f030011 target id: string/str4", stream.str()); ASSERT_CONTAINS_REGEX(ADDRESS "000000b4 string pool size", stream.str()); ASSERT_CONTAINS_REGEX(ADDRESS "........ string pool", stream.str()); } diff --git a/cmds/idmap2/tests/ResourceMappingTests.cpp b/cmds/idmap2/tests/ResourceMappingTests.cpp index 016d427e7452..380e462a3aba 100644 --- a/cmds/idmap2/tests/ResourceMappingTests.cpp +++ b/cmds/idmap2/tests/ResourceMappingTests.cpp @@ -23,6 +23,7 @@ #include <memory> #include <string> +#include <fcntl.h> #include "R.h" #include "TestConstants.h" #include "TestHelpers.h" @@ -76,7 +77,12 @@ Result<Unit> MappingExists(const ResourceMapping& mapping, ResourceId target_res auto target_map = mapping.GetTargetToOverlayMap(); auto entry_map = target_map.find(target_resource); if (entry_map == target_map.end()) { - return Error("Failed to find mapping for target resource"); + std::string keys; + for (const auto &pair : target_map) { + keys.append(fmt::format("0x{:x}", pair.first)).append(" "); + } + return Error(R"(Failed to find mapping for target resource "0x%02x": "%s")", + target_resource, keys.c_str()); } auto actual_overlay_resource = std::get_if<ResourceId>(&entry_map->second); @@ -108,7 +114,12 @@ Result<Unit> MappingExists(const ResourceMapping& mapping, const ResourceId& tar auto target_map = mapping.GetTargetToOverlayMap(); auto entry_map = target_map.find(target_resource); if (entry_map == target_map.end()) { - return Error("Failed to find mapping for target resource"); + std::string keys; + for (const auto &pair : target_map) { + keys.append(fmt::format("{:x}", pair.first)).append(" "); + } + return Error(R"(Failed to find mapping for target resource "0x%02x": "%s")", + target_resource, keys.c_str()); } auto config_map = std::get_if<ConfigMap>(&entry_map->second); @@ -193,11 +204,16 @@ TEST(ResourceMappingTests, InlineResources) { } TEST(ResourceMappingTests, FabricatedOverlay) { + auto path = GetTestDataPath() + "/overlay/res/drawable/android.png"; + auto fd = android::base::unique_fd(::open(path.c_str(), O_RDONLY | O_CLOEXEC)); + ASSERT_TRUE(fd > 0) << "errno " << errno << " for path " << path; auto frro = FabricatedOverlay::Builder("com.example.overlay", "SandTheme", "test.target") .SetOverlayable("TestResources") .SetResourceValue("integer/int1", Res_value::TYPE_INT_DEC, 2U, "") .SetResourceValue("string/str1", Res_value::TYPE_REFERENCE, 0x7f010000, "") .SetResourceValue("string/str2", Res_value::TYPE_STRING, "foobar", "") + .SetResourceValue("drawable/dr1", fd, "") + .setFrroPath("/foo/bar/biz.frro") .Build(); ASSERT_TRUE(frro); @@ -214,11 +230,16 @@ TEST(ResourceMappingTests, FabricatedOverlay) { auto string_pool_data = res.GetStringPoolData(); auto string_pool = ResStringPool(string_pool_data.data(), string_pool_data.size(), false); - ASSERT_EQ(res.GetTargetToOverlayMap().size(), 3U); + std::u16string expected_uri = u"frro://foo/bar/biz.frro?offset=16&size=8341"; + uint32_t uri_index + = string_pool.indexOfString(expected_uri.data(), expected_uri.length()).value_or(-1); + + ASSERT_EQ(res.GetTargetToOverlayMap().size(), 4U); ASSERT_EQ(res.GetOverlayToTargetMap().size(), 0U); ASSERT_RESULT(MappingExists(res, R::target::string::str1, Res_value::TYPE_REFERENCE, 0x7f010000)); ASSERT_RESULT(MappingExists(res, R::target::string::str2, Res_value::TYPE_STRING, (uint32_t) (string_pool.indexOfString(u"foobar", 6)).value_or(-1))); + ASSERT_RESULT(MappingExists(res, R::target::drawable::dr1, Res_value::TYPE_STRING, uri_index)); ASSERT_RESULT(MappingExists(res, R::target::integer::int1, Res_value::TYPE_INT_DEC, 2U)); } diff --git a/cmds/idmap2/tests/TestConstants.h b/cmds/idmap2/tests/TestConstants.h index d5799adf0ec3..794d6221c2c0 100644 --- a/cmds/idmap2/tests/TestConstants.h +++ b/cmds/idmap2/tests/TestConstants.h @@ -19,8 +19,8 @@ namespace android::idmap2::TestConstants { -constexpr const auto TARGET_CRC = 0x7c2d4719; -constexpr const auto TARGET_CRC_STRING = "7c2d4719"; +constexpr const auto TARGET_CRC = 0xa960a69; +constexpr const auto TARGET_CRC_STRING = "0a960a69"; constexpr const auto OVERLAY_CRC = 0xb71095cf; constexpr const auto OVERLAY_CRC_STRING = "b71095cf"; diff --git a/cmds/idmap2/tests/data/overlay/res/drawable/android.png b/cmds/idmap2/tests/data/overlay/res/drawable/android.png Binary files differnew file mode 100644 index 000000000000..b7317b0933fb --- /dev/null +++ b/cmds/idmap2/tests/data/overlay/res/drawable/android.png diff --git a/cmds/idmap2/tests/data/target/build b/cmds/idmap2/tests/data/target/build index e6df742cc9da..cd13a7ec1bd8 100755 --- a/cmds/idmap2/tests/data/target/build +++ b/cmds/idmap2/tests/data/target/build @@ -17,5 +17,7 @@ aapt2 link --manifest AndroidManifest.xml -A assets -o target.apk compiled.flata rm compiled.flata aapt2 compile res/values/values.xml -o . -aapt2 link --manifest AndroidManifest.xml -A assets -o target-no-overlayable.apk values_values.arsc.flat -rm values_values.arsc.flat
\ No newline at end of file +aapt2 compile res/drawable/dr1.png -o . +aapt2 link --manifest AndroidManifest.xml -A assets -o target-no-overlayable.apk values_values.arsc.flat drawable_dr1.png.flat +rm values_values.arsc.flat +rm drawable_dr1.png.flat diff --git a/cmds/idmap2/tests/data/target/res/drawable/dr1.png b/cmds/idmap2/tests/data/target/res/drawable/dr1.png Binary files differnew file mode 100644 index 000000000000..1a56e68fd6d2 --- /dev/null +++ b/cmds/idmap2/tests/data/target/res/drawable/dr1.png diff --git a/cmds/idmap2/tests/data/target/res/values/overlayable.xml b/cmds/idmap2/tests/data/target/res/values/overlayable.xml index 57e6c439c23c..aac9081b9513 100644 --- a/cmds/idmap2/tests/data/target/res/values/overlayable.xml +++ b/cmds/idmap2/tests/data/target/res/values/overlayable.xml @@ -63,6 +63,7 @@ <item type="string" name="y" /> <item type="string" name="z" /> <item type="integer" name="int1" /> + <item type="drawable" name="dr1" /> </policy> </overlayable> diff --git a/cmds/idmap2/tests/data/target/target-no-overlayable.apk b/cmds/idmap2/tests/data/target/target-no-overlayable.apk Binary files differindex cc3491de894d..680eeb609f8e 100644 --- a/cmds/idmap2/tests/data/target/target-no-overlayable.apk +++ b/cmds/idmap2/tests/data/target/target-no-overlayable.apk diff --git a/cmds/idmap2/tests/data/target/target.apk b/cmds/idmap2/tests/data/target/target.apk Binary files differindex 4a58c5e28f49..145e737ca138 100644 --- a/cmds/idmap2/tests/data/target/target.apk +++ b/cmds/idmap2/tests/data/target/target.apk diff --git a/core/api/current.txt b/core/api/current.txt index 3766a0b93de4..ee04a461eff5 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -91,6 +91,18 @@ package android { field public static final String EXPAND_STATUS_BAR = "android.permission.EXPAND_STATUS_BAR"; field public static final String FACTORY_TEST = "android.permission.FACTORY_TEST"; field public static final String FOREGROUND_SERVICE = "android.permission.FOREGROUND_SERVICE"; + field public static final String FOREGROUND_SERVICE_CAMERA = "android.permission.FOREGROUND_SERVICE_CAMERA"; + field public static final String FOREGROUND_SERVICE_CONNECTED_DEVICE = "android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE"; + field public static final String FOREGROUND_SERVICE_DATA_SYNC = "android.permission.FOREGROUND_SERVICE_DATA_SYNC"; + field public static final String FOREGROUND_SERVICE_HEALTH = "android.permission.FOREGROUND_SERVICE_HEALTH"; + field public static final String FOREGROUND_SERVICE_LOCATION = "android.permission.FOREGROUND_SERVICE_LOCATION"; + field public static final String FOREGROUND_SERVICE_MEDIA_PLAYBACK = "android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"; + field public static final String FOREGROUND_SERVICE_MEDIA_PROJECTION = "android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION"; + field public static final String FOREGROUND_SERVICE_MICROPHONE = "android.permission.FOREGROUND_SERVICE_MICROPHONE"; + field public static final String FOREGROUND_SERVICE_PHONE_CALL = "android.permission.FOREGROUND_SERVICE_PHONE_CALL"; + field public static final String FOREGROUND_SERVICE_REMOTE_MESSAGING = "android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING"; + field public static final String FOREGROUND_SERVICE_SPECIAL_USE = "android.permission.FOREGROUND_SERVICE_SPECIAL_USE"; + field public static final String FOREGROUND_SERVICE_SYSTEM_EXEMPTED = "android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED"; field public static final String GET_ACCOUNTS = "android.permission.GET_ACCOUNTS"; field public static final String GET_ACCOUNTS_PRIVILEGED = "android.permission.GET_ACCOUNTS_PRIVILEGED"; field public static final String GET_PACKAGE_SIZE = "android.permission.GET_PACKAGE_SIZE"; @@ -3112,10 +3124,13 @@ package android.accessibilityservice { method public final void setServiceInfo(android.accessibilityservice.AccessibilityServiceInfo); method public void setTouchExplorationPassthroughRegion(int, @NonNull android.graphics.Region); method public void takeScreenshot(int, @NonNull java.util.concurrent.Executor, @NonNull android.accessibilityservice.AccessibilityService.TakeScreenshotCallback); + method public void takeScreenshotOfWindow(int, @NonNull java.util.concurrent.Executor, @NonNull android.accessibilityservice.AccessibilityService.TakeScreenshotCallback); field public static final int ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR = 1; // 0x1 field public static final int ERROR_TAKE_SCREENSHOT_INTERVAL_TIME_SHORT = 3; // 0x3 field public static final int ERROR_TAKE_SCREENSHOT_INVALID_DISPLAY = 4; // 0x4 + field public static final int ERROR_TAKE_SCREENSHOT_INVALID_WINDOW = 5; // 0x5 field public static final int ERROR_TAKE_SCREENSHOT_NO_ACCESSIBILITY_ACCESS = 2; // 0x2 + field public static final int ERROR_TAKE_SCREENSHOT_SECURE_WINDOW = 6; // 0x6 field public static final int GESTURE_2_FINGER_DOUBLE_TAP = 20; // 0x14 field public static final int GESTURE_2_FINGER_DOUBLE_TAP_AND_HOLD = 40; // 0x28 field public static final int GESTURE_2_FINGER_SINGLE_TAP = 19; // 0x13 @@ -5275,6 +5290,13 @@ package android.app { field @NonNull public static final android.os.Parcelable.Creator<android.app.ForegroundServiceStartNotAllowedException> CREATOR; } + public final class ForegroundServiceTypeNotAllowedException extends android.app.ServiceStartNotAllowedException implements android.os.Parcelable { + ctor public ForegroundServiceTypeNotAllowedException(@NonNull String); + method public int describeContents(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.app.ForegroundServiceTypeNotAllowedException> CREATOR; + } + @Deprecated public class Fragment implements android.content.ComponentCallbacks2 android.view.View.OnCreateContextMenuListener { ctor @Deprecated public Fragment(); method @Deprecated public void dump(String, java.io.FileDescriptor, java.io.PrintWriter, String[]); @@ -6936,7 +6958,7 @@ package android.app { method public void onTrimMemory(int); method public boolean onUnbind(android.content.Intent); method public final void startForeground(int, android.app.Notification); - method public final void startForeground(int, @NonNull android.app.Notification, int); + method public final void startForeground(int, @NonNull android.app.Notification, @RequiresPermission int); method @Deprecated public final void stopForeground(boolean); method public final void stopForeground(int); method public final void stopSelf(); @@ -7669,6 +7691,7 @@ package android.app.admin { 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 +8471,31 @@ package android.app.job { 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 { @@ -12171,6 +12206,7 @@ package android.content.pm { field public static final int PERMISSION_DENIED = -1; // 0xffffffff field public static final int PERMISSION_GRANTED = 0; // 0x0 field public static final String PROPERTY_MEDIA_CAPABILITIES = "android.media.PROPERTY_MEDIA_CAPABILITIES"; + field public static final String PROPERTY_SPECIAL_USE_FGS_SUBTYPE = "android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"; field public static final int SIGNATURE_FIRST_NOT_SIGNED = -1; // 0xffffffff field public static final int SIGNATURE_MATCH = 0; // 0x0 field public static final int SIGNATURE_NEITHER_SIGNED = 1; // 0x1 @@ -12384,16 +12420,20 @@ package android.content.pm { field public static final int FLAG_SINGLE_USER = 1073741824; // 0x40000000 field public static final int FLAG_STOP_WITH_TASK = 1; // 0x1 field public static final int FLAG_USE_APP_ZYGOTE = 8; // 0x8 - field public static final int FOREGROUND_SERVICE_TYPE_CAMERA = 64; // 0x40 - field public static final int FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE = 16; // 0x10 - field public static final int FOREGROUND_SERVICE_TYPE_DATA_SYNC = 1; // 0x1 - field public static final int FOREGROUND_SERVICE_TYPE_LOCATION = 8; // 0x8 + field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_CAMERA}, anyOf={android.Manifest.permission.CAMERA}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_CAMERA = 64; // 0x40 + field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE}, anyOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.CHANGE_NETWORK_STATE, android.Manifest.permission.CHANGE_WIFI_STATE, android.Manifest.permission.CHANGE_WIFI_MULTICAST_STATE, android.Manifest.permission.NFC, android.Manifest.permission.TRANSMIT_IR}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE = 16; // 0x10 + field @Deprecated @RequiresPermission(value=android.Manifest.permission.FOREGROUND_SERVICE_DATA_SYNC, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_DATA_SYNC = 1; // 0x1 + field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_HEALTH}, anyOf={android.Manifest.permission.ACTIVITY_RECOGNITION, android.Manifest.permission.BODY_SENSORS, android.Manifest.permission.HIGH_SAMPLING_RATE_SENSORS}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_HEALTH = 256; // 0x100 + field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_LOCATION}, anyOf={android.Manifest.permission.ACCESS_COARSE_LOCATION, android.Manifest.permission.ACCESS_FINE_LOCATION}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_LOCATION = 8; // 0x8 field public static final int FOREGROUND_SERVICE_TYPE_MANIFEST = -1; // 0xffffffff - field public static final int FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK = 2; // 0x2 - field public static final int FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION = 32; // 0x20 - field public static final int FOREGROUND_SERVICE_TYPE_MICROPHONE = 128; // 0x80 - field public static final int FOREGROUND_SERVICE_TYPE_NONE = 0; // 0x0 - field public static final int FOREGROUND_SERVICE_TYPE_PHONE_CALL = 4; // 0x4 + field @RequiresPermission(value=android.Manifest.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK = 2; // 0x2 + field @RequiresPermission(value=android.Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION = 32; // 0x20 + field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_MICROPHONE}, anyOf={android.Manifest.permission.CAPTURE_AUDIO_OUTPUT, android.Manifest.permission.RECORD_AUDIO}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_MICROPHONE = 128; // 0x80 + field @Deprecated public static final int FOREGROUND_SERVICE_TYPE_NONE = 0; // 0x0 + field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_PHONE_CALL}, anyOf={android.Manifest.permission.MANAGE_OWN_CALLS}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_PHONE_CALL = 4; // 0x4 + field @RequiresPermission(value=android.Manifest.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING = 512; // 0x200 + field @RequiresPermission(value=android.Manifest.permission.FOREGROUND_SERVICE_SPECIAL_USE, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_SPECIAL_USE = 1073741824; // 0x40000000 + field @RequiresPermission(value=android.Manifest.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED = 1024; // 0x400 field public int flags; field public String permission; } @@ -16585,6 +16625,7 @@ package android.graphics.fonts { 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 +32210,12 @@ package android.os { 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 +32665,7 @@ package android.os { 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 +35848,7 @@ package android.provider { 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"; @@ -39907,7 +39955,7 @@ package android.service.wallpaper { public abstract class WallpaperService extends android.app.Service { ctor public WallpaperService(); method public final android.os.IBinder onBind(android.content.Intent); - method public abstract android.service.wallpaper.WallpaperService.Engine onCreateEngine(); + method @MainThread public abstract android.service.wallpaper.WallpaperService.Engine onCreateEngine(); field public static final String SERVICE_INTERFACE = "android.service.wallpaper.WallpaperService"; field public static final String SERVICE_META_DATA = "android.service.wallpaper"; } @@ -39922,20 +39970,20 @@ package android.service.wallpaper { method public boolean isPreview(); method public boolean isVisible(); method public void notifyColorsChanged(); - method public void onApplyWindowInsets(android.view.WindowInsets); - method public android.os.Bundle onCommand(String, int, int, int, android.os.Bundle, boolean); - method @Nullable public android.app.WallpaperColors onComputeColors(); - method public void onCreate(android.view.SurfaceHolder); - method public void onDesiredSizeChanged(int, int); - method public void onDestroy(); - method public void onOffsetsChanged(float, float, float, float, int, int); - method public void onSurfaceChanged(android.view.SurfaceHolder, int, int, int); - method public void onSurfaceCreated(android.view.SurfaceHolder); - method public void onSurfaceDestroyed(android.view.SurfaceHolder); - method public void onSurfaceRedrawNeeded(android.view.SurfaceHolder); - method public void onTouchEvent(android.view.MotionEvent); - method public void onVisibilityChanged(boolean); - method public void onZoomChanged(@FloatRange(from=0.0f, to=1.0f) float); + method @MainThread public void onApplyWindowInsets(android.view.WindowInsets); + method @MainThread public android.os.Bundle onCommand(String, int, int, int, android.os.Bundle, boolean); + method @MainThread @Nullable public android.app.WallpaperColors onComputeColors(); + method @MainThread public void onCreate(android.view.SurfaceHolder); + method @MainThread public void onDesiredSizeChanged(int, int); + method @MainThread public void onDestroy(); + method @MainThread public void onOffsetsChanged(float, float, float, float, int, int); + method @MainThread public void onSurfaceChanged(android.view.SurfaceHolder, int, int, int); + method @MainThread public void onSurfaceCreated(android.view.SurfaceHolder); + method @MainThread public void onSurfaceDestroyed(android.view.SurfaceHolder); + method @MainThread public void onSurfaceRedrawNeeded(android.view.SurfaceHolder); + method @MainThread public void onTouchEvent(android.view.MotionEvent); + method @MainThread public void onVisibilityChanged(boolean); + method @MainThread public void onZoomChanged(@FloatRange(from=0.0f, to=1.0f) float); method public void setOffsetNotificationsEnabled(boolean); method public void setTouchEventsEnabled(boolean); } @@ -40370,6 +40418,7 @@ package android.telecom { field public static final String EVENT_DISPLAY_DIAGNOSTIC_MESSAGE = "android.telecom.event.DISPLAY_DIAGNOSTIC_MESSAGE"; field public static final String EXTRA_DIAGNOSTIC_MESSAGE = "android.telecom.extra.DIAGNOSTIC_MESSAGE"; field public static final String EXTRA_DIAGNOSTIC_MESSAGE_ID = "android.telecom.extra.DIAGNOSTIC_MESSAGE_ID"; + field public static final String EXTRA_IS_SUPPRESSED_BY_DO_NOT_DISTURB = "android.telecom.extra.IS_SUPPRESSED_BY_DO_NOT_DISTURB"; field public static final String EXTRA_LAST_EMERGENCY_CALLBACK_TIME_MILLIS = "android.telecom.extra.LAST_EMERGENCY_CALLBACK_TIME_MILLIS"; field public static final String EXTRA_SILENT_RINGING_REQUESTED = "android.telecom.extra.SILENT_RINGING_REQUESTED"; field public static final String EXTRA_SUGGESTED_PHONE_ACCOUNTS = "android.telecom.extra.SUGGESTED_PHONE_ACCOUNTS"; @@ -41863,6 +41912,7 @@ package android.telephony { field public static final String KEY_VOICE_PRIVACY_DISABLE_UI_BOOL = "voice_privacy_disable_ui_bool"; field public static final String KEY_VOLTE_REPLACEMENT_RAT_INT = "volte_replacement_rat_int"; field public static final String KEY_VONR_ENABLED_BOOL = "vonr_enabled_bool"; + field public static final String KEY_VONR_ON_BY_DEFAULT_BOOL = "vonr_on_by_default_bool"; field public static final String KEY_VONR_SETTING_VISIBILITY_BOOL = "vonr_setting_visibility_bool"; field public static final String KEY_VT_UPGRADE_SUPPORTED_FOR_DOWNGRADED_RTT_CALL_BOOL = "vt_upgrade_supported_for_downgraded_rtt_call"; field public static final String KEY_VVM_CELLULAR_DATA_REQUIRED_BOOL = "vvm_cellular_data_required_bool"; @@ -44098,7 +44148,7 @@ package android.telephony { 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 +51956,7 @@ package android.view { 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 +52405,7 @@ package android.view.accessibility { 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 +52493,7 @@ package android.view.accessibility { 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 +52573,13 @@ package android.view.accessibility { 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/module-lib-current.txt b/core/api/module-lib-current.txt index ce18745046ff..e928eb5f7731 100644 --- a/core/api/module-lib-current.txt +++ b/core/api/module-lib-current.txt @@ -349,6 +349,7 @@ package android.os { } public final class ServiceManager { + method @NonNull public static String[] getDeclaredInstances(@NonNull String); method public static boolean isDeclared(@NonNull String); method @Nullable public static android.os.IBinder waitForDeclaredService(@NonNull String); method @Nullable public static android.os.IBinder waitForService(@NonNull String); @@ -384,6 +385,7 @@ package android.os { method public static void traceBegin(long, @NonNull String); method public static void traceCounter(long, @NonNull String, int); method public static void traceEnd(long); + field public static final long TRACE_TAG_AIDL = 16777216L; // 0x1000000L field public static final long TRACE_TAG_NETWORK = 2097152L; // 0x200000L } diff --git a/core/api/system-current.txt b/core/api/system-current.txt index ae6e58c7ed48..ce1eff143185 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -588,6 +588,7 @@ package android.app { field public static final String OPSTR_READ_MEDIA_IMAGES = "android:read_media_images"; field public static final String OPSTR_READ_MEDIA_VIDEO = "android:read_media_video"; field public static final String OPSTR_READ_MEDIA_VISUAL_USER_SELECTED = "android:read_media_visual_user_selected"; + field public static final String OPSTR_READ_WRITE_HEALTH_DATA = "android:read_write_health_data"; field public static final String OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO = "android:receive_ambient_trigger_audio"; field public static final String OPSTR_RECEIVE_EMERGENCY_BROADCAST = "android:receive_emergency_broadcast"; field public static final String OPSTR_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO = "android:receive_explicit_user_interaction_audio"; @@ -7155,6 +7156,7 @@ package android.media.tv.tuner { public class Tuner implements java.lang.AutoCloseable { ctor @RequiresPermission(android.Manifest.permission.ACCESS_TV_TUNER) public Tuner(@NonNull android.content.Context, @Nullable String, int); + method public int applyFrontend(@NonNull android.media.tv.tuner.frontend.FrontendInfo); method public int cancelScanning(); method public int cancelTuning(); method public void clearOnTuneEventListener(); @@ -7185,7 +7187,6 @@ package android.media.tv.tuner { method @Nullable @RequiresPermission(android.Manifest.permission.ACCESS_TV_SHARED_FILTER) public static android.media.tv.tuner.filter.SharedFilter openSharedFilter(@NonNull android.content.Context, @NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.media.tv.tuner.filter.SharedFilterCallback); method @Nullable public android.media.tv.tuner.filter.TimeFilter openTimeFilter(); method public int removeOutputPid(@IntRange(from=0) int); - method public int requestFrontendById(int); method public int scan(@NonNull android.media.tv.tuner.frontend.FrontendSettings, int, @NonNull java.util.concurrent.Executor, @NonNull android.media.tv.tuner.frontend.ScanCallback); method public int setLnaEnabled(boolean); method public int setMaxNumberOfFrontends(int, @IntRange(from=0) int); @@ -9968,9 +9969,10 @@ package android.os { method public boolean isCloneProfile(); method public boolean isCredentialSharableWithParent(); method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS, android.Manifest.permission.QUERY_USERS}) public boolean isGuestUser(); + method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS, android.Manifest.permission.QUERY_USERS}) public boolean isMainUser(); method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.QUERY_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional=true) public boolean isManagedProfile(int); method public boolean isMediaSharedWithParent(); - method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS, android.Manifest.permission.QUERY_USERS}) public boolean isPrimaryUser(); + method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS, android.Manifest.permission.QUERY_USERS}) public boolean isPrimaryUser(); method public static boolean isRemoveResultSuccessful(int); method public boolean isRestrictedProfile(); method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}, conditional=true) public boolean isRestrictedProfile(@NonNull android.os.UserHandle); @@ -11924,11 +11926,39 @@ package android.service.timezone { method public abstract void onStopUpdates(); method public final void reportPermanentFailure(@NonNull Throwable); method public final void reportSuggestion(@NonNull android.service.timezone.TimeZoneProviderSuggestion); + method public final void reportSuggestion(@NonNull android.service.timezone.TimeZoneProviderSuggestion, @NonNull android.service.timezone.TimeZoneProviderStatus); method public final void reportUncertain(); + method public final void reportUncertain(@NonNull android.service.timezone.TimeZoneProviderStatus); field public static final String PRIMARY_LOCATION_TIME_ZONE_PROVIDER_SERVICE_INTERFACE = "android.service.timezone.PrimaryLocationTimeZoneProviderService"; field public static final String SECONDARY_LOCATION_TIME_ZONE_PROVIDER_SERVICE_INTERFACE = "android.service.timezone.SecondaryLocationTimeZoneProviderService"; } + public final class TimeZoneProviderStatus implements android.os.Parcelable { + method public int describeContents(); + method public int getConnectivityDependencyStatus(); + method public int getLocationDetectionDependencyStatus(); + method public int getTimeZoneResolutionOperationStatus(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.service.timezone.TimeZoneProviderStatus> CREATOR; + field public static final int DEPENDENCY_STATUS_BLOCKED_BY_ENVIRONMENT = 4; // 0x4 + field public static final int DEPENDENCY_STATUS_BLOCKED_BY_SETTINGS = 6; // 0x6 + field public static final int DEPENDENCY_STATUS_DEGRADED_BY_SETTINGS = 5; // 0x5 + field public static final int DEPENDENCY_STATUS_NOT_APPLICABLE = 1; // 0x1 + field public static final int DEPENDENCY_STATUS_OK = 2; // 0x2 + field public static final int DEPENDENCY_STATUS_TEMPORARILY_UNAVAILABLE = 3; // 0x3 + field public static final int OPERATION_STATUS_FAILED = 3; // 0x3 + field public static final int OPERATION_STATUS_NOT_APPLICABLE = 1; // 0x1 + field public static final int OPERATION_STATUS_OK = 2; // 0x2 + } + + public static final class TimeZoneProviderStatus.Builder { + ctor public TimeZoneProviderStatus.Builder(); + method @NonNull public android.service.timezone.TimeZoneProviderStatus build(); + method @NonNull public android.service.timezone.TimeZoneProviderStatus.Builder setConnectivityDependencyStatus(int); + method @NonNull public android.service.timezone.TimeZoneProviderStatus.Builder setLocationDetectionDependencyStatus(int); + method @NonNull public android.service.timezone.TimeZoneProviderStatus.Builder setTimeZoneResolutionOperationStatus(int); + } + public final class TimeZoneProviderSuggestion implements android.os.Parcelable { method public int describeContents(); method public long getElapsedRealtimeMillis(); @@ -12120,6 +12150,7 @@ package android.service.voice { method public static int getMaxScore(); method @Nullable public android.media.MediaSyncEvent getMediaSyncEvent(); method public int getPersonalizedScore(); + method public int getProximity(); method public int getScore(); method public boolean isHotwordDetectionPersonalized(); method public void writeToParcel(@NonNull android.os.Parcel, int); @@ -12133,6 +12164,9 @@ package android.service.voice { field public static final int CONFIDENCE_LEVEL_VERY_HIGH = 6; // 0x6 field @NonNull public static final android.os.Parcelable.Creator<android.service.voice.HotwordDetectedResult> CREATOR; field public static final int HOTWORD_OFFSET_UNSET = -1; // 0xffffffff + field public static final int PROXIMITY_FAR = 2; // 0x2 + field public static final int PROXIMITY_NEAR = 1; // 0x1 + field public static final int PROXIMITY_UNKNOWN = -1; // 0xffffffff } public static final class HotwordDetectedResult.Builder { @@ -12221,7 +12255,7 @@ package android.service.wallpaper { public class WallpaperService.Engine { method public boolean isInAmbientMode(); - method public void onAmbientModeChanged(boolean, long); + method @MainThread public void onAmbientModeChanged(boolean, long); } } @@ -12469,6 +12503,8 @@ package android.telecom { field public static final int CS_BOUND = 6; // 0x6 field public static final int DIRECT_TO_VM_FINISHED = 103; // 0x67 field public static final int DIRECT_TO_VM_INITIATED = 102; // 0x66 + field public static final int DND_CHECK_COMPLETED = 110; // 0x6e + field public static final int DND_CHECK_INITIATED = 109; // 0x6d field public static final int FILTERING_COMPLETED = 107; // 0x6b field public static final int FILTERING_INITIATED = 106; // 0x6a field public static final int FILTERING_TIMED_OUT = 108; // 0x6c @@ -12508,6 +12544,7 @@ package android.telecom { field @NonNull public static final android.os.Parcelable.Creator<android.telecom.ParcelableCallAnalytics.EventTiming> CREATOR; field public static final int DIRECT_TO_VM_FINISHED_TIMING = 8; // 0x8 field public static final int DISCONNECT_TIMING = 2; // 0x2 + field public static final int DND_PRE_CALL_PRE_CHECK_TIMING = 12; // 0xc field public static final int FILTERING_COMPLETED_TIMING = 10; // 0xa field public static final int FILTERING_TIMED_OUT_TIMING = 11; // 0xb field public static final int HOLD_TIMING = 3; // 0x3 @@ -14155,6 +14192,7 @@ package android.telephony.data { 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 ae5a5ee88d98..3fee610943ba 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -758,6 +758,7 @@ package android.content { method public int getUserId(); method public void setAutofillOptions(@Nullable android.content.AutofillOptions); method public void setContentCaptureOptions(@Nullable android.content.ContentCaptureOptions); + field public static final String ATTENTION_SERVICE = "attention"; field public static final String CONTENT_CAPTURE_MANAGER_SERVICE = "content_capture"; field public static final String DEVICE_IDLE_CONTROLLER = "deviceidle"; field public static final String DREAM_SERVICE = "dream"; @@ -904,6 +905,7 @@ package android.content.pm { method public boolean isFull(); method public boolean isGuest(); method public boolean isInitialized(); + method public boolean isMain(); method public boolean isManagedProfile(); method public boolean isPrimary(); method public boolean isProfile(); @@ -922,6 +924,7 @@ package android.content.pm { field public static final int FLAG_FULL = 1024; // 0x400 field @Deprecated public static final int FLAG_GUEST = 4; // 0x4 field public static final int FLAG_INITIALIZED = 16; // 0x10 + field public static final int FLAG_MAIN = 16384; // 0x4000 field @Deprecated public static final int FLAG_MANAGED_PROFILE = 32; // 0x20 field public static final int FLAG_PRIMARY = 1; // 0x1 field public static final int FLAG_PROFILE = 4096; // 0x1000 @@ -1900,6 +1903,7 @@ package android.os { method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public java.util.List<android.content.pm.UserInfo> getUsers(boolean, boolean, boolean); method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public boolean hasBaseUserRestriction(@NonNull String, @NonNull android.os.UserHandle); method public static boolean isSplitSystemUser(); + method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public boolean isUserTypeEnabled(@NonNull String); method public boolean isUsersOnSecondaryDisplaysSupported(); method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public android.content.pm.UserInfo preCreateUser(@NonNull String) throws android.os.UserManager.UserOperationException; } @@ -2499,6 +2503,10 @@ package android.service.voice { method @NonNull public android.service.voice.AlwaysOnHotwordDetector.EventPayload.Builder setKeyphraseRecognitionExtras(@NonNull java.util.List<android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra>); } + public abstract class HotwordDetectionService extends android.app.Service { + field public static final boolean ENABLE_PROXIMITY_RESULT = true; + } + public final class VisibleActivityInfo implements android.os.Parcelable { ctor public VisibleActivityInfo(int, @NonNull android.os.IBinder); } @@ -2622,13 +2630,23 @@ package android.telephony { method public int getCarrierIdListVersion(); method @NonNull @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public java.util.List<java.lang.String> getCertsFromCarrierPrivilegeAccessRules(); method @NonNull public java.util.List<android.telephony.data.ApnSetting> getDevicePolicyOverrideApns(@NonNull android.content.Context); + method @NonNull public android.util.Pair<java.lang.Integer,java.lang.Integer> getHalVersion(int); method @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public String getLine1AlphaTag(); - method public android.util.Pair<java.lang.Integer,java.lang.Integer> getRadioHalVersion(); + method @Deprecated public android.util.Pair<java.lang.Integer,java.lang.Integer> getRadioHalVersion(); method public boolean modifyDevicePolicyOverrideApn(@NonNull android.content.Context, int, @NonNull android.telephony.data.ApnSetting); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void refreshUiccProfile(); method @Deprecated public void setCarrierTestOverride(String, String, String, String, String, String, String); method public void setCarrierTestOverride(String, String, String, String, String, String, String, String, String); method @RequiresPermission(android.Manifest.permission.BIND_TELECOM_CONNECTION_SERVICE) public void setVoiceServiceStateOverride(boolean); + field public static final int HAL_SERVICE_DATA = 1; // 0x1 + field public static final int HAL_SERVICE_IMS = 7; // 0x7 + field public static final int HAL_SERVICE_MESSAGING = 2; // 0x2 + field public static final int HAL_SERVICE_MODEM = 3; // 0x3 + field public static final int HAL_SERVICE_NETWORK = 4; // 0x4 + field public static final int HAL_SERVICE_SIM = 5; // 0x5 + field public static final int HAL_SERVICE_VOICE = 6; // 0x6 + field public static final android.util.Pair HAL_VERSION_UNKNOWN; + field public static final android.util.Pair HAL_VERSION_UNSUPPORTED; field public static final int UNKNOWN_CARRIER_ID_LIST_VERSION = -1; // 0xffffffff } @@ -2945,6 +2963,7 @@ package android.view { 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/api/test-lint-baseline.txt b/core/api/test-lint-baseline.txt index f9b8a3006880..8e21d8c2cb56 100644 --- a/core/api/test-lint-baseline.txt +++ b/core/api/test-lint-baseline.txt @@ -567,6 +567,10 @@ MissingNullability: android.telephony.SmsManager#checkSmsShortCodeDestination(St Missing nullability on parameter `destAddress` in method `checkSmsShortCodeDestination` MissingNullability: android.telephony.SmsManager#checkSmsShortCodeDestination(String, String) parameter #1: Missing nullability on parameter `countryIso` in method `checkSmsShortCodeDestination` +MissingNullability: android.telephony.TelephonyManager#HAL_VERSION_UNKNOWN: + Missing nullability on field `HAL_VERSION_UNKNOWN` in class `class android.telephony.TelephonyManager` +MissingNullability: android.telephony.TelephonyManager#HAL_VERSION_UNSUPPORTED: + Missing nullability on field `HAL_VERSION_UNSUPPORTED` in class `class android.telephony.TelephonyManager` MissingNullability: android.telephony.TelephonyManager#getLine1AlphaTag(): Missing nullability on method `getLine1AlphaTag` return MissingNullability: android.telephony.TelephonyManager#getRadioHalVersion(): diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java index e86d2f35ef16..2fe5d5140371 100644 --- a/core/java/android/accessibilityservice/AccessibilityService.java +++ b/core/java/android/accessibilityservice/AccessibilityService.java @@ -696,7 +696,8 @@ public abstract class AccessibilityService extends Service { ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR, ERROR_TAKE_SCREENSHOT_NO_ACCESSIBILITY_ACCESS, ERROR_TAKE_SCREENSHOT_INTERVAL_TIME_SHORT, - ERROR_TAKE_SCREENSHOT_INVALID_DISPLAY + ERROR_TAKE_SCREENSHOT_INVALID_DISPLAY, + ERROR_TAKE_SCREENSHOT_INVALID_WINDOW }) public @interface ScreenshotErrorCode {} @@ -728,6 +729,18 @@ public abstract class AccessibilityService extends Service { public static final int ERROR_TAKE_SCREENSHOT_INVALID_DISPLAY = 4; /** + * The status of taking screenshot is failure and the reason is invalid accessibility window Id. + */ + public static final int ERROR_TAKE_SCREENSHOT_INVALID_WINDOW = 5; + + /** + * The status of taking screenshot is failure and the reason is the window contains secure + * content. + * @see WindowManager.LayoutParams#FLAG_SECURE + */ + public static final int ERROR_TAKE_SCREENSHOT_SECURE_WINDOW = 6; + + /** * The interval time of calling * {@link AccessibilityService#takeScreenshot(int, Executor, Consumer)} API. * @hide @@ -2568,6 +2581,7 @@ public abstract class AccessibilityService extends Service { * @param executor Executor on which to run the callback. * @param callback The callback invoked when taking screenshot has succeeded or failed. * See {@link TakeScreenshotCallback} for details. + * @see #takeScreenshotOfWindow */ public void takeScreenshot(int displayId, @NonNull @CallbackExecutor Executor executor, @NonNull TakeScreenshotCallback callback) { @@ -2589,7 +2603,8 @@ public abstract class AccessibilityService extends Service { final HardwareBuffer hardwareBuffer = result.getParcelable(KEY_ACCESSIBILITY_SCREENSHOT_HARDWAREBUFFER, android.hardware.HardwareBuffer.class); final ParcelableColorSpace colorSpace = - result.getParcelable(KEY_ACCESSIBILITY_SCREENSHOT_COLORSPACE, android.graphics.ParcelableColorSpace.class); + result.getParcelable(KEY_ACCESSIBILITY_SCREENSHOT_COLORSPACE, + android.graphics.ParcelableColorSpace.class); final ScreenshotResult screenshot = new ScreenshotResult(hardwareBuffer, colorSpace.getColorSpace(), result.getLong(KEY_ACCESSIBILITY_SCREENSHOT_TIMESTAMP)); @@ -2601,6 +2616,37 @@ public abstract class AccessibilityService extends Service { } /** + * Takes a screenshot of the specified window and returns it via an + * {@link AccessibilityService.ScreenshotResult}. You can use {@link Bitmap#wrapHardwareBuffer} + * to construct the bitmap from the ScreenshotResult's payload. + * <p> + * <strong>Note:</strong> In order to take screenshots your service has + * to declare the capability to take screenshot by setting the + * {@link android.R.styleable#AccessibilityService_canTakeScreenshot} + * property in its meta-data. For details refer to {@link #SERVICE_META_DATA}. + * </p> + * <p> + * Both this method and {@link #takeScreenshot} can be used for machine learning-based visual + * screen understanding. Use <code>takeScreenshotOfWindow</code> if your target window might be + * visually underneath an accessibility overlay (from your or another accessibility service) in + * order to capture the window contents without the screenshot being covered by the overlay + * contents drawn on the screen. + * </p> + * + * @param accessibilityWindowId The window id, from {@link AccessibilityWindowInfo#getId()}. + * @param executor Executor on which to run the callback. + * @param callback The callback invoked when taking screenshot has succeeded or failed. + * See {@link TakeScreenshotCallback} for details. + * @see #takeScreenshot + */ + public void takeScreenshotOfWindow(int accessibilityWindowId, + @NonNull @CallbackExecutor Executor executor, + @NonNull TakeScreenshotCallback callback) { + AccessibilityInteractionClient.getInstance(this).takeScreenshotOfWindow( + mConnectionId, accessibilityWindowId, executor, callback); + } + + /** * Sets the strokeWidth and color of the accessibility focus rectangle. * <p> * <strong>Note:</strong> This setting persists until this or another active @@ -3113,7 +3159,8 @@ public abstract class AccessibilityService extends Service { private final @NonNull ColorSpace mColorSpace; private final long mTimestamp; - private ScreenshotResult(@NonNull HardwareBuffer hardwareBuffer, + /** @hide */ + public ScreenshotResult(@NonNull HardwareBuffer hardwareBuffer, @NonNull ColorSpace colorSpace, long timestamp) { Preconditions.checkNotNull(hardwareBuffer, "hardwareBuffer cannot be null"); Preconditions.checkNotNull(colorSpace, "colorSpace cannot be null"); diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl index 9abce3a39944..da14b50e6481 100644 --- a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl +++ b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl @@ -30,6 +30,7 @@ import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.view.accessibility.IAccessibilityInteractionConnectionCallback; import android.view.accessibility.AccessibilityWindowInfo; import java.util.List; +import android.window.ScreenCapture; /** * Interface given to an AccessibilitySerivce to talk to the AccessibilityManagerService. @@ -122,6 +123,10 @@ interface IAccessibilityServiceConnection { void takeScreenshot(int displayId, in RemoteCallback callback); + void takeScreenshotOfWindow(int accessibilityWindowId, int interactionId, + in ScreenCapture.ScreenCaptureListener listener, + IAccessibilityInteractionConnectionCallback callback); + void setGestureDetectionPassthroughRegion(int displayId, in Region region); void setTouchExplorationPassthroughRegion(int displayId, in Region region); diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index dc325ff96de1..1b3282e752f4 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -42,6 +42,7 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; import android.database.DatabaseUtils; +import android.healthconnect.HealthConnectManager; import android.media.AudioAttributes.AttributeUsage; import android.os.Binder; import android.os.Build; @@ -1390,9 +1391,25 @@ public class AppOpsManager { public static final int OP_SYSTEM_EXEMPT_FROM_FORCED_APP_STANDBY = AppProtoEnums.APP_OP_SYSTEM_EXEMPT_FROM_FORCED_APP_STANDBY; + /** + * An app op for reading/writing health connect data. + * + * @hide + */ + public static final int OP_READ_WRITE_HEALTH_DATA = AppProtoEnums.APP_OP_READ_WRITE_HEALTH_DATA; + + /** + * Use foreground service with the type + * {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_SPECIAL_USE}. + * + * @hide + */ + public static final int OP_FOREGROUND_SERVICE_SPECIAL_USE = + AppProtoEnums.APP_OP_FOREGROUND_SERVICE_SPECIAL_USE; + /** @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - public static final int _NUM_OP = 126; + public static final int _NUM_OP = 128; /** Access to coarse location information. */ public static final String OPSTR_COARSE_LOCATION = "android:coarse_location"; @@ -1873,6 +1890,15 @@ public class AppOpsManager { "android:read_media_visual_user_selected"; /** + * An app op for reading/writing health connect data. + * + * @hide + */ + @SystemApi + public static final String OPSTR_READ_WRITE_HEALTH_DATA = + "android:read_write_health_data"; + + /** * Record audio from near-field microphone (ie. TV remote) * Allows audio recording regardless of sensor privacy state, * as it is an intentional user interaction: hold-to-talk @@ -1913,6 +1939,14 @@ public class AppOpsManager { public static final String OPSTR_SYSTEM_EXEMPT_FROM_FORCED_APP_STANDBY = "android:system_exempt_from_forced_app_standby"; + /** + * Start a foreground service with the type "specialUse". + * + * @hide + */ + public static final String OPSTR_FOREGROUND_SERVICE_SPECIAL_USE = + "android:foreground_service_special_use"; + /** {@link #sAppOpsToNote} not initialized yet for this op */ private static final byte SHOULD_COLLECT_NOTE_OP_NOT_INITIALIZED = 0; /** Should not collect noting of this app-op in {@link #sAppOpsToNote} */ @@ -2009,6 +2043,7 @@ public class AppOpsManager { OP_TURN_SCREEN_ON, OP_RUN_LONG_JOBS, OP_READ_MEDIA_VISUAL_USER_SELECTED, + OP_FOREGROUND_SERVICE_SPECIAL_USE, }; static final AppOpInfo[] sAppOpInfos = new AppOpInfo[]{ @@ -2400,9 +2435,17 @@ public class AppOpsManager { "SYSTEM_EXEMPT_FROM_APP_STANDBY").build(), new AppOpInfo.Builder(OP_SYSTEM_EXEMPT_FROM_FORCED_APP_STANDBY, OPSTR_SYSTEM_EXEMPT_FROM_FORCED_APP_STANDBY, - "SYSTEM_EXEMPT_FROM_FORCED_APP_STANDBY").build() + "SYSTEM_EXEMPT_FROM_FORCED_APP_STANDBY").build(), + new AppOpInfo.Builder(OP_READ_WRITE_HEALTH_DATA, OPSTR_READ_WRITE_HEALTH_DATA, + "READ_WRITE_HEALTH_DATA").setDefaultMode(AppOpsManager.MODE_ALLOWED).build(), + new AppOpInfo.Builder(OP_FOREGROUND_SERVICE_SPECIAL_USE, + OPSTR_FOREGROUND_SERVICE_SPECIAL_USE, "FOREGROUND_SERVICE_SPECIAL_USE") + .setPermission(Manifest.permission.FOREGROUND_SERVICE_SPECIAL_USE).build(), }; + // The number of longs needed to form a full bitmask of app ops + private static final int BITMASK_LEN = ((_NUM_OP - 1) / Long.SIZE) + 1; + /** * @hide */ @@ -2437,8 +2480,8 @@ public class AppOpsManager { * @see #getNotedOpCollectionMode * @see #collectNotedOpSync */ - private static final ThreadLocal<ArrayMap<String, long[]>> sAppOpsNotedInThisBinderTransaction = - new ThreadLocal<>(); + private static final ThreadLocal<ArrayMap<String, BitSet>> + sAppOpsNotedInThisBinderTransaction = new ThreadLocal<>(); static { if (sAppOpInfos.length != _NUM_OP) { @@ -2557,7 +2600,14 @@ public class AppOpsManager { @TestApi public static int permissionToOpCode(String permission) { Integer boxedOpCode = sPermToOp.get(permission); - return boxedOpCode != null ? boxedOpCode : OP_NONE; + if (boxedOpCode != null) { + return boxedOpCode; + } + if (HealthConnectManager.isHealthPermission(ActivityThread.currentApplication(), + permission)) { + return OP_READ_WRITE_HEALTH_DATA; + } + return OP_NONE; } /** @@ -7221,10 +7271,14 @@ public class AppOpsManager { */ public static @Nullable String permissionToOp(@NonNull String permission) { final Integer opCode = sPermToOp.get(permission); - if (opCode == null) { - return null; + if (opCode != null) { + return sAppOpInfos[opCode].name; + } + if (HealthConnectManager.isHealthPermission(ActivityThread.currentApplication(), + permission)) { + return sAppOpInfos[OP_READ_WRITE_HEALTH_DATA].name; } - return sAppOpInfos[opCode].name; + return null; } /** @@ -8453,8 +8507,9 @@ public class AppOpsManager { */ public int startProxyOpNoThrow(int op, @NonNull AttributionSource attributionSource, @Nullable String message, boolean skipProxyOperation) { - return startProxyOpNoThrow(op, attributionSource, message, skipProxyOperation, - ATTRIBUTION_FLAGS_NONE, ATTRIBUTION_FLAGS_NONE, ATTRIBUTION_CHAIN_ID_NONE); + return startProxyOpNoThrow(attributionSource.getToken(), op, attributionSource, message, + skipProxyOperation, ATTRIBUTION_FLAGS_NONE, ATTRIBUTION_FLAGS_NONE, + ATTRIBUTION_CHAIN_ID_NONE); } /** @@ -8466,7 +8521,8 @@ public class AppOpsManager { * * @hide */ - public int startProxyOpNoThrow(int op, @NonNull AttributionSource attributionSource, + public int startProxyOpNoThrow(@NonNull IBinder clientId, int op, + @NonNull AttributionSource attributionSource, @Nullable String message, boolean skipProxyOperation, @AttributionFlags int proxyAttributionFlags, @AttributionFlags int proxiedAttributionFlags, int attributionChainId) { @@ -8484,7 +8540,7 @@ public class AppOpsManager { } } - SyncNotedAppOp syncOp = mService.startProxyOperation(op, + SyncNotedAppOp syncOp = mService.startProxyOperation(clientId, op, attributionSource, false, collectionMode == COLLECT_ASYNC, message, shouldCollectMessage, skipProxyOperation, proxyAttributionFlags, proxiedAttributionFlags, attributionChainId); @@ -8582,9 +8638,10 @@ public class AppOpsManager { */ public void finishProxyOp(@NonNull String op, int proxiedUid, @NonNull String proxiedPackageName, @Nullable String proxiedAttributionTag) { - finishProxyOp(op, new AttributionSource(mContext.getAttributionSource(), + IBinder token = mContext.getAttributionSource().getToken(); + finishProxyOp(token, op, new AttributionSource(mContext.getAttributionSource(), new AttributionSource(proxiedUid, proxiedPackageName, proxiedAttributionTag, - mContext.getAttributionSource().getToken())), /*skipProxyOperation*/ false); + token)), /*skipProxyOperation*/ false); } /** @@ -8599,10 +8656,11 @@ public class AppOpsManager { * * @hide */ - public void finishProxyOp(@NonNull String op, @NonNull AttributionSource attributionSource, - boolean skipProxyOperation) { + public void finishProxyOp(@NonNull IBinder clientId, @NonNull String op, + @NonNull AttributionSource attributionSource, boolean skipProxyOperation) { try { - mService.finishProxyOperation(strOpToOp(op), attributionSource, skipProxyOperation); + mService.finishProxyOperation(clientId, strOpToOp(op), attributionSource, + skipProxyOperation); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -8684,10 +8742,10 @@ public class AppOpsManager { */ public static class PausedNotedAppOpsCollection { final int mUid; - final @Nullable ArrayMap<String, long[]> mCollectedNotedAppOps; + final @Nullable ArrayMap<String, BitSet> mCollectedNotedAppOps; PausedNotedAppOpsCollection(int uid, @Nullable ArrayMap<String, - long[]> collectedNotedAppOps) { + BitSet> collectedNotedAppOps) { mUid = uid; mCollectedNotedAppOps = collectedNotedAppOps; } @@ -8705,7 +8763,7 @@ public class AppOpsManager { public static @Nullable PausedNotedAppOpsCollection pauseNotedAppOpsCollection() { Integer previousUid = sBinderThreadCallingUid.get(); if (previousUid != null) { - ArrayMap<String, long[]> previousCollectedNotedAppOps = + ArrayMap<String, BitSet> previousCollectedNotedAppOps = sAppOpsNotedInThisBinderTransaction.get(); sBinderThreadCallingUid.remove(); @@ -8779,23 +8837,19 @@ public class AppOpsManager { // We are inside of a two-way binder call. Delivered to caller via // {@link #prefixParcelWithAppOpsIfNeeded} int op = sOpStrToOp.get(syncOp.getOp()); - ArrayMap<String, long[]> appOpsNoted = sAppOpsNotedInThisBinderTransaction.get(); + ArrayMap<String, BitSet> appOpsNoted = sAppOpsNotedInThisBinderTransaction.get(); if (appOpsNoted == null) { appOpsNoted = new ArrayMap<>(1); sAppOpsNotedInThisBinderTransaction.set(appOpsNoted); } - long[] appOpsNotedForAttribution = appOpsNoted.get(syncOp.getAttributionTag()); + BitSet appOpsNotedForAttribution = appOpsNoted.get(syncOp.getAttributionTag()); if (appOpsNotedForAttribution == null) { - appOpsNotedForAttribution = new long[2]; + appOpsNotedForAttribution = new BitSet(_NUM_OP); appOpsNoted.put(syncOp.getAttributionTag(), appOpsNotedForAttribution); } - if (op < 64) { - appOpsNotedForAttribution[0] |= 1L << op; - } else { - appOpsNotedForAttribution[1] |= 1L << (op - 64); - } + appOpsNotedForAttribution.set(op); } /** @hide */ @@ -8869,7 +8923,7 @@ public class AppOpsManager { */ // TODO (b/186872903) Refactor how sync noted ops are propagated. public static void prefixParcelWithAppOpsIfNeeded(@NonNull Parcel p) { - ArrayMap<String, long[]> notedAppOps = sAppOpsNotedInThisBinderTransaction.get(); + ArrayMap<String, BitSet> notedAppOps = sAppOpsNotedInThisBinderTransaction.get(); if (notedAppOps == null) { return; } @@ -8881,8 +8935,15 @@ public class AppOpsManager { for (int i = 0; i < numAttributionWithNotesAppOps; i++) { p.writeString(notedAppOps.keyAt(i)); - p.writeLong(notedAppOps.valueAt(i)[0]); - p.writeLong(notedAppOps.valueAt(i)[1]); + // Bitmask's toLongArray will truncate the array, if upper bits arent used + long[] notedOpsMask = notedAppOps.valueAt(i).toLongArray(); + for (int j = 0; j < BITMASK_LEN; j++) { + if (j < notedOpsMask.length) { + p.writeLong(notedOpsMask[j]); + } else { + p.writeLong(0); + } + } } } @@ -8901,12 +8962,13 @@ public class AppOpsManager { for (int i = 0; i < numAttributionsWithNotedAppOps; i++) { String attributionTag = p.readString(); - long[] rawNotedAppOps = new long[2]; - rawNotedAppOps[0] = p.readLong(); - rawNotedAppOps[1] = p.readLong(); + long[] rawNotedAppOps = new long[BITMASK_LEN]; + for (int j = 0; j < rawNotedAppOps.length; j++) { + rawNotedAppOps[j] = p.readLong(); + } + BitSet notedAppOps = BitSet.valueOf(rawNotedAppOps); - if (rawNotedAppOps[0] != 0 || rawNotedAppOps[1] != 0) { - BitSet notedAppOps = BitSet.valueOf(rawNotedAppOps); + if (!notedAppOps.isEmpty()) { synchronized (sLock) { for (int code = notedAppOps.nextSetBit(0); code != -1; diff --git a/core/java/android/app/AppOpsManagerInternal.java b/core/java/android/app/AppOpsManagerInternal.java index 4d6e4aedba66..43023fe9c2ab 100644 --- a/core/java/android/app/AppOpsManagerInternal.java +++ b/core/java/android/app/AppOpsManagerInternal.java @@ -26,13 +26,11 @@ import android.util.SparseArray; import android.util.SparseIntArray; import com.android.internal.app.IAppOpsCallback; -import com.android.internal.util.function.DecFunction; import com.android.internal.util.function.HeptFunction; import com.android.internal.util.function.HexFunction; import com.android.internal.util.function.QuadFunction; import com.android.internal.util.function.QuintConsumer; import com.android.internal.util.function.QuintFunction; -import com.android.internal.util.function.TriFunction; import com.android.internal.util.function.UndecFunction; /** @@ -135,6 +133,7 @@ public abstract class AppOpsManagerInternal { /** * Allows overriding start proxy operation behavior. * + * @param clientId The client calling start, represented by an IBinder * @param code The op code to start. * @param attributionSource The permission identity of the caller. * @param startIfModeDefault Whether to start the op of the mode is default. @@ -148,11 +147,12 @@ public abstract class AppOpsManagerInternal { * @param superImpl The super implementation. * @return The app op note result. */ - SyncNotedAppOp startProxyOperation(int code, @NonNull AttributionSource attributionSource, - boolean startIfModeDefault, boolean shouldCollectAsyncNotedOp, String message, - boolean shouldCollectMessage, boolean skipProxyOperation, @AttributionFlags - int proxyAttributionFlags, @AttributionFlags int proxiedAttributionFlags, - int attributionChainId, @NonNull DecFunction<Integer, AttributionSource, Boolean, + SyncNotedAppOp startProxyOperation(@NonNull IBinder clientId, int code, + @NonNull AttributionSource attributionSource, boolean startIfModeDefault, + boolean shouldCollectAsyncNotedOp, String message, boolean shouldCollectMessage, + boolean skipProxyOperation, @AttributionFlags int proxyAttributionFlags, + @AttributionFlags int proxiedAttributionFlags, int attributionChainId, + @NonNull UndecFunction<IBinder, Integer, AttributionSource, Boolean, Boolean, String, Boolean, Boolean, Integer, Integer, Integer, SyncNotedAppOp> superImpl); @@ -176,10 +176,15 @@ public abstract class AppOpsManagerInternal { * * @param code The op code to finish. * @param attributionSource The permission identity of the caller. + * @param skipProxyOperation Whether to skip the proxy in the proxy/proxied operation + * @param clientId The client calling finishProxyOperation + * @param superImpl The "standard" implementation to potentially call */ - void finishProxyOperation(int code, @NonNull AttributionSource attributionSource, + void finishProxyOperation(@NonNull IBinder clientId, int code, + @NonNull AttributionSource attributionSource, boolean skipProxyOperation, - @NonNull TriFunction<Integer, AttributionSource, Boolean, Void> superImpl); + @NonNull QuadFunction<IBinder, Integer, AttributionSource, Boolean, + Void> superImpl); } /** diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 10cdf5315b55..042bdd7e2ed5 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -1814,12 +1814,6 @@ class ContextImpl extends Context { } } try { - ActivityThread thread = ActivityThread.currentActivityThread(); - Instrumentation instrumentation = thread.getInstrumentation(); - if (instrumentation.isInstrumenting() - && ((flags & Context.RECEIVER_NOT_EXPORTED) == 0)) { - flags = flags | Context.RECEIVER_EXPORTED; - } final Intent intent = ActivityManager.getService().registerReceiverWithFeature( mMainThread.getApplicationThread(), mBasePackageName, getAttributionTag(), AppOpsManager.toReceiverId(receiver), rd, filter, broadcastPermission, userId, diff --git a/core/java/android/app/ForegroundServiceTypeNotAllowedException.java b/core/java/android/app/ForegroundServiceTypeNotAllowedException.java new file mode 100644 index 000000000000..c258242627c9 --- /dev/null +++ b/core/java/android/app/ForegroundServiceTypeNotAllowedException.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.app; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Exception thrown when an app tries to start a foreground {@link Service} without a valid type. + */ +public final class ForegroundServiceTypeNotAllowedException + extends ServiceStartNotAllowedException implements Parcelable { + /** + * Constructor. + */ + public ForegroundServiceTypeNotAllowedException(@NonNull String message) { + super(message); + } + + ForegroundServiceTypeNotAllowedException(@NonNull Parcel source) { + super(source.readString()); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeString(getMessage()); + } + + public static final @NonNull Creator<android.app.ForegroundServiceTypeNotAllowedException> + CREATOR = new Creator<android.app.ForegroundServiceTypeNotAllowedException>() { + @NonNull + public android.app.ForegroundServiceTypeNotAllowedException createFromParcel( + Parcel source) { + return new android.app.ForegroundServiceTypeNotAllowedException(source); + } + + @NonNull + public android.app.ForegroundServiceTypeNotAllowedException[] newArray(int size) { + return new android.app.ForegroundServiceTypeNotAllowedException[size]; + } + }; +} diff --git a/core/java/android/app/ForegroundServiceTypePolicy.java b/core/java/android/app/ForegroundServiceTypePolicy.java new file mode 100644 index 000000000000..eccc5631d86a --- /dev/null +++ b/core/java/android/app/ForegroundServiceTypePolicy.java @@ -0,0 +1,1033 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app; + +import static android.app.AppOpsManager.MODE_ALLOWED; +import static android.app.AppOpsManager.MODE_FOREGROUND; +import static android.content.pm.PackageManager.PERMISSION_DENIED; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA; +import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE; +import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC; +import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_HEALTH; +import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION; +import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK; +import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION; +import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE; +import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE; +import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL; +import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING; +import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE; +import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED; + +import android.Manifest; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.app.compat.CompatChanges; +import android.compat.Compatibility; +import android.compat.annotation.ChangeId; +import android.compat.annotation.Disabled; +import android.compat.annotation.EnabledAfter; +import android.compat.annotation.Overridable; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.ServiceInfo; +import android.content.pm.ServiceInfo.ForegroundServiceType; +import android.hardware.usb.UsbAccessory; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbManager; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.ArraySet; +import android.util.SparseArray; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.compat.CompatibilityChangeConfig; +import com.android.internal.compat.IPlatformCompat; +import com.android.internal.util.ArrayUtils; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.HashMap; +import java.util.Optional; + +/** + * This class enforces the policies around the foreground service types. + * + * @hide + */ +public abstract class ForegroundServiceTypePolicy { + static final String TAG = "ForegroundServiceTypePolicy"; + static final boolean DEBUG_FOREGROUND_SERVICE_TYPE_POLICY = false; + + /** + * The FGS type enforcement: + * deprecating the {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_NONE}. + * + * <p>Starting a FGS with this type (equivalent of no type) from apps with + * targetSdkVersion {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} or later will + * result in a warning in the log.</p> + * + * @hide + */ + @ChangeId + @EnabledAfter(targetSdkVersion = android.os.Build.VERSION_CODES.TIRAMISU) + @Overridable + public static final long FGS_TYPE_NONE_DEPRECATION_CHANGE_ID = 255042465L; + + /** + * The FGS type enforcement: + * disabling the {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_NONE}. + * + * <p>Starting a FGS with this type (equivalent of no type) from apps with + * targetSdkVersion {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} or later will + * result in an exception.</p> + * + * @hide + */ + // TODO (b/254661666): Change to @EnabledAfter(T) + @ChangeId + @Disabled + @Overridable + public static final long FGS_TYPE_NONE_DISABLED_CHANGE_ID = 255038118L; + + /** + * The FGS type enforcement: + * deprecating the {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_DATA_SYNC}. + * + * <p>Starting a FGS with this type from apps with targetSdkVersion + * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} or later will + * result in a warning in the log.</p> + * + * @hide + */ + @ChangeId + @EnabledAfter(targetSdkVersion = android.os.Build.VERSION_CODES.TIRAMISU) + @Overridable + public static final long FGS_TYPE_DATA_SYNC_DEPRECATION_CHANGE_ID = 255039210L; + + /** + * The FGS type enforcement: + * disabling the {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_DATA_SYNC}. + * + * <p>Starting a FGS with this type from apps with targetSdkVersion + * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} or later will + * result in an exception.</p> + * + * @hide + */ + // TODO (b/254661666): Change to @EnabledSince(U) in next OS release + @ChangeId + @Disabled + @Overridable + public static final long FGS_TYPE_DATA_SYNC_DISABLED_CHANGE_ID = 255659651L; + + /** + * The FGS type enforcement: Starting a FGS from apps with targetSdkVersion + * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} or later but without the required + * permissions associated with the FGS type will result in a SecurityException. + * + * @hide + */ + // TODO (b/254661666): Change to @EnabledAfter(T) + @ChangeId + @Disabled + @Overridable + public static final long FGS_TYPE_PERMISSION_CHANGE_ID = 254662522L; + + /** + * The policy for the {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_NONE}. + * + * @hide + */ + public static final @NonNull ForegroundServiceTypePolicyInfo FGS_TYPE_POLICY_NONE = + new ForegroundServiceTypePolicyInfo( + FOREGROUND_SERVICE_TYPE_NONE, + FGS_TYPE_NONE_DEPRECATION_CHANGE_ID, + FGS_TYPE_NONE_DISABLED_CHANGE_ID, + null, + null + ); + + /** + * The policy for the {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_DATA_SYNC}. + * + * @hide + */ + public static final @NonNull ForegroundServiceTypePolicyInfo FGS_TYPE_POLICY_DATA_SYNC = + new ForegroundServiceTypePolicyInfo( + FOREGROUND_SERVICE_TYPE_DATA_SYNC, + FGS_TYPE_DATA_SYNC_DEPRECATION_CHANGE_ID, + FGS_TYPE_DATA_SYNC_DISABLED_CHANGE_ID, + new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] { + new RegularPermission(Manifest.permission.FOREGROUND_SERVICE_DATA_SYNC) + }, true), + null + ); + + /** + * The policy for the {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK}. + * + * @hide + */ + public static final @NonNull ForegroundServiceTypePolicyInfo FGS_TYPE_POLICY_MEDIA_PLAYBACK = + new ForegroundServiceTypePolicyInfo( + FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK, + ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID, + ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID, + new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] { + new RegularPermission(Manifest.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK) + }, true), + null + ); + + /** + * The policy for the {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_PHONE_CALL}. + * + * @hide + */ + public static final @NonNull ForegroundServiceTypePolicyInfo FGS_TYPE_POLICY_PHONE_CALL = + new ForegroundServiceTypePolicyInfo( + FOREGROUND_SERVICE_TYPE_PHONE_CALL, + ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID, + ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID, + new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] { + new RegularPermission(Manifest.permission.FOREGROUND_SERVICE_PHONE_CALL) + }, true), + new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] { + new RegularPermission(Manifest.permission.MANAGE_OWN_CALLS) + }, false) + ); + + /** + * The policy for the {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_LOCATION}. + * + * @hide + */ + public static final @NonNull ForegroundServiceTypePolicyInfo FGS_TYPE_POLICY_LOCATION = + new ForegroundServiceTypePolicyInfo( + FOREGROUND_SERVICE_TYPE_LOCATION, + ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID, + ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID, + new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] { + new RegularPermission(Manifest.permission.FOREGROUND_SERVICE_LOCATION) + }, true), + new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] { + new RegularPermission(Manifest.permission.ACCESS_COARSE_LOCATION), + new RegularPermission(Manifest.permission.ACCESS_FINE_LOCATION), + }, false) + ); + + /** + * The policy for the {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE}. + * + * @hide + */ + public static final @NonNull ForegroundServiceTypePolicyInfo FGS_TYPE_POLICY_CONNECTED_DEVICE = + new ForegroundServiceTypePolicyInfo( + FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE, + ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID, + ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID, + new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] { + new RegularPermission(Manifest.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE) + }, true), + new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] { + new RegularPermission(Manifest.permission.BLUETOOTH_CONNECT), + new RegularPermission(Manifest.permission.CHANGE_NETWORK_STATE), + new RegularPermission(Manifest.permission.CHANGE_WIFI_STATE), + new RegularPermission(Manifest.permission.CHANGE_WIFI_MULTICAST_STATE), + new RegularPermission(Manifest.permission.NFC), + new RegularPermission(Manifest.permission.TRANSMIT_IR), + new UsbDevicePermission(), + new UsbAccessoryPermission(), + }, false) + ); + + /** + * The policy for the {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION}. + * + * @hide + */ + public static final @NonNull ForegroundServiceTypePolicyInfo FGS_TYPE_POLICY_MEDIA_PROJECTION = + new ForegroundServiceTypePolicyInfo( + FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION, + ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID, + ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID, + new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] { + new RegularPermission(Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION) + }, true), + new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] { + new RegularPermission(Manifest.permission.CAPTURE_VIDEO_OUTPUT), + new AppOpPermission(AppOpsManager.OP_PROJECT_MEDIA) + }, false) + ); + + /** + * The policy for the {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_CAMERA}. + * + * @hide + */ + public static final @NonNull ForegroundServiceTypePolicyInfo FGS_TYPE_POLICY_CAMERA = + new ForegroundServiceTypePolicyInfo( + FOREGROUND_SERVICE_TYPE_CAMERA, + ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID, + ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID, + new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] { + new RegularPermission(Manifest.permission.FOREGROUND_SERVICE_CAMERA) + }, true), + new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] { + new RegularPermission(Manifest.permission.CAMERA), + new RegularPermission(Manifest.permission.SYSTEM_CAMERA), + }, false) + ); + + /** + * The policy for the {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_MICROPHONE}. + * + * @hide + */ + public static final @NonNull ForegroundServiceTypePolicyInfo FGS_TYPE_POLICY_MICROPHONE = + new ForegroundServiceTypePolicyInfo( + FOREGROUND_SERVICE_TYPE_MICROPHONE, + ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID, + ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID, + new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] { + new RegularPermission(Manifest.permission.FOREGROUND_SERVICE_MICROPHONE) + }, true), + new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] { + new RegularPermission(Manifest.permission.CAPTURE_AUDIO_HOTWORD), + new RegularPermission(Manifest.permission.CAPTURE_AUDIO_OUTPUT), + new RegularPermission(Manifest.permission.CAPTURE_MEDIA_OUTPUT), + new RegularPermission(Manifest.permission.CAPTURE_TUNER_AUDIO_INPUT), + new RegularPermission(Manifest.permission.CAPTURE_VOICE_COMMUNICATION_OUTPUT), + new RegularPermission(Manifest.permission.RECORD_AUDIO), + }, false) + ); + + /** + * The policy for the {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_HEALTH}. + * + * @hide + */ + public static final @NonNull ForegroundServiceTypePolicyInfo FGS_TYPE_POLICY_HEALTH = + new ForegroundServiceTypePolicyInfo( + FOREGROUND_SERVICE_TYPE_HEALTH, + ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID, + ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID, + new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] { + new RegularPermission(Manifest.permission.FOREGROUND_SERVICE_HEALTH) + }, true), + new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] { + new RegularPermission(Manifest.permission.ACTIVITY_RECOGNITION), + new RegularPermission(Manifest.permission.BODY_SENSORS), + new RegularPermission(Manifest.permission.HIGH_SAMPLING_RATE_SENSORS), + }, false) + ); + + /** + * The policy for the {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING}. + * + * @hide + */ + public static final @NonNull ForegroundServiceTypePolicyInfo FGS_TYPE_POLICY_REMOTE_MESSAGING = + new ForegroundServiceTypePolicyInfo( + FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING, + ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID, + ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID, + new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] { + new RegularPermission(Manifest.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING) + }, true), + null + ); + + /** + * The policy for the {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED}. + * + * @hide + */ + public static final @NonNull ForegroundServiceTypePolicyInfo FGS_TYPE_POLICY_SYSTEM_EXEMPTED = + new ForegroundServiceTypePolicyInfo( + FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED, + ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID, + ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID, + new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] { + new RegularPermission(Manifest.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED) + }, true), + new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] { + new RegularPermission(Manifest.permission.SCHEDULE_EXACT_ALARM), + new RegularPermission(Manifest.permission.USE_EXACT_ALARM), + new AppOpPermission(AppOpsManager.OP_ACTIVATE_VPN), + new AppOpPermission(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN), + }, false) + ); + + /** + * The policy for the {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_SPECIAL_USE}. + * + * @hide + */ + public static final @NonNull ForegroundServiceTypePolicyInfo FGS_TYPE_POLICY_SPECIAL_USE = + new ForegroundServiceTypePolicyInfo( + FOREGROUND_SERVICE_TYPE_SPECIAL_USE, + ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID, + ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID, + new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] { + new RegularPermission(Manifest.permission.FOREGROUND_SERVICE_SPECIAL_USE) + }, true), + null + ); + + /** + * Foreground service policy check result code: this one is not actually being used. + * + * @hide + */ + public static final int FGS_TYPE_POLICY_CHECK_UNKNOWN = + AppProtoEnums.FGS_TYPE_POLICY_CHECK_UNKNOWN; + + /** + * Foreground service policy check result code: okay to go. + * + * @hide + */ + public static final int FGS_TYPE_POLICY_CHECK_OK = + AppProtoEnums.FGS_TYPE_POLICY_CHECK_OK; + + /** + * Foreground service policy check result code: this foreground service type is deprecated. + * + * @hide + */ + public static final int FGS_TYPE_POLICY_CHECK_DEPRECATED = + AppProtoEnums.FGS_TYPE_POLICY_CHECK_DEPRECATED; + + /** + * Foreground service policy check result code: this foreground service type is disabled. + * + * @hide + */ + public static final int FGS_TYPE_POLICY_CHECK_DISABLED = + AppProtoEnums.FGS_TYPE_POLICY_CHECK_DISABLED; + + /** + * Foreground service policy check result code: the caller doesn't have permission to start + * foreground service with this type, but the policy is permissive. + * + * @hide + */ + public static final int FGS_TYPE_POLICY_CHECK_PERMISSION_DENIED_PERMISSIVE = + AppProtoEnums.FGS_TYPE_POLICY_CHECK_PERMISSION_DENIED_PERMISSIVE; + + /** + * Foreground service policy check result code: the caller doesn't have permission to start + * foreground service with this type, and the policy is enforced. + * + * @hide + */ + public static final int FGS_TYPE_POLICY_CHECK_PERMISSION_DENIED_ENFORCED = + AppProtoEnums.FGS_TYPE_POLICY_CHECK_PERMISSION_DENIED_ENFORCED; + + /** + * @hide + */ + @IntDef(flag = true, prefix = { "FGS_TYPE_POLICY_CHECK_" }, value = { + FGS_TYPE_POLICY_CHECK_UNKNOWN, + FGS_TYPE_POLICY_CHECK_OK, + FGS_TYPE_POLICY_CHECK_DEPRECATED, + FGS_TYPE_POLICY_CHECK_DISABLED, + FGS_TYPE_POLICY_CHECK_PERMISSION_DENIED_PERMISSIVE, + FGS_TYPE_POLICY_CHECK_PERMISSION_DENIED_ENFORCED, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ForegroundServicePolicyCheckCode{} + + /** + * @return The policy info for the given type. + */ + @NonNull + public abstract ForegroundServiceTypePolicyInfo getForegroundServiceTypePolicyInfo( + @ForegroundServiceType int type, @ForegroundServiceType int defaultToType); + + /** + * Run check on the foreground service type policy for the given uid/pid + * + * @hide + */ + @ForegroundServicePolicyCheckCode + public abstract int checkForegroundServiceTypePolicy(@NonNull Context context, + @NonNull String packageName, int callerUid, int callerPid, boolean allowWhileInUse, + @NonNull ForegroundServiceTypePolicyInfo policy); + + @GuardedBy("sLock") + private static ForegroundServiceTypePolicy sDefaultForegroundServiceTypePolicy = null; + + private static final Object sLock = new Object(); + + /** + * Return the default policy for FGS type. + */ + public static @NonNull ForegroundServiceTypePolicy getDefaultPolicy() { + synchronized (sLock) { + if (sDefaultForegroundServiceTypePolicy == null) { + sDefaultForegroundServiceTypePolicy = new DefaultForegroundServiceTypePolicy(); + } + return sDefaultForegroundServiceTypePolicy; + } + } + + /** + * Constructor. + * + * @hide + */ + public ForegroundServiceTypePolicy() { + } + + /** + * This class represents the policy for a specific FGS service type. + * + * @hide + */ + public static final class ForegroundServiceTypePolicyInfo { + /** + * The foreground service type. + */ + final @ForegroundServiceType int mType; + + /** + * The change id to tell if this FGS type is deprecated. + * + * <p>A 0 indicates it's not deprecated.</p> + */ + final long mDeprecationChangeId; + + /** + * The change id to tell if this FGS type is disabled. + * + * <p>A 0 indicates it's not disabled.</p> + */ + final long mDisabledChangeId; + + /** + * The required permissions to start a foreground with this type, all of them + * MUST have been granted. + */ + final @Nullable ForegroundServiceTypePermissions mAllOfPermissions; + + /** + * The required permissions to start a foreground with this type, any one of them + * being granted is sufficient. + */ + final @Nullable ForegroundServiceTypePermissions mAnyOfPermissions; + + /** + * A customized check for the permissions. + */ + @Nullable ForegroundServiceTypePermission mCustomPermission; + + /** + * Not a real change id, but a place holder. + */ + private static final long INVALID_CHANGE_ID = 0L; + + /** + * @return {@code true} if the given change id is valid. + */ + private static boolean isValidChangeId(long changeId) { + return changeId != INVALID_CHANGE_ID; + } + + /** + * Construct a new instance. + * + * @hide + */ + public ForegroundServiceTypePolicyInfo(@ForegroundServiceType int type, + long deprecationChangeId, long disabledChangeId, + @Nullable ForegroundServiceTypePermissions allOfPermissions, + @Nullable ForegroundServiceTypePermissions anyOfPermissions) { + mType = type; + mDeprecationChangeId = deprecationChangeId; + mDisabledChangeId = disabledChangeId; + mAllOfPermissions = allOfPermissions; + mAnyOfPermissions = anyOfPermissions; + } + + /** + * @return The foreground service type. + */ + @ForegroundServiceType + public int getForegroundServiceType() { + return mType; + } + + @Override + public String toString() { + final StringBuilder sb = toPermissionString(new StringBuilder()); + sb.append("type=0x"); + sb.append(Integer.toHexString(mType)); + sb.append(" deprecationChangeId="); + sb.append(mDeprecationChangeId); + sb.append(" disabledChangeId="); + sb.append(mDisabledChangeId); + sb.append(" customPermission="); + sb.append(mCustomPermission); + return sb.toString(); + } + + /** + * @return The required permissions. + */ + public String toPermissionString() { + return toPermissionString(new StringBuilder()).toString(); + } + + private StringBuilder toPermissionString(StringBuilder sb) { + if (mAllOfPermissions != null) { + sb.append("all of the permissions "); + sb.append(mAllOfPermissions.toString()); + sb.append(' '); + } + if (mAnyOfPermissions != null) { + sb.append("any of the permissions "); + sb.append(mAnyOfPermissions.toString()); + sb.append(' '); + } + return sb; + } + + /** + * @hide + */ + public void setCustomPermission( + @Nullable ForegroundServiceTypePermission customPermission) { + mCustomPermission = customPermission; + } + + /** + * @return The name of the permissions which are all required. + * It may contain app op names. + * + * For test only. + */ + public @NonNull Optional<String[]> getRequiredAllOfPermissionsForTest() { + if (mAllOfPermissions == null) { + return Optional.empty(); + } + return Optional.of(mAllOfPermissions.toStringArray()); + } + + /** + * @return The name of the permissions where any of the is granted is sufficient. + * It may contain app op names. + * + * For test only. + */ + public @NonNull Optional<String[]> getRequiredAnyOfPermissionsForTest() { + if (mAnyOfPermissions == null) { + return Optional.empty(); + } + return Optional.of(mAnyOfPermissions.toStringArray()); + } + + /** + * Whether or not this type is disabled. + */ + @SuppressLint("AndroidFrameworkRequiresPermission") + public boolean isTypeDisabled(int callerUid) { + return isValidChangeId(mDisabledChangeId) + && CompatChanges.isChangeEnabled(mDisabledChangeId, callerUid); + } + + /** + * Override the type disabling change Id. + * + * For test only. + */ + public void setTypeDisabledForTest(boolean disabled, @NonNull String packageName) + throws RemoteException { + overrideChangeIdForTest(mDisabledChangeId, disabled, packageName); + } + + /** + * clear the type disabling change Id. + * + * For test only. + */ + public void clearTypeDisabledForTest(@NonNull String packageName) throws RemoteException { + clearOverrideForTest(mDisabledChangeId, packageName); + } + + @SuppressLint("AndroidFrameworkRequiresPermission") + boolean isTypeDeprecated(int callerUid) { + return isValidChangeId(mDeprecationChangeId) + && CompatChanges.isChangeEnabled(mDeprecationChangeId, callerUid); + } + + private void overrideChangeIdForTest(long changeId, boolean enable, String packageName) + throws RemoteException { + if (!isValidChangeId(changeId)) { + return; + } + final ArraySet<Long> enabled = new ArraySet<>(); + final ArraySet<Long> disabled = new ArraySet<>(); + if (enable) { + enabled.add(changeId); + } else { + disabled.add(changeId); + } + final CompatibilityChangeConfig overrides = new CompatibilityChangeConfig( + new Compatibility.ChangeConfig(enabled, disabled)); + IPlatformCompat platformCompat = IPlatformCompat.Stub.asInterface( + ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE)); + platformCompat.setOverridesForTest(overrides, packageName); + } + + private void clearOverrideForTest(long changeId, @NonNull String packageName) + throws RemoteException { + IPlatformCompat platformCompat = IPlatformCompat.Stub.asInterface( + ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE)); + platformCompat.clearOverrideForTest(changeId, packageName); + } + } + + /** + * This represents the set of permissions that's going to be required + * for a specific service type. + * + * @hide + */ + public static class ForegroundServiceTypePermissions { + /** + * The set of the permissions to be required. + */ + final @NonNull ForegroundServiceTypePermission[] mPermissions; + + /** + * Are we requiring all of the permissions to be granted or any of them. + */ + final boolean mAllOf; + + /** + * Constructor. + */ + public ForegroundServiceTypePermissions( + @NonNull ForegroundServiceTypePermission[] permissions, boolean allOf) { + mPermissions = permissions; + mAllOf = allOf; + } + + /** + * Check the permissions. + */ + @PackageManager.PermissionResult + public int checkPermissions(@NonNull Context context, int callerUid, int callerPid, + @NonNull String packageName, boolean allowWhileInUse) { + if (mAllOf) { + for (ForegroundServiceTypePermission perm : mPermissions) { + final int result = perm.checkPermission(context, callerUid, callerPid, + packageName, allowWhileInUse); + if (result != PERMISSION_GRANTED) { + return PERMISSION_DENIED; + } + } + return PERMISSION_GRANTED; + } else { + boolean anyOfGranted = false; + for (ForegroundServiceTypePermission perm : mPermissions) { + final int result = perm.checkPermission(context, callerUid, callerPid, + packageName, allowWhileInUse); + if (result == PERMISSION_GRANTED) { + anyOfGranted = true; + break; + } + } + return anyOfGranted ? PERMISSION_GRANTED : PERMISSION_DENIED; + } + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("allOf="); + sb.append(mAllOf); + sb.append(' '); + sb.append('['); + for (int i = 0; i < mPermissions.length; i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(mPermissions[i].toString()); + } + sb.append(']'); + return sb.toString(); + } + + @NonNull String[] toStringArray() { + final String[] names = new String[mPermissions.length]; + for (int i = 0; i < mPermissions.length; i++) { + names[i] = mPermissions[i].mName; + } + return names; + } + } + + /** + * This represents a permission that's going to be required for a specific service type. + * + * @hide + */ + public abstract static class ForegroundServiceTypePermission { + /** + * The name of this permission. + */ + final @NonNull String mName; + + /** + * Constructor. + */ + public ForegroundServiceTypePermission(@NonNull String name) { + mName = name; + } + + /** + * Check if the given uid/pid/package has the access to the permission. + */ + @PackageManager.PermissionResult + public abstract int checkPermission(@NonNull Context context, int callerUid, int callerPid, + @NonNull String packageName, boolean allowWhileInUse); + + @Override + public String toString() { + return mName; + } + } + + /** + * This represents a regular Android permission to be required for a specific service type. + */ + static class RegularPermission extends ForegroundServiceTypePermission { + RegularPermission(@NonNull String name) { + super(name); + } + + @Override + @SuppressLint("AndroidFrameworkRequiresPermission") + @PackageManager.PermissionResult + public int checkPermission(Context context, int callerUid, int callerPid, + String packageName, boolean allowWhileInUse) { + // Simple case, check if it's already granted. + if (context.checkPermission(mName, callerPid, callerUid) == PERMISSION_GRANTED) { + return PERMISSION_GRANTED; + } + if (allowWhileInUse) { + // Check its appops + final int opCode = AppOpsManager.permissionToOpCode(mName); + final AppOpsManager appOpsManager = context.getSystemService(AppOpsManager.class); + if (opCode != AppOpsManager.OP_NONE) { + final int currentMode = appOpsManager.unsafeCheckOpRawNoThrow(opCode, callerUid, + packageName); + if (currentMode == MODE_FOREGROUND) { + // It's in foreground only mode and we're allowing while-in-use. + return PERMISSION_GRANTED; + } + } + } + return PERMISSION_DENIED; + } + } + + /** + * This represents an app op permission to be required for a specific service type. + */ + static class AppOpPermission extends ForegroundServiceTypePermission { + final int mOpCode; + + AppOpPermission(int opCode) { + super(AppOpsManager.opToPublicName(opCode)); + mOpCode = opCode; + } + + @Override + @PackageManager.PermissionResult + public int checkPermission(Context context, int callerUid, int callerPid, + String packageName, boolean allowWhileInUse) { + final AppOpsManager appOpsManager = context.getSystemService(AppOpsManager.class); + final int mode = appOpsManager.unsafeCheckOpRawNoThrow(mOpCode, callerUid, packageName); + return (mode == MODE_ALLOWED || (allowWhileInUse && mode == MODE_FOREGROUND)) + ? PERMISSION_GRANTED : PERMISSION_DENIED; + } + } + + /** + * This represents a special Android permission to be required for accessing usb devices. + */ + static class UsbDevicePermission extends ForegroundServiceTypePermission { + UsbDevicePermission() { + super("USB Device"); + } + + @Override + @SuppressLint("AndroidFrameworkRequiresPermission") + @PackageManager.PermissionResult + public int checkPermission(Context context, int callerUid, int callerPid, + String packageName, boolean allowWhileInUse) { + final UsbManager usbManager = context.getSystemService(UsbManager.class); + final HashMap<String, UsbDevice> devices = usbManager.getDeviceList(); + if (!ArrayUtils.isEmpty(devices)) { + for (UsbDevice device : devices.values()) { + if (usbManager.hasPermission(device, packageName, callerPid, callerUid)) { + return PERMISSION_GRANTED; + } + } + } + return PERMISSION_DENIED; + } + } + + /** + * This represents a special Android permission to be required for accessing usb accessories. + */ + static class UsbAccessoryPermission extends ForegroundServiceTypePermission { + UsbAccessoryPermission() { + super("USB Accessory"); + } + + @Override + @SuppressLint("AndroidFrameworkRequiresPermission") + @PackageManager.PermissionResult + public int checkPermission(Context context, int callerUid, int callerPid, + String packageName, boolean allowWhileInUse) { + final UsbManager usbManager = context.getSystemService(UsbManager.class); + final UsbAccessory[] accessories = usbManager.getAccessoryList(); + if (!ArrayUtils.isEmpty(accessories)) { + for (UsbAccessory accessory: accessories) { + if (usbManager.hasPermission(accessory, callerPid, callerUid)) { + return PERMISSION_GRANTED; + } + } + } + return PERMISSION_DENIED; + } + } + + /** + * The default policy for the foreground service types. + * + * @hide + */ + public static class DefaultForegroundServiceTypePolicy extends ForegroundServiceTypePolicy { + private final SparseArray<ForegroundServiceTypePolicyInfo> mForegroundServiceTypePolicies = + new SparseArray<>(); + + /** + * Constructor + */ + public DefaultForegroundServiceTypePolicy() { + mForegroundServiceTypePolicies.put(FOREGROUND_SERVICE_TYPE_NONE, + FGS_TYPE_POLICY_NONE); + mForegroundServiceTypePolicies.put(FOREGROUND_SERVICE_TYPE_DATA_SYNC, + FGS_TYPE_POLICY_DATA_SYNC); + mForegroundServiceTypePolicies.put(FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK, + FGS_TYPE_POLICY_MEDIA_PLAYBACK); + mForegroundServiceTypePolicies.put(FOREGROUND_SERVICE_TYPE_PHONE_CALL, + FGS_TYPE_POLICY_PHONE_CALL); + mForegroundServiceTypePolicies.put(FOREGROUND_SERVICE_TYPE_LOCATION, + FGS_TYPE_POLICY_LOCATION); + mForegroundServiceTypePolicies.put(FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE, + FGS_TYPE_POLICY_CONNECTED_DEVICE); + mForegroundServiceTypePolicies.put(FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION, + FGS_TYPE_POLICY_MEDIA_PROJECTION); + mForegroundServiceTypePolicies.put(FOREGROUND_SERVICE_TYPE_CAMERA, + FGS_TYPE_POLICY_CAMERA); + mForegroundServiceTypePolicies.put(FOREGROUND_SERVICE_TYPE_MICROPHONE, + FGS_TYPE_POLICY_MICROPHONE); + mForegroundServiceTypePolicies.put(FOREGROUND_SERVICE_TYPE_HEALTH, + FGS_TYPE_POLICY_HEALTH); + mForegroundServiceTypePolicies.put(FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING, + FGS_TYPE_POLICY_REMOTE_MESSAGING); + mForegroundServiceTypePolicies.put(FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED, + FGS_TYPE_POLICY_SYSTEM_EXEMPTED); + mForegroundServiceTypePolicies.put(FOREGROUND_SERVICE_TYPE_SPECIAL_USE, + FGS_TYPE_POLICY_SPECIAL_USE); + } + + @Override + public ForegroundServiceTypePolicyInfo getForegroundServiceTypePolicyInfo( + @ForegroundServiceType int type, @ForegroundServiceType int defaultToType) { + ForegroundServiceTypePolicyInfo info = mForegroundServiceTypePolicies.get(type); + if (info == null) { + // Unknown type, fallback to the defaultToType + info = mForegroundServiceTypePolicies.get(defaultToType); + if (info == null) { + // It shouldn't happen. + throw new IllegalArgumentException("Invalid default fgs type " + defaultToType); + } + } + return info; + } + + @Override + @SuppressLint("AndroidFrameworkRequiresPermission") + @ForegroundServicePolicyCheckCode + public int checkForegroundServiceTypePolicy(Context context, String packageName, + int callerUid, int callerPid, boolean allowWhileInUse, + @NonNull ForegroundServiceTypePolicyInfo policy) { + // Has this FGS type been disabled and not allowed to use anymore? + if (policy.isTypeDisabled(callerUid)) { + return FGS_TYPE_POLICY_CHECK_DISABLED; + } + int permissionResult = PERMISSION_DENIED; + // Do we have the permission to start FGS with this type. + if (policy.mAllOfPermissions != null) { + permissionResult = policy.mAllOfPermissions.checkPermissions(context, + callerUid, callerPid, packageName, allowWhileInUse); + } + // If it has the "all of" permissions granted, check the "any of" ones. + if (permissionResult == PERMISSION_GRANTED) { + boolean checkCustomPermission = true; + // Check the "any of" permissions. + if (policy.mAnyOfPermissions != null) { + permissionResult = policy.mAnyOfPermissions.checkPermissions(context, + callerUid, callerPid, packageName, allowWhileInUse); + if (permissionResult == PERMISSION_GRANTED) { + // We have one of them granted, no need to check custom permissions. + checkCustomPermission = false; + } + } + // If we have a customized permission checker, also call it now. + if (checkCustomPermission && policy.mCustomPermission != null) { + permissionResult = policy.mCustomPermission.checkPermission(context, + callerUid, callerPid, packageName, allowWhileInUse); + } + } + if (permissionResult != PERMISSION_GRANTED) { + return (CompatChanges.isChangeEnabled( + FGS_TYPE_PERMISSION_CHANGE_ID, callerUid)) + ? FGS_TYPE_POLICY_CHECK_PERMISSION_DENIED_ENFORCED + : FGS_TYPE_POLICY_CHECK_PERMISSION_DENIED_PERMISSIVE; + } + // Has this FGS type been deprecated? + if (policy.isTypeDeprecated(callerUid)) { + return FGS_TYPE_POLICY_CHECK_DEPRECATED; + } + return FGS_TYPE_POLICY_CHECK_OK; + } + } +} diff --git a/core/java/android/app/Service.java b/core/java/android/app/Service.java index 7635138f6cdd..01e4b150fd7c 100644 --- a/core/java/android/app/Service.java +++ b/core/java/android/app/Service.java @@ -21,6 +21,7 @@ import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.compat.annotation.UnsupportedAppUsage; import android.content.ComponentCallbacks2; import android.content.ComponentName; @@ -726,10 +727,32 @@ public abstract class Service extends ContextWrapper implements ComponentCallbac * for more details. * </div> * + * <div class="caution"> + * <p><strong>Note:</strong> + * Beginning with SDK Version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, + * apps targeting SDK Version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} + * or higher are not allowed to start foreground services without specifying a valid + * foreground service type in the manifest attribute + * {@link android.R.attr#foregroundServiceType}. + * See + * <a href="{@docRoot}/about/versions/14/behavior-changes-14"> + * Behavior changes: Apps targeting Android 14 + * </a> + * for more details. + * </div> + * * @throws ForegroundServiceStartNotAllowedException * If the app targeting API is * {@link android.os.Build.VERSION_CODES#S} or later, and the service is restricted from * becoming foreground service due to background restriction. + * @throws ForegroundServiceTypeNotAllowedException + * If the app targeting API is + * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} or later, and the manifest attribute + * {@link android.R.attr#foregroundServiceType} is not set. + * @throws SecurityException If the app targeting API is + * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} or later and doesn't have the + * permission to start the foreground service with the specified type in the manifest attribute + * {@link android.R.attr#foregroundServiceType}. * * @param id The identifier for this notification as per * {@link NotificationManager#notify(int, Notification) @@ -748,51 +771,77 @@ public abstract class Service extends ContextWrapper implements ComponentCallbac } } - /** - * An overloaded version of {@link #startForeground(int, Notification)} with additional - * foregroundServiceType parameter. - * - * <p>Apps built with SDK version {@link android.os.Build.VERSION_CODES#Q} or later can specify - * the foreground service types using attribute {@link android.R.attr#foregroundServiceType} in - * service element of manifest file. The value of attribute - * {@link android.R.attr#foregroundServiceType} can be multiple flags ORed together.</p> - * - * <p>The foregroundServiceType parameter must be a subset flags of what is specified in manifest - * attribute {@link android.R.attr#foregroundServiceType}, if not, an IllegalArgumentException is - * thrown. Specify foregroundServiceType parameter as - * {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_MANIFEST} to use all flags that - * is specified in manifest attribute foregroundServiceType.</p> - * - * <div class="caution"> - * <p><strong>Note:</strong> - * Beginning with SDK Version {@link android.os.Build.VERSION_CODES#S}, - * apps targeting SDK Version {@link android.os.Build.VERSION_CODES#S} - * or higher are not allowed to start foreground services from the background. - * See - * <a href="{@docRoot}/about/versions/12/behavior-changes-12"> - * Behavior changes: Apps targeting Android 12 - * </a> - * for more details. - * </div> - * - * @param id The identifier for this notification as per - * {@link NotificationManager#notify(int, Notification) - * NotificationManager.notify(int, Notification)}; must not be 0. - * @param notification The Notification to be displayed. - * @param foregroundServiceType must be a subset flags of manifest attribute - * {@link android.R.attr#foregroundServiceType} flags. - * - * @throws IllegalArgumentException if param foregroundServiceType is not subset of manifest - * attribute {@link android.R.attr#foregroundServiceType}. - * @throws ForegroundServiceStartNotAllowedException - * If the app targeting API is - * {@link android.os.Build.VERSION_CODES#S} or later, and the service is restricted from - * becoming foreground service due to background restriction. - * - * @see android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_MANIFEST - */ + /** + * An overloaded version of {@link #startForeground(int, Notification)} with additional + * foregroundServiceType parameter. + * + * <p>Apps built with SDK version {@link android.os.Build.VERSION_CODES#Q} or later can specify + * the foreground service types using attribute {@link android.R.attr#foregroundServiceType} in + * service element of manifest file. The value of attribute + * {@link android.R.attr#foregroundServiceType} can be multiple flags ORed together.</p> + * + * <p>The foregroundServiceType parameter must be a subset flags of what is specified in + * manifest attribute {@link android.R.attr#foregroundServiceType}, if not, an + * IllegalArgumentException is thrown. Specify foregroundServiceType parameter as + * {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_MANIFEST} to use all flags that + * is specified in manifest attribute foregroundServiceType.</p> + * + * <div class="caution"> + * <p><strong>Note:</strong> + * Beginning with SDK Version {@link android.os.Build.VERSION_CODES#S}, + * apps targeting SDK Version {@link android.os.Build.VERSION_CODES#S} + * or higher are not allowed to start foreground services from the background. + * See + * <a href="{@docRoot}/about/versions/12/behavior-changes-12"> + * Behavior changes: Apps targeting Android 12 + * </a> + * for more details. + * </div> + * + * <div class="caution"> + * <p><strong>Note:</strong> + * Beginning with SDK Version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, + * apps targeting SDK Version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} + * or higher are not allowed to start foreground services without specifying a valid + * foreground service type in the manifest attribute + * {@link android.R.attr#foregroundServiceType}, and the parameter {@code foregroundServiceType} + * here must not be the {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_NONE}. + * See + * <a href="{@docRoot}/about/versions/14/behavior-changes-14"> + * Behavior changes: Apps targeting Android 14 + * </a> + * for more details. + * </div> + * + * @param id The identifier for this notification as per + * {@link NotificationManager#notify(int, Notification) + * NotificationManager.notify(int, Notification)}; must not be 0. + * @param notification The Notification to be displayed. + * @param foregroundServiceType must be a subset flags of manifest attribute + * {@link android.R.attr#foregroundServiceType} flags; must not be + * {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_NONE}. + * + * @throws IllegalArgumentException if param foregroundServiceType is not subset of manifest + * attribute {@link android.R.attr#foregroundServiceType}. + * @throws ForegroundServiceStartNotAllowedException + * If the app targeting API is + * {@link android.os.Build.VERSION_CODES#S} or later, and the service is restricted from + * becoming foreground service due to background restriction. + * @throws ForegroundServiceTypeNotAllowedException + * If the app targeting API is + * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} or later, and the manifest attribute + * {@link android.R.attr#foregroundServiceType} is not set, or the param + * {@code foregroundServiceType} is {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_NONE}. + * @throws SecurityException If the app targeting API is + * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} or later and doesn't have the + * permission to start the foreground service with the specified type in + * {@code foregroundServiceType}. + * {@link android.R.attr#foregroundServiceType}. + * + * @see android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_MANIFEST + */ public final void startForeground(int id, @NonNull Notification notification, - @ForegroundServiceType int foregroundServiceType) { + @RequiresPermission @ForegroundServiceType int foregroundServiceType) { try { mActivityManager.setServiceForeground( new ComponentName(this, mClassName), mToken, id, diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index f4cee5a09ff4..6fedb41884ec 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -171,6 +171,7 @@ public class DevicePolicyManager { private final boolean mParentInstance; private final DevicePolicyResourcesManager mResourcesManager; + /** @hide */ public DevicePolicyManager(Context context, IDevicePolicyManager service) { this(context, service, false); @@ -6207,46 +6208,46 @@ public class DevicePolicyManager { 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. + * Ask that all user data be wiped. + * + * <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> - * 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. + * 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 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. + * 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 @@ public class DevicePolicyManager { * @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) + */ + // 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) { + 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 class DevicePolicyManager { 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 75bfc2554442..6c27dd7b771b 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -117,7 +117,10 @@ interface IDevicePolicyManager { 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 @@ interface IDevicePolicyManager { 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/app/backup/BackupRestoreEventLogger.java b/core/java/android/app/backup/BackupRestoreEventLogger.java index 760c6f0fc333..6f62c8a03078 100644 --- a/core/java/android/app/backup/BackupRestoreEventLogger.java +++ b/core/java/android/app/backup/BackupRestoreEventLogger.java @@ -53,6 +53,8 @@ public class BackupRestoreEventLogger { /** * Operation types for which this logger can be used. + * + * @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef({ @@ -87,6 +89,8 @@ public class BackupRestoreEventLogger { * {@link OperationType}. Attempts to use logging methods that don't match * the specified operation type will be rejected (e.g. use backup methods * for a restore logger and vice versa). + * + * @hide */ public BackupRestoreEventLogger(@OperationType int operationType) { mOperationType = operationType; @@ -111,11 +115,9 @@ public class BackupRestoreEventLogger { * * @param dataType the type of data being backed. * @param count number of items of the given type that have been successfully backed up. - * - * @return boolean, indicating whether the log has been accepted. */ - public boolean logItemsBackedUp(@NonNull @BackupRestoreDataType String dataType, int count) { - return logSuccess(OperationType.BACKUP, dataType, count); + public void logItemsBackedUp(@NonNull @BackupRestoreDataType String dataType, int count) { + logSuccess(OperationType.BACKUP, dataType, count); } /** @@ -130,12 +132,10 @@ public class BackupRestoreEventLogger { * @param dataType the type of data being backed. * @param count number of items of the given type that have failed to back up. * @param error optional, the error that has caused the failure. - * - * @return boolean, indicating whether the log has been accepted. */ - public boolean logItemsBackupFailed(@NonNull @BackupRestoreDataType String dataType, int count, + public void logItemsBackupFailed(@NonNull @BackupRestoreDataType String dataType, int count, @Nullable @BackupRestoreError String error) { - return logFailure(OperationType.BACKUP, dataType, count, error); + logFailure(OperationType.BACKUP, dataType, count, error); } /** @@ -151,12 +151,10 @@ public class BackupRestoreEventLogger { * * @param dataType the type of data being backed up. * @param metaData the metadata associated with the data type. - * - * @return boolean, indicating whether the log has been accepted. */ - public boolean logBackupMetaData(@NonNull @BackupRestoreDataType String dataType, + public void logBackupMetaData(@NonNull @BackupRestoreDataType String dataType, @NonNull String metaData) { - return logMetaData(OperationType.BACKUP, dataType, metaData); + logMetaData(OperationType.BACKUP, dataType, metaData); } /** @@ -172,11 +170,9 @@ public class BackupRestoreEventLogger { * * @param dataType the type of data being restored. * @param count number of items of the given type that have been successfully restored. - * - * @return boolean, indicating whether the log has been accepted. */ - public boolean logItemsRestored(@NonNull @BackupRestoreDataType String dataType, int count) { - return logSuccess(OperationType.RESTORE, dataType, count); + public void logItemsRestored(@NonNull @BackupRestoreDataType String dataType, int count) { + logSuccess(OperationType.RESTORE, dataType, count); } /** @@ -193,12 +189,10 @@ public class BackupRestoreEventLogger { * @param dataType the type of data being restored. * @param count number of items of the given type that have failed to restore. * @param error optional, the error that has caused the failure. - * - * @return boolean, indicating whether the log has been accepted. */ - public boolean logItemsRestoreFailed(@NonNull @BackupRestoreDataType String dataType, int count, + public void logItemsRestoreFailed(@NonNull @BackupRestoreDataType String dataType, int count, @Nullable @BackupRestoreError String error) { - return logFailure(OperationType.RESTORE, dataType, count, error); + logFailure(OperationType.RESTORE, dataType, count, error); } /** @@ -216,12 +210,10 @@ public class BackupRestoreEventLogger { * * @param dataType the type of data being restored. * @param metadata the metadata associated with the data type. - * - * @return boolean, indicating whether the log has been accepted. */ - public boolean logRestoreMetadata(@NonNull @BackupRestoreDataType String dataType, + public void logRestoreMetadata(@NonNull @BackupRestoreDataType String dataType, @NonNull String metadata) { - return logMetaData(OperationType.RESTORE, dataType, metadata); + logMetaData(OperationType.RESTORE, dataType, metadata); } /** @@ -240,52 +232,47 @@ public class BackupRestoreEventLogger { * * @hide */ - public @OperationType int getOperationType() { + @OperationType + public int getOperationType() { return mOperationType; } - private boolean logSuccess(@OperationType int operationType, + private void logSuccess(@OperationType int operationType, @BackupRestoreDataType String dataType, int count) { DataTypeResult dataTypeResult = getDataTypeResult(operationType, dataType); if (dataTypeResult == null) { - return false; + return; } dataTypeResult.mSuccessCount += count; mResults.put(dataType, dataTypeResult); - - return true; } - private boolean logFailure(@OperationType int operationType, + private void logFailure(@OperationType int operationType, @NonNull @BackupRestoreDataType String dataType, int count, @Nullable @BackupRestoreError String error) { DataTypeResult dataTypeResult = getDataTypeResult(operationType, dataType); if (dataTypeResult == null) { - return false; + return; } dataTypeResult.mFailCount += count; if (error != null) { dataTypeResult.mErrors.merge(error, count, Integer::sum); } - - return true; } - private boolean logMetaData(@OperationType int operationType, + private void logMetaData(@OperationType int operationType, @NonNull @BackupRestoreDataType String dataType, @NonNull String metaData) { if (mHashDigest == null) { - return false; + return; } DataTypeResult dataTypeResult = getDataTypeResult(operationType, dataType); if (dataTypeResult == null) { - return false; + return; } dataTypeResult.mMetadataHash = getMetaDataHash(metaData); - - return true; } /** diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 1df0fa8084a5..ae1f68958d0f 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -4917,6 +4917,7 @@ public abstract class Context { * @see android.server.attention.AttentionManagerService * @hide */ + @TestApi public static final String ATTENTION_SERVICE = "attention"; /** diff --git a/core/java/android/content/IntentFilter.java b/core/java/android/content/IntentFilter.java index b3435b1180c2..8b6c4dd8497b 100644 --- a/core/java/android/content/IntentFilter.java +++ b/core/java/android/content/IntentFilter.java @@ -28,6 +28,7 @@ import android.os.Parcelable; import android.os.PatternMatcher; import android.text.TextUtils; import android.util.AndroidException; +import android.util.ArraySet; import android.util.Log; import android.util.Printer; import android.util.proto.ProtoOutputStream; @@ -302,7 +303,7 @@ public class IntentFilter implements Parcelable { @UnsupportedAppUsage private int mOrder; @UnsupportedAppUsage - private final ArrayList<String> mActions; + private final ArraySet<String> mActions; private ArrayList<String> mCategories = null; private ArrayList<String> mDataSchemes = null; private ArrayList<PatternMatcher> mDataSchemeSpecificParts = null; @@ -433,7 +434,7 @@ public class IntentFilter implements Parcelable { */ public IntentFilter() { mPriority = 0; - mActions = new ArrayList<String>(); + mActions = new ArraySet<>(); } /** @@ -445,7 +446,7 @@ public class IntentFilter implements Parcelable { */ public IntentFilter(String action) { mPriority = 0; - mActions = new ArrayList<String>(); + mActions = new ArraySet<>(); addAction(action); } @@ -468,7 +469,7 @@ public class IntentFilter implements Parcelable { public IntentFilter(String action, String dataType) throws MalformedMimeTypeException { mPriority = 0; - mActions = new ArrayList<String>(); + mActions = new ArraySet<>(); addAction(action); addDataType(dataType); } @@ -481,7 +482,7 @@ public class IntentFilter implements Parcelable { public IntentFilter(IntentFilter o) { mPriority = o.mPriority; mOrder = o.mOrder; - mActions = new ArrayList<String>(o.mActions); + mActions = new ArraySet<>(o.mActions); if (o.mCategories != null) { mCategories = new ArrayList<String>(o.mCategories); } @@ -742,9 +743,7 @@ public class IntentFilter implements Parcelable { * @param action Name of the action to match, such as Intent.ACTION_VIEW. */ public final void addAction(String action) { - if (!mActions.contains(action)) { - mActions.add(action.intern()); - } + mActions.add(action.intern()); } /** @@ -758,7 +757,7 @@ public class IntentFilter implements Parcelable { * Return an action in the filter. */ public final String getAction(int index) { - return mActions.get(index); + return mActions.valueAt(index); } /** @@ -797,8 +796,11 @@ public class IntentFilter implements Parcelable { if (ignoreActions == null) { return !mActions.isEmpty(); } + if (mActions.size() > ignoreActions.size()) { + return true; // some actions are definitely not ignored + } for (int i = mActions.size() - 1; i >= 0; i--) { - if (!ignoreActions.contains(mActions.get(i))) { + if (!ignoreActions.contains(mActions.valueAt(i))) { return true; } } @@ -1918,7 +1920,7 @@ public class IntentFilter implements Parcelable { int N = countActions(); for (int i=0; i<N; i++) { serializer.startTag(null, ACTION_STR); - serializer.attribute(null, NAME_STR, mActions.get(i)); + serializer.attribute(null, NAME_STR, mActions.valueAt(i)); serializer.endTag(null, ACTION_STR); } N = countCategories(); @@ -2313,7 +2315,7 @@ public class IntentFilter implements Parcelable { } public final void writeToParcel(Parcel dest, int flags) { - dest.writeStringList(mActions); + dest.writeStringArray(mActions.toArray(new String[mActions.size()])); if (mCategories != null) { dest.writeInt(1); dest.writeStringList(mCategories); @@ -2407,8 +2409,9 @@ public class IntentFilter implements Parcelable { /** @hide */ public IntentFilter(Parcel source) { - mActions = new ArrayList<String>(); - source.readStringList(mActions); + List<String> actions = new ArrayList<>(); + source.readStringList(actions); + mActions = new ArraySet<>(actions); if (source.readInt() != 0) { mCategories = new ArrayList<String>(); source.readStringList(mCategories); diff --git a/core/java/android/content/om/FabricatedOverlay.java b/core/java/android/content/om/FabricatedOverlay.java index 3ca056097c1f..dbefa65f5c68 100644 --- a/core/java/android/content/om/FabricatedOverlay.java +++ b/core/java/android/content/om/FabricatedOverlay.java @@ -20,6 +20,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.os.FabricatedOverlayInternal; import android.os.FabricatedOverlayInternalEntry; +import android.os.ParcelFileDescriptor; import android.text.TextUtils; import com.android.internal.util.Preconditions; @@ -169,6 +170,24 @@ public class FabricatedOverlay { return this; } + /** + * Sets the value of the fabricated overlay + * + * @param resourceName name of the target resource to overlay (in the form + * [package]:type/entry) + * @param value the file descriptor whose contents are the value of the frro + * @param configuration The string representation of the config this overlay is enabled for + */ + public Builder setResourceValue(@NonNull String resourceName, ParcelFileDescriptor value, + String configuration) { + final FabricatedOverlayInternalEntry entry = new FabricatedOverlayInternalEntry(); + entry.resourceName = resourceName; + entry.binaryData = value; + entry.configuration = configuration; + mEntries.add(entry); + return this; + } + /** Builds an immutable fabricated overlay. */ public FabricatedOverlay build() { final FabricatedOverlayInternal overlay = new FabricatedOverlayInternal(); diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index aa86af92df87..103b3f18a33e 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -165,6 +165,21 @@ public abstract class PackageManager { "android.internal.PROPERTY_NO_APP_DATA_STORAGE"; /** + * <service> level {@link android.content.pm.PackageManager.Property} tag specifying + * the actual use case of the service if it's foreground service with the type + * {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_SPECIAL_USE}. + * + * <p> + * For example: + * <service> + * <property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" + * android:value="foo"/> + * </service> + */ + public static final String PROPERTY_SPECIAL_USE_FGS_SUBTYPE = + "android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"; + + /** * A property value set within the manifest. * <p> * The value of a property will only have a single type, as defined by diff --git a/core/java/android/content/pm/ServiceInfo.java b/core/java/android/content/pm/ServiceInfo.java index 88d700432e8a..ab20b6f7f9cc 100644 --- a/core/java/android/content/pm/ServiceInfo.java +++ b/core/java/android/content/pm/ServiceInfo.java @@ -16,7 +16,9 @@ package android.content.pm; +import android.Manifest; import android.annotation.IntDef; +import android.annotation.RequiresPermission; import android.os.Parcel; import android.os.Parcelable; import android.util.Printer; @@ -100,7 +102,15 @@ public class ServiceInfo extends ComponentInfo /** * The default foreground service type if not been set in manifest file. + * + * <p>Apps targeting API level {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and + * later should NOT use this type, + * calling {@link android.app.Service#startForeground(int, android.app.Notification, int)} with + * this type will get a {@link android.app.ForegroundServiceTypeNotAllowedException}.</p> + * + * @deprecated Do not use. */ + @Deprecated public static final int FOREGROUND_SERVICE_TYPE_NONE = 0; /** @@ -108,14 +118,36 @@ public class ServiceInfo extends ComponentInfo * the {@link android.R.attr#foregroundServiceType} attribute. * Data(photo, file, account) upload/download, backup/restore, import/export, fetch, * transfer over network between device and cloud. + * + * <p>Apps targeting API level {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and + * later should NOT use this type: + * calling {@link android.app.Service#startForeground(int, android.app.Notification, int)} with + * this type on devices running {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} is still + * allowed, but calling it with this type on devices running future platform releases may get a + * {@link android.app.ForegroundServiceTypeNotAllowedException}.</p> + * + * @deprecated Use {@link android.app.job.JobInfo.Builder} data transfer APIs instead. */ + @RequiresPermission( + value = Manifest.permission.FOREGROUND_SERVICE_DATA_SYNC, + conditional = true + ) + @Deprecated public static final int FOREGROUND_SERVICE_TYPE_DATA_SYNC = 1 << 0; /** * Constant corresponding to <code>mediaPlayback</code> in * the {@link android.R.attr#foregroundServiceType} attribute. * Music, video, news or other media playback. + * + * <p>Starting foreground service with this type from apps targeting API level + * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and later, will require permission + * {@link android.Manifest.permission#FOREGROUND_SERVICE_MEDIA_PLAYBACK}. */ + @RequiresPermission( + value = Manifest.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK, + conditional = true + ) public static final int FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK = 1 << 1; /** @@ -123,28 +155,94 @@ public class ServiceInfo extends ComponentInfo * the {@link android.R.attr#foregroundServiceType} attribute. * Ongoing operations related to phone calls, video conferencing, * or similar interactive communication. + * + * <p>Starting foreground service with this type from apps targeting API level + * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and later, will require permission + * {@link android.Manifest.permission#FOREGROUND_SERVICE_PHONE_CALL} and + * {@link android.Manifest.permission#MANAGE_OWN_CALLS}. */ + @RequiresPermission( + allOf = { + Manifest.permission.FOREGROUND_SERVICE_PHONE_CALL, + }, + anyOf = { + Manifest.permission.MANAGE_OWN_CALLS, + }, + conditional = true + ) public static final int FOREGROUND_SERVICE_TYPE_PHONE_CALL = 1 << 2; /** * Constant corresponding to <code>location</code> in * the {@link android.R.attr#foregroundServiceType} attribute. * GPS, map, navigation location update. + * + * <p>Starting foreground service with this type from apps targeting API level + * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and later, will require permission + * {@link android.Manifest.permission#FOREGROUND_SERVICE_LOCATION} and one of the + * following permissions: + * {@link android.Manifest.permission#ACCESS_COARSE_LOCATION}, + * {@link android.Manifest.permission#ACCESS_FINE_LOCATION}. */ + @RequiresPermission( + allOf = { + Manifest.permission.FOREGROUND_SERVICE_LOCATION, + }, + anyOf = { + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION, + }, + conditional = true + ) public static final int FOREGROUND_SERVICE_TYPE_LOCATION = 1 << 3; /** * Constant corresponding to <code>connectedDevice</code> in * the {@link android.R.attr#foregroundServiceType} attribute. * Auto, bluetooth, TV or other devices connection, monitoring and interaction. + * + * <p>Starting foreground service with this type from apps targeting API level + * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and later, will require permission + * {@link android.Manifest.permission#FOREGROUND_SERVICE_CONNECTED_DEVICE} and one of the + * following permissions: + * {@link android.Manifest.permission#BLUETOOTH_CONNECT}, + * {@link android.Manifest.permission#CHANGE_NETWORK_STATE}, + * {@link android.Manifest.permission#CHANGE_WIFI_STATE}, + * {@link android.Manifest.permission#CHANGE_WIFI_MULTICAST_STATE}, + * {@link android.Manifest.permission#NFC}, + * {@link android.Manifest.permission#TRANSMIT_IR}, + * or has been granted the access to one of the attached USB devices/accessories. */ + @RequiresPermission( + allOf = { + Manifest.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE, + }, + anyOf = { + Manifest.permission.BLUETOOTH_CONNECT, + Manifest.permission.CHANGE_NETWORK_STATE, + Manifest.permission.CHANGE_WIFI_STATE, + Manifest.permission.CHANGE_WIFI_MULTICAST_STATE, + Manifest.permission.NFC, + Manifest.permission.TRANSMIT_IR, + }, + conditional = true + ) public static final int FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE = 1 << 4; /** * Constant corresponding to {@code mediaProjection} in * the {@link android.R.attr#foregroundServiceType} attribute. * Managing a media projection session, e.g for screen recording or taking screenshots. + * + * <p>Starting foreground service with this type from apps targeting API level + * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and later, will require permission + * {@link android.Manifest.permission#FOREGROUND_SERVICE_MEDIA_PROJECTION}, and the user must + * have allowed the screen capture request from this app. */ + @RequiresPermission( + value = Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION, + conditional = true + ) public static final int FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION = 1 << 5; /** @@ -155,7 +253,21 @@ public class ServiceInfo extends ComponentInfo * above, a foreground service will not be able to access the camera if this type is not * specified in the manifest and in * {@link android.app.Service#startForeground(int, android.app.Notification, int)}. + * + * <p>Starting foreground service with this type from apps targeting API level + * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and later, will require permission + * {@link android.Manifest.permission#FOREGROUND_SERVICE_CAMERA} and + * {@link android.Manifest.permission#CAMERA}. */ + @RequiresPermission( + allOf = { + Manifest.permission.FOREGROUND_SERVICE_CAMERA, + }, + anyOf = { + Manifest.permission.CAMERA, + }, + conditional = true + ) public static final int FOREGROUND_SERVICE_TYPE_CAMERA = 1 << 6; /** @@ -166,17 +278,148 @@ public class ServiceInfo extends ComponentInfo * above, a foreground service will not be able to access the microphone if this type is not * specified in the manifest and in * {@link android.app.Service#startForeground(int, android.app.Notification, int)}. + * + * <p>Starting foreground service with this type from apps targeting API level + * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and later, will require permission + * {@link android.Manifest.permission#FOREGROUND_SERVICE_MICROPHONE} and one of the following + * permissions: + * {@link android.Manifest.permission#CAPTURE_AUDIO_OUTPUT}, + * {@link android.Manifest.permission#RECORD_AUDIO}. */ + @RequiresPermission( + allOf = { + Manifest.permission.FOREGROUND_SERVICE_MICROPHONE, + }, + anyOf = { + Manifest.permission.CAPTURE_AUDIO_OUTPUT, + Manifest.permission.RECORD_AUDIO, + }, + conditional = true + ) public static final int FOREGROUND_SERVICE_TYPE_MICROPHONE = 1 << 7; /** - * The number of foreground service types, this doesn't include - * the {@link #FOREGROUND_SERVICE_TYPE_MANIFEST} and {@link #FOREGROUND_SERVICE_TYPE_NONE} - * as they're not real service types. + * Constant corresponding to {@code health} in + * the {@link android.R.attr#foregroundServiceType} attribute. + * Health, wellness and fitness. + * + * <p>The caller app is required to have the permissions + * {@link android.Manifest.permission#FOREGROUND_SERVICE_HEALTH} and one of the following + * permissions: + * {@link android.Manifest.permission#ACTIVITY_RECOGNITION}, + * {@link android.Manifest.permission#BODY_SENSORS}, + * {@link android.Manifest.permission#HIGH_SAMPLING_RATE_SENSORS}. + */ + @RequiresPermission( + allOf = { + Manifest.permission.FOREGROUND_SERVICE_HEALTH, + }, + anyOf = { + Manifest.permission.ACTIVITY_RECOGNITION, + Manifest.permission.BODY_SENSORS, + Manifest.permission.HIGH_SAMPLING_RATE_SENSORS, + }, + conditional = true + ) + public static final int FOREGROUND_SERVICE_TYPE_HEALTH = 1 << 8; + + /** + * Constant corresponding to {@code remoteMessaging} in + * the {@link android.R.attr#foregroundServiceType} attribute. + * Messaging use cases which host local server to relay messages across devices. + */ + @RequiresPermission( + value = Manifest.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING, + conditional = true + ) + public static final int FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING = 1 << 9; + + /** + * Constant corresponding to {@code systemExempted} in + * the {@link android.R.attr#foregroundServiceType} attribute. + * The system exmpted foreground service use cases. + * + * <p class="note">Note, apps are allowed to use this type only in the following cases: + * <ul> + * <li>App has a UID < {@link android.os.Process#FIRST_APPLICATION_UID}</li> + * <li>App is on Doze allowlist</li> + * <li>Device is running in <a href="https://android.googlesource.com/platform/frameworks/base/+/master/packages/SystemUI/docs/demo_mode.md">Demo Mode</a></li> + * <li><a href="https://source.android.com/devices/tech/admin/provision">Device owner app</a><li> + * <li><a href="https://source.android.com/devices/tech/admin/managed-profiles">Profile owner apps</a><li> + * <li>Persistent apps</li> + * <li><a href="https://source.android.com/docs/core/connect/carrier">Carrier privileged apps</a></li> + * <li>Apps that have the {@code android.app.role.RoleManager#ROLE_EMERGENCY} role</li> + * <li>Headless system apps</li> + * <li><a href="{@docRoot}guide/topics/admin/device-admin">Device admin apps</a></li> + * <li>Active VPN apps</li> + * <li>Apps holding {@link Manifest.permission#SCHEDULE_EXACT_ALARM} or + * {@link Manifest.permission#USE_EXACT_ALARM} permission.</li> + * </ul> + * </p> + */ + @RequiresPermission( + value = Manifest.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED, + conditional = true + ) + public static final int FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED = 1 << 10; + + /** + * Constant corresponding to {@code specialUse} in + * the {@link android.R.attr#foregroundServiceType} attribute. + * Use cases that can't be categorized into any other foreground service types, but also + * can't use {@link android.app.job.JobInfo.Builder} APIs. + * + * <p>The use of this foreground service type may be restricted. Additionally, apps must declare + * a service-level {@link PackageManager#PROPERTY_SPECIAL_USE_FGS_SUBTYPE <property>} in + * {@code AndroidManifest.xml} as a hint of what the exact use case here is. + * Here is an example: + * <pre> + * <uses-permission + * android:name="android.permissions.FOREGROUND_SERVICE_SPECIAL_USE" + * /> + * <service + * android:name=".MySpecialForegroundService" + * android:foregroundServiceType="specialUse"> + * <property + * android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" + * android:value="foo" + * /> + * </service> + * </pre> + * + * In a future release of Android, if the above foreground service type {@code foo} is supported + * by the platform, to offer the backward compatibility, the app could specify + * the {@code android:maxSdkVersion} attribute in the <uses-permission> section, + * and also add the foreground service type {@code foo} into + * the {@code android:foregroundServiceType}, therefore the same app could be installed + * in both platforms. + * <pre> + * <uses-permission + * android:name="android.permissions.FOREGROUND_SERVICE_SPECIAL_USE" + * android:maxSdkVersion="last_sdk_version_without_type_foo" + * /> + * <service + * android:name=".MySpecialForegroundService" + * android:foregroundServiceType="specialUse|foo"> + * <property + * android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"" + * android:value="foo" + * /> + * </service> + * </pre> + */ + @RequiresPermission( + value = Manifest.permission.FOREGROUND_SERVICE_SPECIAL_USE, + conditional = true + ) + public static final int FOREGROUND_SERVICE_TYPE_SPECIAL_USE = 1 << 30; + + /** + * The max index being used in the definition of foreground service types. * * @hide */ - public static final int NUM_OF_FOREGROUND_SERVICE_TYPES = 8; + public static final int FOREGROUND_SERVICE_TYPES_MAX_INDEX = 30; /** * A special value indicates to use all types set in manifest file. @@ -199,7 +442,11 @@ public class ServiceInfo extends ComponentInfo FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE, FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION, FOREGROUND_SERVICE_TYPE_CAMERA, - FOREGROUND_SERVICE_TYPE_MICROPHONE + FOREGROUND_SERVICE_TYPE_MICROPHONE, + FOREGROUND_SERVICE_TYPE_HEALTH, + FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING, + FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED, + FOREGROUND_SERVICE_TYPE_SPECIAL_USE }) @Retention(RetentionPolicy.SOURCE) public @interface ForegroundServiceType {} @@ -275,6 +522,14 @@ public class ServiceInfo extends ComponentInfo return "camera"; case FOREGROUND_SERVICE_TYPE_MICROPHONE: return "microphone"; + case FOREGROUND_SERVICE_TYPE_HEALTH: + return "health"; + case FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING: + return "remoteMessaging"; + case FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED: + return "systemExempted"; + case FOREGROUND_SERVICE_TYPE_SPECIAL_USE: + return "specialUse"; default: return "unknown"; } diff --git a/core/java/android/content/pm/UserInfo.java b/core/java/android/content/pm/UserInfo.java index 9baa6ba2fb49..2be0323a1e8b 100644 --- a/core/java/android/content/pm/UserInfo.java +++ b/core/java/android/content/pm/UserInfo.java @@ -159,6 +159,18 @@ public class UserInfo implements Parcelable { public static final int FLAG_EPHEMERAL_ON_CREATE = 0x00002000; /** + * Indicates that this user is the designated main user on the device. This user may have access + * to certain features which are limited to at most one user. + * + * <p>Currently, this will be the first user to go through setup on the device, but in future + * releases this status may be transferable or may even not be given to any users. + * + * <p>This is not necessarily the system user. For example, it will not be the system user on + * devices for which {@link UserManager#isHeadlessSystemUserMode()} returns true. + */ + public static final int FLAG_MAIN = 0x00004000; + + /** * @hide */ @IntDef(flag = true, prefix = "FLAG_", value = { @@ -175,7 +187,8 @@ public class UserInfo implements Parcelable { FLAG_FULL, FLAG_SYSTEM, FLAG_PROFILE, - FLAG_EPHEMERAL_ON_CREATE + FLAG_EPHEMERAL_ON_CREATE, + FLAG_MAIN }) @Retention(RetentionPolicy.SOURCE) public @interface UserInfoFlag { @@ -369,6 +382,13 @@ public class UserInfo implements Parcelable { } /** + * @see #FLAG_MAIN + */ + public boolean isMain() { + return (flags & FLAG_MAIN) == FLAG_MAIN; + } + + /** * Returns true if the user is a split system user. * <p>If {@link UserManager#isSplitSystemUser split system user mode} is not enabled, * the method always returns false. diff --git a/core/java/android/content/pm/UserProperties.java b/core/java/android/content/pm/UserProperties.java index 0d3c8db3cb8b..fd35378efba2 100644 --- a/core/java/android/content/pm/UserProperties.java +++ b/core/java/android/content/pm/UserProperties.java @@ -45,6 +45,7 @@ public final class UserProperties implements Parcelable { 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 @@ public final class UserProperties implements Parcelable { 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 @@ public final class UserProperties implements Parcelable { 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 @@ public final class UserProperties implements Parcelable { 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 @@ public final class UserProperties implements Parcelable { } 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 @@ public final class UserProperties implements Parcelable { + ", mStartWithParent=" + getStartWithParent() + ", mShowInSettings=" + getShowInSettings() + ", mInheritDevicePolicy=" + getInheritDevicePolicy() + + ", mUseParentsContacts=" + getUseParentsContacts() + "}"; } @@ -341,6 +379,7 @@ public final class UserProperties implements Parcelable { pw.println(prefix + " mStartWithParent=" + getStartWithParent()); pw.println(prefix + " mShowInSettings=" + getShowInSettings()); pw.println(prefix + " mInheritDevicePolicy=" + getInheritDevicePolicy()); + pw.println(prefix + " mUseParentsContacts=" + getUseParentsContacts()); } /** @@ -386,6 +425,9 @@ public final class UserProperties implements Parcelable { 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 @@ public final class UserProperties implements Parcelable { 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 @@ public final class UserProperties implements Parcelable { dest.writeBoolean(mStartWithParent); dest.writeInt(mShowInSettings); dest.writeInt(mInheritDevicePolicy); + dest.writeBoolean(mUseParentsContacts); } /** @@ -440,6 +487,7 @@ public final class UserProperties implements Parcelable { mStartWithParent = source.readBoolean(); mShowInSettings = source.readInt(); mInheritDevicePolicy = source.readInt(); + mUseParentsContacts = source.readBoolean(); } @Override @@ -468,6 +516,7 @@ public final class UserProperties implements Parcelable { 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 @@ public final class UserProperties implements Parcelable { 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 @@ public final class UserProperties implements Parcelable { @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/content/res/ResourcesImpl.java b/core/java/android/content/res/ResourcesImpl.java index ff072916292b..09d24d47cc7a 100644 --- a/core/java/android/content/res/ResourcesImpl.java +++ b/core/java/android/content/res/ResourcesImpl.java @@ -40,8 +40,10 @@ import android.graphics.drawable.ColorStateListDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.DrawableContainer; import android.icu.text.PluralRules; +import android.net.Uri; import android.os.Build; import android.os.LocaleList; +import android.os.ParcelFileDescriptor; import android.os.Trace; import android.util.AttributeSet; import android.util.DisplayMetrics; @@ -59,6 +61,8 @@ import libcore.util.NativeAllocationRegistry; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; +import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; @@ -799,7 +803,21 @@ public class ResourcesImpl { private Drawable decodeImageDrawable(@NonNull AssetInputStream ais, @NonNull Resources wrapper, @NonNull TypedValue value) { ImageDecoder.Source src = new ImageDecoder.AssetInputStreamSource(ais, - wrapper, value); + wrapper, value); + try { + return ImageDecoder.decodeDrawable(src, (decoder, info, s) -> { + decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE); + }); + } catch (IOException ioe) { + // This is okay. This may be something that ImageDecoder does not + // support, like SVG. + return null; + } + } + + @Nullable + private Drawable decodeImageDrawable(@NonNull FileInputStream fis, @NonNull Resources wrapper) { + ImageDecoder.Source src = ImageDecoder.createSource(wrapper, fis); try { return ImageDecoder.decodeDrawable(src, (decoder, info, s) -> { decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE); @@ -860,6 +878,17 @@ public class ResourcesImpl { } else { dr = loadXmlDrawable(wrapper, value, id, density, file); } + } else if (file.startsWith("frro://")) { + Uri uri = Uri.parse(file); + File f = new File('/' + uri.getHost() + uri.getPath()); + ParcelFileDescriptor pfd = ParcelFileDescriptor.open(f, + ParcelFileDescriptor.MODE_READ_ONLY); + AssetFileDescriptor afd = new AssetFileDescriptor( + pfd, + Long.parseLong(uri.getQueryParameter("offset")), + Long.parseLong(uri.getQueryParameter("size"))); + FileInputStream is = afd.createInputStream(); + dr = decodeImageDrawable(is, wrapper); } else { final InputStream is = mAssets.openNonAsset( value.assetCookie, file, AssetManager.ACCESS_STREAMING); diff --git a/core/java/android/credentials/ui/CreateCredentialProviderData.java b/core/java/android/credentials/ui/CreateCredentialProviderData.java index 98157d73ad42..044427894d01 100644 --- a/core/java/android/credentials/ui/CreateCredentialProviderData.java +++ b/core/java/android/credentials/ui/CreateCredentialProviderData.java @@ -131,6 +131,13 @@ public class CreateCredentialProviderData extends ProviderData implements Parcel 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/hardware/usb/IUsbManager.aidl b/core/java/android/hardware/usb/IUsbManager.aidl index 5a442445d832..51236fe3b2c0 100644 --- a/core/java/android/hardware/usb/IUsbManager.aidl +++ b/core/java/android/hardware/usb/IUsbManager.aidl @@ -79,9 +79,20 @@ interface IUsbManager /* Returns true if the caller has permission to access the device. */ boolean hasDevicePermission(in UsbDevice device, String packageName); + /* Returns true if the given package/pid/uid has permission to access the device. */ + @JavaPassthrough(annotation= + "@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_USB)") + boolean hasDevicePermissionWithIdentity(in UsbDevice device, String packageName, + int pid, int uid); + /* Returns true if the caller has permission to access the accessory. */ boolean hasAccessoryPermission(in UsbAccessory accessory); + /* Returns true if the given pid/uid has permission to access the accessory. */ + @JavaPassthrough(annotation= + "@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_USB)") + boolean hasAccessoryPermissionWithIdentity(in UsbAccessory accessory, int pid, int uid); + /* Requests permission for the given package to access the device. * Will display a system dialog to query the user if permission * had not already been given. diff --git a/core/java/android/hardware/usb/UsbManager.java b/core/java/android/hardware/usb/UsbManager.java index 2c38f7031eff..50dd0064a5cb 100644 --- a/core/java/android/hardware/usb/UsbManager.java +++ b/core/java/android/hardware/usb/UsbManager.java @@ -838,6 +838,28 @@ public class UsbManager { } /** + * Returns true if the caller has permission to access the device. It's similar to the + * {@link #hasPermission(UsbDevice)} but allows to specify a different package/uid/pid. + * + * <p>Not for third-party apps.</p> + * + * @hide + */ + @RequiresPermission(Manifest.permission.MANAGE_USB) + @RequiresFeature(PackageManager.FEATURE_USB_HOST) + public boolean hasPermission(@NonNull UsbDevice device, @NonNull String packageName, + int pid, int uid) { + if (mService == null) { + return false; + } + try { + return mService.hasDevicePermissionWithIdentity(device, packageName, pid, uid); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Returns true if the caller has permission to access the accessory. * Permission might have been granted temporarily via * {@link #requestPermission(UsbAccessory, PendingIntent)} or @@ -859,6 +881,27 @@ public class UsbManager { } /** + * Returns true if the caller has permission to access the accessory. It's similar to the + * {@link #hasPermission(UsbAccessory)} but allows to specify a different uid/pid. + * + * <p>Not for third-party apps.</p> + * + * @hide + */ + @RequiresPermission(Manifest.permission.MANAGE_USB) + @RequiresFeature(PackageManager.FEATURE_USB_ACCESSORY) + public boolean hasPermission(@NonNull UsbAccessory accessory, int pid, int uid) { + if (mService == null) { + return false; + } + try { + return mService.hasAccessoryPermissionWithIdentity(accessory, pid, uid); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Requests temporary permission for the given package to access the device. * This may result in a system dialog being displayed to the user * if permission had not already been granted. diff --git a/core/java/android/inputmethodservice/IInputMethodWrapper.java b/core/java/android/inputmethodservice/IInputMethodWrapper.java index d23fb363df1c..d55367f19a70 100644 --- a/core/java/android/inputmethodservice/IInputMethodWrapper.java +++ b/core/java/android/inputmethodservice/IInputMethodWrapper.java @@ -32,6 +32,7 @@ import android.os.ResultReceiver; 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 @@ class IInputMethodWrapper extends IInputMethod.Stub 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 @@ class IInputMethodWrapper extends IInputMethod.Stub 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 @@ class IInputMethodWrapper extends IInputMethod.Stub @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 d902486faced..bf4fc4a72fdc 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -124,6 +124,7 @@ import android.view.inputmethod.CursorAnchorInfo; 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 @@ public class InputMethodService extends AbstractInputMethodService { */ 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 @@ public class InputMethodService extends AbstractInputMethodService { @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 @@ public class InputMethodService extends AbstractInputMethodService { @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 @@ public class InputMethodService extends AbstractInputMethodService { @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 @@ public class InputMethodService extends AbstractInputMethodService { @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 @@ public class InputMethodService extends AbstractInputMethodService { 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 @@ public class InputMethodService extends AbstractInputMethodService { 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 64c121194932..3282d567d369 100644 --- a/core/java/android/nfc/NfcAdapter.java +++ b/core/java/android/nfc/NfcAdapter.java @@ -29,7 +29,6 @@ import android.app.PendingIntent; 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 @@ public final class NfcAdapter { } /** - * 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 @@ public final class NfcAdapter { * @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 @@ public final class NfcAdapter { */ @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 @@ public final class NfcAdapter { 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 @@ public final class NfcAdapter { /** 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 @@ public final class NfcAdapter { "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 0b56d19201fb..6a4209135c66 100644 --- a/core/java/android/nfc/cardemulation/CardEmulation.java +++ b/core/java/android/nfc/cardemulation/CardEmulation.java @@ -22,11 +22,9 @@ import android.annotation.RequiresPermission; 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 @@ public final class CardEmulation { 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 3c924556365e..48bbf5b61052 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 @@ public final class NfcFCardEmulation { 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 d5c3de146015..b478a37984e0 100644 --- a/core/java/android/os/FileUtils.java +++ b/core/java/android/os/FileUtils.java @@ -84,7 +84,6 @@ import java.security.NoSuchAlgorithmException; 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 @@ public final class FileUtils { 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 @@ public final class FileUtils { 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 09bc4cc4eb7e..0d1dde105c09 100644 --- a/core/java/android/os/IHintSession.aidl +++ b/core/java/android/os/IHintSession.aidl @@ -22,4 +22,5 @@ oneway interface IHintSession { 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 a75b5ef6d65e..86135bcb0abf 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 android.content.Context; 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 @@ public final class PerformanceHintManager { 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 @@ public final class PerformanceHintManager { 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 @@ public final class PerformanceHintManager { 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/ServiceManager.java b/core/java/android/os/ServiceManager.java index e321a660e8e0..b6ff102b00b1 100644 --- a/core/java/android/os/ServiceManager.java +++ b/core/java/android/os/ServiceManager.java @@ -258,12 +258,14 @@ public final class ServiceManager { * waitForService should always be able to return the service. * @hide */ + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + @NonNull public static String[] getDeclaredInstances(@NonNull String iface) { try { return getIServiceManager().getDeclaredInstances(iface); } catch (RemoteException e) { Log.e(TAG, "error in getDeclaredInstances", e); - return null; + throw e.rethrowFromSystemServer(); } } diff --git a/core/java/android/os/SystemVibrator.java b/core/java/android/os/SystemVibrator.java index 6091bf9088d3..bf72b1d7a035 100644 --- a/core/java/android/os/SystemVibrator.java +++ b/core/java/android/os/SystemVibrator.java @@ -21,6 +21,7 @@ import android.annotation.NonNull; 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 @@ public class SystemVibrator extends Vibrator { 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 @@ public class SystemVibrator extends Vibrator { 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/Trace.java b/core/java/android/os/Trace.java index 8bfa0e99e3b4..fb197f5be2a8 100644 --- a/core/java/android/os/Trace.java +++ b/core/java/android/os/Trace.java @@ -100,6 +100,7 @@ public final class Trace { /** @hide */ public static final long TRACE_TAG_VIBRATOR = 1L << 23; /** @hide */ + @SystemApi(client = MODULE_LIBRARIES) public static final long TRACE_TAG_AIDL = 1L << 24; /** @hide */ public static final long TRACE_TAG_NNAPI = 1L << 25; diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index a38d9daee74a..1f21bfe6cd72 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -1506,6 +1506,30 @@ public class UserManager { 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 @@ public class UserManager { DISALLOW_WIFI_DIRECT, DISALLOW_ADD_WIFI_CONFIG, DISALLOW_CELLULAR_2G, + DISALLOW_ULTRA_WIDEBAND_RADIO, }) @Retention(RetentionPolicy.SOURCE) public @interface UserRestrictionKey {} @@ -1612,6 +1637,16 @@ public class UserManager { /** @hide */ public static final String SYSTEM_USER_MODE_EMULATION_HEADLESS = "headless"; + /** + * System Property used to override whether users can be created even if their type is disabled + * or their limit is reached. Set value to 1 to enable. + * + * <p>Only used on non-user builds. + * + * @hide + */ + public static final String DEV_CREATE_OVERRIDE_PROPERTY = "debug.user.creation_override"; + private static final String ACTION_CREATE_USER = "android.os.action.CREATE_USER"; /** @@ -2306,12 +2341,18 @@ public class UserManager { } /** - * Used to check if the context user is the primary user. The primary user - * is the first human user on a device. This is not supported in headless system user mode. + * Used to check if the context user is the primary user. The primary user is the first human + * user on a device. This is not supported in headless system user mode. * * @return whether the context user is the primary user. + * + * @deprecated This method always returns true for the system user, who may not be a full user + * if {@link #isHeadlessSystemUserMode} is true. Use {@link #isSystemUser}, {@link #isAdminUser} + * or {@link #isMainUser} instead. + * * @hide */ + @Deprecated @SystemApi @RequiresPermission(anyOf = { Manifest.permission.MANAGE_USERS, @@ -2336,6 +2377,29 @@ public class UserManager { } /** + * Returns true if the context user is the designated "main user" of the device. This user may + * have access to certain features which are limited to at most one user. + * + * <p>Currently, the first human user on the device will be the main user; in the future, the + * concept may be transferable, so a different user (or even no user at all) may be designated + * the main user instead. + * + * <p>Note that this will be the not be the system user on devices for which + * {@link #isHeadlessSystemUserMode()} returns true. + * @hide + */ + @SystemApi + @RequiresPermission(anyOf = { + Manifest.permission.MANAGE_USERS, + Manifest.permission.CREATE_USERS, + Manifest.permission.QUERY_USERS}) + @UserHandleAware + public boolean isMainUser() { + final UserInfo user = getUserInfo(mUserId); + return user != null && user.isMain(); + } + + /** * Used to check if the context user is an admin user. An admin user is allowed to * modify or configure certain settings that aren't available to non-admin users, * create and delete additional users, etc. There can be more than one admin users. @@ -4357,6 +4421,7 @@ public class UserManager { * @return true if the creation of users of the given user type is enabled on this device. * @hide */ + @TestApi @RequiresPermission(anyOf = { android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index fab6f7b97790..52b1adb876fc 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -192,6 +192,21 @@ public final class Settings { "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 @@ public final class Settings { "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 final class Settings { 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 @@ public final class Settings { /** * 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/service/credentials/CreateCredentialResponse.java b/core/java/android/service/credentials/CreateCredentialResponse.java index e330d1e94134..f69dca81950d 100644 --- a/core/java/android/service/credentials/CreateCredentialResponse.java +++ b/core/java/android/service/credentials/CreateCredentialResponse.java @@ -33,13 +33,11 @@ import java.util.Objects; * @hide */ public final class CreateCredentialResponse implements Parcelable { - private final @Nullable CharSequence mHeader; private final @NonNull List<SaveEntry> mSaveEntries; private final @Nullable Action mRemoteSaveEntry; //TODO : Add actions if needed private CreateCredentialResponse(@NonNull Parcel in) { - mHeader = in.readCharSequence(); List<SaveEntry> saveEntries = new ArrayList<>(); in.readTypedList(saveEntries, SaveEntry.CREATOR); mSaveEntries = saveEntries; @@ -48,7 +46,6 @@ public final class CreateCredentialResponse implements Parcelable { @Override public void writeToParcel(@NonNull Parcel dest, int flags) { - dest.writeCharSequence(mHeader); dest.writeTypedList(mSaveEntries); dest.writeTypedObject(mRemoteSaveEntry, flags); } @@ -72,21 +69,14 @@ public final class CreateCredentialResponse implements Parcelable { }; /* package-private */ CreateCredentialResponse( - @Nullable CharSequence header, @NonNull List<SaveEntry> saveEntries, @Nullable Action remoteSaveEntry) { - this.mHeader = header; this.mSaveEntries = saveEntries; com.android.internal.util.AnnotationValidations.validate( NonNull.class, null, mSaveEntries); this.mRemoteSaveEntry = remoteSaveEntry; } - /** Returns the header to be displayed on the UI. */ - public @Nullable CharSequence getHeader() { - return mHeader; - } - /** Returns the list of save entries to be displayed on the UI. */ public @NonNull List<SaveEntry> getSaveEntries() { return mSaveEntries; @@ -102,17 +92,9 @@ public final class CreateCredentialResponse implements Parcelable { */ @SuppressWarnings("WeakerAccess") public static final class Builder { - - private @Nullable CharSequence mHeader; private @NonNull List<SaveEntry> mSaveEntries = new ArrayList<>(); private @Nullable Action mRemoteSaveEntry; - /** Sets the header to be displayed on the UI. */ - public @NonNull Builder setHeader(@Nullable CharSequence header) { - mHeader = header; - return this; - } - /** * Sets the list of save entries to be shown on the UI. * @@ -154,7 +136,6 @@ public final class CreateCredentialResponse implements Parcelable { Preconditions.checkCollectionNotEmpty(mSaveEntries, "saveEntries must " + "not be empty"); return new CreateCredentialResponse( - mHeader, mSaveEntries, mRemoteSaveEntry); } diff --git a/core/java/android/service/credentials/CredentialEntry.java b/core/java/android/service/credentials/CredentialEntry.java index 1d4ac25eb284..98c537a6d6ef 100644 --- a/core/java/android/service/credentials/CredentialEntry.java +++ b/core/java/android/service/credentials/CredentialEntry.java @@ -173,7 +173,7 @@ public final class CredentialEntry implements Parcelable { */ public @NonNull Builder setPendingIntent(@Nullable PendingIntent pendingIntent) { if (pendingIntent != null) { - Preconditions.checkState(mCredential != null, + Preconditions.checkState(mCredential == null, "credential is already set. Cannot set both the pendingIntent " + "and the credential"); } @@ -189,7 +189,7 @@ public final class CredentialEntry implements Parcelable { */ public @NonNull Builder setCredential(@Nullable Credential credential) { if (credential != null) { - Preconditions.checkState(mPendingIntent != null, + Preconditions.checkState(mPendingIntent == null, "pendingIntent is already set. Cannot set both the " + "pendingIntent and the credential"); } @@ -215,10 +215,10 @@ public final class CredentialEntry implements Parcelable { * is set, or if both are set. */ public @NonNull CredentialEntry build() { - Preconditions.checkState(mPendingIntent == null && mCredential == null, - "Either pendingIntent or credential must be set"); - Preconditions.checkState(mPendingIntent != null && mCredential != null, - "Cannot set both the pendingIntent and credential"); + Preconditions.checkState(((mPendingIntent != null && mCredential == null) + || (mPendingIntent == null && mCredential != null)), + "Either pendingIntent or credential must be set, and both cannot" + + "be set at the same time"); return new CredentialEntry(mType, mSlice, mPendingIntent, mCredential, mAutoSelectAllowed); } diff --git a/core/java/android/service/credentials/CredentialProviderInfo.java b/core/java/android/service/credentials/CredentialProviderInfo.java index 2c7a983826f6..f89ad8e6e429 100644 --- a/core/java/android/service/credentials/CredentialProviderInfo.java +++ b/core/java/android/service/credentials/CredentialProviderInfo.java @@ -24,7 +24,6 @@ import android.app.AppGlobals; import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageItemInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; @@ -96,6 +95,8 @@ public final class CredentialProviderInfo { mLabel = mServiceInfo.loadSafeLabel( mContext.getPackageManager(), 0 /* do not ellipsize */, TextUtils.SAFE_STRING_FLAG_FIRST_LINE | TextUtils.SAFE_STRING_FLAG_TRIM); + Log.i(TAG, "mLabel is : " + mLabel + ", for: " + mServiceInfo.getComponentName() + .flattenToString()); populateProviderCapabilities(context, serviceInfo); } diff --git a/core/java/android/service/credentials/CredentialProviderService.java b/core/java/android/service/credentials/CredentialProviderService.java index b1b08f466622..6f3e786ffc4e 100644 --- a/core/java/android/service/credentials/CredentialProviderService.java +++ b/core/java/android/service/credentials/CredentialProviderService.java @@ -41,6 +41,18 @@ import java.util.Objects; * @hide */ public abstract class CredentialProviderService extends Service { + /** Extra to be used by provider to populate the credential when ending the activity started + * through the {@code pendingIntent} on the selected {@link SaveEntry}. **/ + public static final String EXTRA_SAVE_CREDENTIAL = + "android.service.credentials.extra.SAVE_CREDENTIAL"; + + /** + * Provider must read the value against this extra to receive the complete create credential + * request parameters, when a pending intent is launched. + */ + public static final String EXTRA_CREATE_CREDENTIAL_REQUEST_PARAMS = + "android.service.credentials.extra.CREATE_CREDENTIAL_REQUEST_PARAMS"; + private static final String TAG = "CredProviderService"; public static final String CAPABILITY_META_DATA_KEY = "android.credentials.capabilities"; @@ -64,7 +76,7 @@ public abstract class CredentialProviderService extends Service { } @Override - public final @NonNull IBinder onBind(@NonNull Intent intent) { + @NonNull public final IBinder onBind(@NonNull Intent intent) { if (SERVICE_INTERFACE.equals(intent.getAction())) { return mInterface.asBinder(); } diff --git a/core/java/android/service/credentials/CredentialsDisplayContent.java b/core/java/android/service/credentials/CredentialsDisplayContent.java index ab5b5240dbfa..4b23800891a8 100644 --- a/core/java/android/service/credentials/CredentialsDisplayContent.java +++ b/core/java/android/service/credentials/CredentialsDisplayContent.java @@ -34,9 +34,6 @@ import java.util.Objects; * @hide */ public final class CredentialsDisplayContent implements Parcelable { - /** Header to be displayed on the UI. */ - private final @Nullable CharSequence mHeader; - /** List of credential entries to be displayed on the UI. */ private final @NonNull List<CredentialEntry> mCredentialEntries; @@ -46,18 +43,15 @@ public final class CredentialsDisplayContent implements Parcelable { /** Remote credential entry to get the response from a different device. */ private final @Nullable Action mRemoteCredentialEntry; - private CredentialsDisplayContent(@Nullable CharSequence header, - @NonNull List<CredentialEntry> credentialEntries, + private CredentialsDisplayContent(@NonNull List<CredentialEntry> credentialEntries, @NonNull List<Action> actions, @Nullable Action remoteCredentialEntry) { - mHeader = header; mCredentialEntries = credentialEntries; mActions = actions; mRemoteCredentialEntry = remoteCredentialEntry; } private CredentialsDisplayContent(@NonNull Parcel in) { - mHeader = in.readCharSequence(); List<CredentialEntry> credentialEntries = new ArrayList<>(); in.readTypedList(credentialEntries, CredentialEntry.CREATOR); mCredentialEntries = credentialEntries; @@ -87,20 +81,12 @@ public final class CredentialsDisplayContent implements Parcelable { @Override public void writeToParcel(@NonNull Parcel dest, int flags) { - dest.writeCharSequence(mHeader); dest.writeTypedList(mCredentialEntries, flags); dest.writeTypedList(mActions, flags); dest.writeTypedObject(mRemoteCredentialEntry, flags); } /** - * Returns the header to be displayed on the UI. - */ - public @Nullable CharSequence getHeader() { - return mHeader; - } - - /** * Returns the list of credential entries to be displayed on the UI. */ public @NonNull List<CredentialEntry> getCredentialEntries() { @@ -125,20 +111,11 @@ public final class CredentialsDisplayContent implements Parcelable { * Builds an instance of {@link CredentialsDisplayContent}. */ public static final class Builder { - private CharSequence mHeader; private List<CredentialEntry> mCredentialEntries = new ArrayList<>(); private List<Action> mActions = new ArrayList<>(); private Action mRemoteCredentialEntry; /** - * Sets the header to be displayed on the UI. - */ - public @NonNull Builder setHeader(@Nullable CharSequence header) { - mHeader = header; - return this; - } - - /** * Sets the remote credential entry to be displayed on the UI. */ public @NonNull Builder setRemoteCredentialEntry(@Nullable Action remoteCredentialEntry) { @@ -208,7 +185,7 @@ public final class CredentialsDisplayContent implements Parcelable { throw new IllegalStateException("credentialEntries and actions must not both " + "be empty"); } - return new CredentialsDisplayContent(mHeader, mCredentialEntries, mActions, + return new CredentialsDisplayContent(mCredentialEntries, mActions, mRemoteCredentialEntry); } } diff --git a/core/java/android/service/credentials/GetCredentialsRequest.java b/core/java/android/service/credentials/GetCredentialsRequest.java index e06be4433062..03ba20e1df27 100644 --- a/core/java/android/service/credentials/GetCredentialsRequest.java +++ b/core/java/android/service/credentials/GetCredentialsRequest.java @@ -119,9 +119,9 @@ public final class GetCredentialsRequest implements Parcelable { */ public @NonNull Builder setGetCredentialOptions( @NonNull List<GetCredentialOption> getCredentialOptions) { - Preconditions.checkCollectionNotEmpty(mGetCredentialOptions, + Preconditions.checkCollectionNotEmpty(getCredentialOptions, "getCredentialOptions"); - Preconditions.checkCollectionElementsNotNull(mGetCredentialOptions, + Preconditions.checkCollectionElementsNotNull(getCredentialOptions, "getCredentialOptions"); mGetCredentialOptions = getCredentialOptions; return this; diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java index 32bdf7962273..bb22920b7c65 100644 --- a/core/java/android/service/dreams/DreamService.java +++ b/core/java/android/service/dreams/DreamService.java @@ -1294,7 +1294,7 @@ public class DreamService extends Service implements Window.Callback { if (!mWindowless) { Intent i = new Intent(this, DreamActivity.class); i.setPackage(getApplicationContext().getPackageName()); - i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NO_USER_ACTION); i.putExtra(DreamActivity.EXTRA_CALLBACK, new DreamActivityCallbacks(mDreamToken)); final ServiceInfo serviceInfo = fetchServiceInfo(this, new ComponentName(this, getClass())); diff --git a/core/java/android/service/timezone/TimeZoneProviderService.java b/core/java/android/service/timezone/TimeZoneProviderService.java index 2cea95a6c89f..41ca94b2eef2 100644 --- a/core/java/android/service/timezone/TimeZoneProviderService.java +++ b/core/java/android/service/timezone/TimeZoneProviderService.java @@ -44,8 +44,8 @@ import java.util.Objects; * * <p>Once started, providers are expected to detect the time zone if possible, and report the * result via {@link #reportSuggestion(TimeZoneProviderSuggestion)} or {@link - * #reportUncertain()}. Providers may also report that they have permanently failed - * by calling {@link #reportPermanentFailure(Throwable)}. See the javadocs for each + * #reportUncertain(TimeZoneProviderStatus)}. Providers may also report that they have permanently + * failed by calling {@link #reportPermanentFailure(Throwable)}. See the javadocs for each * method for details. * * <p>After starting, providers are expected to issue their first callback within the timeout @@ -213,8 +213,6 @@ public abstract class TimeZoneProviderService extends Service { * * @param providerStatus provider status information that can influence detector service * behavior and/or be reported via the device UI - * - * @hide */ public final void reportSuggestion(@NonNull TimeZoneProviderSuggestion suggestion, @NonNull TimeZoneProviderStatus providerStatus) { @@ -248,8 +246,9 @@ public abstract class TimeZoneProviderService extends Service { /** * Indicates the time zone is not known because of an expected runtime state or error, e.g. when - * the provider is unable to detect location, or there was a problem when resolving the location - * to a time zone. + * the provider is unable to detect location, or there was connectivity issue. + * + * <p>See {@link #reportUncertain(TimeZoneProviderStatus)} for a more expressive version */ public final void reportUncertain() { TimeZoneProviderStatus providerStatus = null; @@ -264,8 +263,6 @@ public abstract class TimeZoneProviderService extends Service { * * @param providerStatus provider status information that can influence detector service * behavior and/or be reported via the device UI - * - * @hide */ public final void reportUncertain(@NonNull TimeZoneProviderStatus providerStatus) { Objects.requireNonNull(providerStatus); @@ -362,8 +359,8 @@ public abstract class TimeZoneProviderService extends Service { * <p>Between {@link #onStartUpdates(long)} and {@link #onStopUpdates()} calls, the Android * system server holds the latest report from the provider in memory. After an initial report, * provider implementations are only required to send a report via {@link - * #reportSuggestion(TimeZoneProviderSuggestion)} or via {@link #reportUncertain()} when it - * differs from the previous report. + * #reportSuggestion(TimeZoneProviderSuggestion, TimeZoneProviderStatus)} or via {@link + * #reportUncertain(TimeZoneProviderStatus)} when it differs from the previous report. * * <p>{@link #reportPermanentFailure(Throwable)} can also be called by provider implementations * in rare cases, after which the provider should consider itself stopped and not make any @@ -375,7 +372,8 @@ public abstract class TimeZoneProviderService extends Service { * Android system server may move on to use other providers or detection methods. Providers * should therefore make best efforts during this time to generate a report, which could involve * increased power usage. Providers should preferably report an explicit {@link - * #reportUncertain()} if the time zone(s) cannot be detected within the initialization timeout. + * #reportUncertain(TimeZoneProviderStatus)} if the time zone(s) cannot be detected within the + * initialization timeout. * * @see #onStopUpdates() for the signal from the system server to stop sending reports */ diff --git a/core/java/android/service/timezone/TimeZoneProviderStatus.java b/core/java/android/service/timezone/TimeZoneProviderStatus.java index 513068f8a4b2..e0b78e9ae008 100644 --- a/core/java/android/service/timezone/TimeZoneProviderStatus.java +++ b/core/java/android/service/timezone/TimeZoneProviderStatus.java @@ -19,6 +19,7 @@ package android.service.timezone; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SystemApi; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; @@ -65,6 +66,7 @@ import java.util.regex.Pattern; * * @hide */ +@SystemApi public final class TimeZoneProviderStatus implements Parcelable { /** diff --git a/core/java/android/service/voice/HotwordDetectedResult.java b/core/java/android/service/voice/HotwordDetectedResult.java index b4f20c910a4a..dee560b322bb 100644 --- a/core/java/android/service/voice/HotwordDetectedResult.java +++ b/core/java/android/service/voice/HotwordDetectedResult.java @@ -31,6 +31,8 @@ import com.android.internal.R; import com.android.internal.util.DataClass; import com.android.internal.util.Preconditions; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -99,14 +101,30 @@ public final class HotwordDetectedResult implements Parcelable { private static final int LIMIT_AUDIO_CHANNEL_MAX_VALUE = 63; /** - * The bundle key for proximity value + * The bundle key for proximity * * TODO(b/238896013): Move the proximity logic out of bundle to proper API. - * - * @hide */ - public static final String EXTRA_PROXIMITY_METERS = - "android.service.voice.extra.PROXIMITY_METERS"; + private static final String EXTRA_PROXIMITY = + "android.service.voice.extra.PROXIMITY"; + + /** Users’ proximity is unknown (proximity sensing was inconclusive and is unsupported). */ + public static final int PROXIMITY_UNKNOWN = -1; + + /** Proximity value that represents that the object is near. */ + public static final int PROXIMITY_NEAR = 1; + + /** Proximity value that represents that the object is far. */ + public static final int PROXIMITY_FAR = 2; + + /** @hide */ + @IntDef(prefix = {"PROXIMITY"}, value = { + PROXIMITY_UNKNOWN, + PROXIMITY_NEAR, + PROXIMITY_FAR + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ProximityValue {} /** Confidence level in the trigger outcome. */ @HotwordConfidenceLevelValue @@ -220,12 +238,14 @@ public final class HotwordDetectedResult implements Parcelable { * versions of Android. * * <p>After the trigger happens, a special case of proximity-related extra, with the key of - * 'android.service.voice.extra.PROXIMITY_METERS' and the value of distance in meters (double), - * will be stored to enable proximity logic. The proximity meters is provided by the system, - * on devices that support detecting proximity of nearby users, to help disambiguate which - * nearby device should respond. When the proximity is unknown, the proximity value will not - * be stored. This mapping will be excluded from the max bundle size calculation because this - * mapping is included after the result is returned from the hotword detector service. + * 'android.service.voice.extra.PROXIMITY_VALUE' and the value of proximity value (integer) + * will be stored to enable proximity logic. {@link HotwordDetectedResult#PROXIMITY_NEAR} will + * indicate 'NEAR' proximity and {@link HotwordDetectedResult#PROXIMITY_FAR} will indicate 'FAR' + * proximity. The proximity value is provided by the system, on devices that support detecting + * proximity of nearby users, to help disambiguate which nearby device should respond. When the + * proximity is unknown, the proximity value will not be stored. This mapping will be excluded + * from the max bundle size calculation because this mapping is included after the result is + * returned from the hotword detector service. * * <p>This is a PersistableBundle so it doesn't allow any remotable objects or other contents * that can be used to communicate with other processes. @@ -348,16 +368,16 @@ public final class HotwordDetectedResult implements Parcelable { // Remove the proximity key from the bundle before checking the bundle size. The // proximity value is added after the privileged module and can avoid the // maxBundleSize limitation. - if (mExtras.containsKey(EXTRA_PROXIMITY_METERS)) { - double proximityMeters = mExtras.getDouble(EXTRA_PROXIMITY_METERS); - mExtras.remove(EXTRA_PROXIMITY_METERS); + if (mExtras.containsKey(EXTRA_PROXIMITY)) { + int proximityValue = mExtras.getInt(EXTRA_PROXIMITY); + mExtras.remove(EXTRA_PROXIMITY); // Skip checking parcelable size if the new bundle size is 0. Newly empty bundle // has parcelable size of 4, but the default bundle has parcelable size of 0. if (mExtras.size() > 0) { Preconditions.checkArgumentInRange(getParcelableSize(mExtras), 0, getMaxBundleSize(), "extras"); } - mExtras.putDouble(EXTRA_PROXIMITY_METERS, proximityMeters); + mExtras.putInt(EXTRA_PROXIMITY, proximityValue); } else { Preconditions.checkArgumentInRange(getParcelableSize(mExtras), 0, getMaxBundleSize(), "extras"); @@ -372,6 +392,52 @@ public final class HotwordDetectedResult implements Parcelable { return List.copyOf(mAudioStreams); } + /** + * Adds proximity level, either near or far, that is mapped for the given distance into + * the bundle. The proximity value is provided by the system, on devices that support detecting + * proximity of nearby users, to help disambiguate which nearby device should respond. + * This mapping will be excluded from the max bundle size calculation because this mapping is + * included after the result is returned from the hotword detector service. The value will not + * be included if the proximity was unknown. + * + * @hide + */ + public void setProximity(double distance) { + int proximityLevel = convertToProximityLevel(distance); + if (proximityLevel != PROXIMITY_UNKNOWN) { + mExtras.putInt(EXTRA_PROXIMITY, proximityLevel); + } + } + + /** + * Returns proximity level, which can be either of {@link HotwordDetectedResult#PROXIMITY_NEAR} + * or {@link HotwordDetectedResult#PROXIMITY_FAR}. If the proximity is unknown, it will + * return {@link HotwordDetectedResult#PROXIMITY_UNKNOWN}. + */ + @ProximityValue + public int getProximity() { + return mExtras.getInt(EXTRA_PROXIMITY, PROXIMITY_UNKNOWN); + } + + /** + * Mapping of the proximity distance (meters) to proximity values, unknown, near, and far. + * Currently, this mapping is handled by HotwordDetectedResult because it handles just + * HotwordDetectionConnection which we know the mapping of. However, the mapping will need to + * move to a more centralized place once there are more clients. + * + * TODO(b/258531144): Move the proximity mapping to a central location + */ + @ProximityValue + private int convertToProximityLevel(double distance) { + if (distance < 0) { + return PROXIMITY_UNKNOWN; + } else if (distance <= 3) { + return PROXIMITY_NEAR; + } else { + return PROXIMITY_FAR; + } + } + @DataClass.Suppress("addAudioStreams") abstract static class BaseBuilder { /** @@ -432,7 +498,7 @@ public final class HotwordDetectedResult implements Parcelable { CONFIDENCE_LEVEL_HIGH, CONFIDENCE_LEVEL_VERY_HIGH }) - @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) + @Retention(RetentionPolicy.SOURCE) @DataClass.Generated.Member public @interface ConfidenceLevel {} @@ -463,7 +529,7 @@ public final class HotwordDetectedResult implements Parcelable { LIMIT_HOTWORD_OFFSET_MAX_VALUE, LIMIT_AUDIO_CHANNEL_MAX_VALUE }) - @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) + @Retention(RetentionPolicy.SOURCE) @DataClass.Generated.Member /* package-private */ @interface Limit {} @@ -479,6 +545,30 @@ public final class HotwordDetectedResult implements Parcelable { } } + /** @hide */ + @IntDef(prefix = "PROXIMITY_", value = { + PROXIMITY_UNKNOWN, + PROXIMITY_NEAR, + PROXIMITY_FAR + }) + @Retention(RetentionPolicy.SOURCE) + @DataClass.Generated.Member + public @interface Proximity {} + + /** @hide */ + @DataClass.Generated.Member + public static String proximityToString(@Proximity int value) { + switch (value) { + case PROXIMITY_UNKNOWN: + return "PROXIMITY_UNKNOWN"; + case PROXIMITY_NEAR: + return "PROXIMITY_NEAR"; + case PROXIMITY_FAR: + return "PROXIMITY_FAR"; + default: return Integer.toHexString(value); + } + } + @DataClass.Generated.Member /* package-private */ HotwordDetectedResult( @HotwordConfidenceLevelValue int confidenceLevel, @@ -605,12 +695,14 @@ public final class HotwordDetectedResult implements Parcelable { * versions of Android. * * <p>After the trigger happens, a special case of proximity-related extra, with the key of - * 'android.service.voice.extra.PROXIMITY_METERS' and the value of distance in meters (double), - * will be stored to enable proximity logic. The proximity meters is provided by the system, - * on devices that support detecting proximity of nearby users, to help disambiguate which - * nearby device should respond. When the proximity is unknown, the proximity value will not - * be stored. This mapping will be excluded from the max bundle size calculation because this - * mapping is included after the result is returned from the hotword detector service. + * 'android.service.voice.extra.PROXIMITY_VALUE' and the value of proximity value (integer) + * will be stored to enable proximity logic. {@link HotwordDetectedResult#PROXIMITY_NEAR} will + * indicate 'NEAR' proximity and {@link HotwordDetectedResult#PROXIMITY_FAR} will indicate 'FAR' + * proximity. The proximity value is provided by the system, on devices that support detecting + * proximity of nearby users, to help disambiguate which nearby device should respond. When the + * proximity is unknown, the proximity value will not be stored. This mapping will be excluded + * from the max bundle size calculation because this mapping is included after the result is + * returned from the hotword detector service. * * <p>This is a PersistableBundle so it doesn't allow any remotable objects or other contents * that can be used to communicate with other processes. @@ -923,12 +1015,14 @@ public final class HotwordDetectedResult implements Parcelable { * versions of Android. * * <p>After the trigger happens, a special case of proximity-related extra, with the key of - * 'android.service.voice.extra.PROXIMITY_METERS' and the value of distance in meters (double), - * will be stored to enable proximity logic. The proximity meters is provided by the system, - * on devices that support detecting proximity of nearby users, to help disambiguate which - * nearby device should respond. When the proximity is unknown, the proximity value will not - * be stored. This mapping will be excluded from the max bundle size calculation because this - * mapping is included after the result is returned from the hotword detector service. + * 'android.service.voice.extra.PROXIMITY_VALUE' and the value of proximity value (integer) + * will be stored to enable proximity logic. {@link HotwordDetectedResult#PROXIMITY_NEAR} will + * indicate 'NEAR' proximity and {@link HotwordDetectedResult#PROXIMITY_FAR} will indicate 'FAR' + * proximity. The proximity value is provided by the system, on devices that support detecting + * proximity of nearby users, to help disambiguate which nearby device should respond. When the + * proximity is unknown, the proximity value will not be stored. This mapping will be excluded + * from the max bundle size calculation because this mapping is included after the result is + * returned from the hotword detector service. * * <p>This is a PersistableBundle so it doesn't allow any remotable objects or other contents * that can be used to communicate with other processes. @@ -1003,10 +1097,10 @@ public final class HotwordDetectedResult implements Parcelable { } @DataClass.Generated( - time = 1666342044844L, + time = 1668385264834L, codegenVersion = "1.0.23", sourceFile = "frameworks/base/core/java/android/service/voice/HotwordDetectedResult.java", - inputSignatures = "public static final int CONFIDENCE_LEVEL_NONE\npublic static final int CONFIDENCE_LEVEL_LOW\npublic static final int CONFIDENCE_LEVEL_LOW_MEDIUM\npublic static final int CONFIDENCE_LEVEL_MEDIUM\npublic static final int CONFIDENCE_LEVEL_MEDIUM_HIGH\npublic static final int CONFIDENCE_LEVEL_HIGH\npublic static final int CONFIDENCE_LEVEL_VERY_HIGH\npublic static final int HOTWORD_OFFSET_UNSET\npublic static final int AUDIO_CHANNEL_UNSET\nprivate static final int LIMIT_HOTWORD_OFFSET_MAX_VALUE\nprivate static final int LIMIT_AUDIO_CHANNEL_MAX_VALUE\npublic static final java.lang.String EXTRA_PROXIMITY_METERS\nprivate final @android.service.voice.HotwordDetectedResult.HotwordConfidenceLevelValue int mConfidenceLevel\nprivate @android.annotation.Nullable android.media.MediaSyncEvent mMediaSyncEvent\nprivate int mHotwordOffsetMillis\nprivate int mHotwordDurationMillis\nprivate int mAudioChannel\nprivate boolean mHotwordDetectionPersonalized\nprivate final int mScore\nprivate final int mPersonalizedScore\nprivate final int mHotwordPhraseId\nprivate final @android.annotation.NonNull java.util.List<android.service.voice.HotwordAudioStream> mAudioStreams\nprivate final @android.annotation.NonNull android.os.PersistableBundle mExtras\nprivate static int sMaxBundleSize\nprivate static int defaultConfidenceLevel()\nprivate static int defaultScore()\nprivate static int defaultPersonalizedScore()\npublic static int getMaxScore()\nprivate static int defaultHotwordPhraseId()\npublic static int getMaxHotwordPhraseId()\nprivate static java.util.List<android.service.voice.HotwordAudioStream> defaultAudioStreams()\nprivate static android.os.PersistableBundle defaultExtras()\npublic static int getMaxBundleSize()\npublic @android.annotation.Nullable android.media.MediaSyncEvent getMediaSyncEvent()\npublic static int getParcelableSize(android.os.Parcelable)\npublic static int getUsageSize(android.service.voice.HotwordDetectedResult)\nprivate static int bitCount(long)\nprivate void onConstructed()\npublic @android.annotation.NonNull java.util.List<android.service.voice.HotwordAudioStream> getAudioStreams()\npublic android.service.voice.HotwordDetectedResult.Builder buildUpon()\nclass HotwordDetectedResult extends java.lang.Object implements [android.os.Parcelable]\npublic @android.annotation.NonNull android.service.voice.HotwordDetectedResult.Builder setAudioStreams(java.util.List<android.service.voice.HotwordAudioStream>)\nclass BaseBuilder extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genBuilder=true, genEqualsHashCode=true, genHiddenConstDefs=true, genParcelable=true, genToString=true)\npublic @android.annotation.NonNull android.service.voice.HotwordDetectedResult.Builder setAudioStreams(java.util.List<android.service.voice.HotwordAudioStream>)\nclass BaseBuilder extends java.lang.Object implements []") + inputSignatures = "public static final int CONFIDENCE_LEVEL_NONE\npublic static final int CONFIDENCE_LEVEL_LOW\npublic static final int CONFIDENCE_LEVEL_LOW_MEDIUM\npublic static final int CONFIDENCE_LEVEL_MEDIUM\npublic static final int CONFIDENCE_LEVEL_MEDIUM_HIGH\npublic static final int CONFIDENCE_LEVEL_HIGH\npublic static final int CONFIDENCE_LEVEL_VERY_HIGH\npublic static final int HOTWORD_OFFSET_UNSET\npublic static final int AUDIO_CHANNEL_UNSET\nprivate static final int LIMIT_HOTWORD_OFFSET_MAX_VALUE\nprivate static final int LIMIT_AUDIO_CHANNEL_MAX_VALUE\nprivate static final java.lang.String EXTRA_PROXIMITY\npublic static final int PROXIMITY_UNKNOWN\npublic static final int PROXIMITY_NEAR\npublic static final int PROXIMITY_FAR\nprivate final @android.service.voice.HotwordDetectedResult.HotwordConfidenceLevelValue int mConfidenceLevel\nprivate @android.annotation.Nullable android.media.MediaSyncEvent mMediaSyncEvent\nprivate int mHotwordOffsetMillis\nprivate int mHotwordDurationMillis\nprivate int mAudioChannel\nprivate boolean mHotwordDetectionPersonalized\nprivate final int mScore\nprivate final int mPersonalizedScore\nprivate final int mHotwordPhraseId\nprivate final @android.annotation.NonNull java.util.List<android.service.voice.HotwordAudioStream> mAudioStreams\nprivate final @android.annotation.NonNull android.os.PersistableBundle mExtras\nprivate static int sMaxBundleSize\nprivate static int defaultConfidenceLevel()\nprivate static int defaultScore()\nprivate static int defaultPersonalizedScore()\npublic static int getMaxScore()\nprivate static int defaultHotwordPhraseId()\npublic static int getMaxHotwordPhraseId()\nprivate static java.util.List<android.service.voice.HotwordAudioStream> defaultAudioStreams()\nprivate static android.os.PersistableBundle defaultExtras()\npublic static int getMaxBundleSize()\npublic @android.annotation.Nullable android.media.MediaSyncEvent getMediaSyncEvent()\npublic static int getParcelableSize(android.os.Parcelable)\npublic static int getUsageSize(android.service.voice.HotwordDetectedResult)\nprivate static int bitCount(long)\nprivate void onConstructed()\npublic @android.annotation.NonNull java.util.List<android.service.voice.HotwordAudioStream> getAudioStreams()\npublic void setProximity(double)\npublic @android.service.voice.HotwordDetectedResult.ProximityValue int getProximity()\nprivate @android.service.voice.HotwordDetectedResult.ProximityValue int convertToProximityLevel(double)\npublic android.service.voice.HotwordDetectedResult.Builder buildUpon()\nclass HotwordDetectedResult extends java.lang.Object implements [android.os.Parcelable]\npublic @android.annotation.NonNull android.service.voice.HotwordDetectedResult.Builder setAudioStreams(java.util.List<android.service.voice.HotwordAudioStream>)\nclass BaseBuilder extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genBuilder=true, genEqualsHashCode=true, genHiddenConstDefs=true, genParcelable=true, genToString=true)\npublic @android.annotation.NonNull android.service.voice.HotwordDetectedResult.Builder setAudioStreams(java.util.List<android.service.voice.HotwordAudioStream>)\nclass BaseBuilder extends java.lang.Object implements []") @Deprecated private void __metadata() {} diff --git a/core/java/android/service/voice/HotwordDetectionService.java b/core/java/android/service/voice/HotwordDetectionService.java index df69cc00709c..552a793b6350 100644 --- a/core/java/android/service/voice/HotwordDetectionService.java +++ b/core/java/android/service/voice/HotwordDetectionService.java @@ -25,6 +25,7 @@ import android.annotation.Nullable; import android.annotation.SdkConstant; import android.annotation.SuppressLint; import android.annotation.SystemApi; +import android.annotation.TestApi; import android.app.Service; import android.content.ContentCaptureOptions; import android.content.Context; @@ -90,10 +91,10 @@ public abstract class HotwordDetectionService extends Service { /** * Feature flag for Attention Service. * - * TODO(b/247920386): Add TestApi annotation * @hide */ - public static final boolean ENABLE_PROXIMITY_RESULT = false; + @TestApi + public static final boolean ENABLE_PROXIMITY_RESULT = true; /** * Indicates that the updated status is successful. diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index 104570d03b42..007478a25016 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -31,6 +31,7 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.annotation.FloatRange; +import android.annotation.MainThread; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SdkConstant; @@ -657,6 +658,7 @@ public abstract class WallpaperService extends Service { * Called once to initialize the engine. After returning, the * engine's surface will be created by the framework. */ + @MainThread public void onCreate(SurfaceHolder surfaceHolder) { } @@ -665,6 +667,7 @@ public abstract class WallpaperService extends Service { * surface will be destroyed and this Engine object is no longer * valid. */ + @MainThread public void onDestroy() { } @@ -673,6 +676,7 @@ public abstract class WallpaperService extends Service { * hidden. <em>It is very important that a wallpaper only use * CPU while it is visible.</em>. */ + @MainThread public void onVisibilityChanged(boolean visible) { } @@ -683,6 +687,7 @@ public abstract class WallpaperService extends Service { * * @param insets Insets to apply. */ + @MainThread public void onApplyWindowInsets(WindowInsets insets) { } @@ -693,6 +698,7 @@ public abstract class WallpaperService extends Service { * user is interacting with, so if it is slow you will get fewer * move events. */ + @MainThread public void onTouchEvent(MotionEvent event) { } @@ -702,6 +708,7 @@ public abstract class WallpaperService extends Service { * call to {@link WallpaperManager#setWallpaperOffsets(IBinder, float, float) * WallpaperManager.setWallpaperOffsets()}. */ + @MainThread public void onOffsetsChanged(float xOffset, float yOffset, float xOffsetStep, float yOffsetStep, int xPixelOffset, int yPixelOffset) { @@ -724,6 +731,7 @@ public abstract class WallpaperService extends Service { * @return If returning a result, create a Bundle and place the * result data in to it. Otherwise return null. */ + @MainThread public Bundle onCommand(String action, int x, int y, int z, Bundle extras, boolean resultRequested) { return null; @@ -742,6 +750,7 @@ public abstract class WallpaperService extends Service { * @hide */ @SystemApi + @MainThread public void onAmbientModeChanged(boolean inAmbientMode, long animationDuration) { } @@ -749,6 +758,7 @@ public abstract class WallpaperService extends Service { * Called when an application has changed the desired virtual size of * the wallpaper. */ + @MainThread public void onDesiredSizeChanged(int desiredWidth, int desiredHeight) { } @@ -756,6 +766,7 @@ public abstract class WallpaperService extends Service { * Convenience for {@link SurfaceHolder.Callback#surfaceChanged * SurfaceHolder.Callback.surfaceChanged()}. */ + @MainThread public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) { } @@ -763,6 +774,7 @@ public abstract class WallpaperService extends Service { * Convenience for {@link SurfaceHolder.Callback2#surfaceRedrawNeeded * SurfaceHolder.Callback.surfaceRedrawNeeded()}. */ + @MainThread public void onSurfaceRedrawNeeded(SurfaceHolder holder) { } @@ -770,6 +782,7 @@ public abstract class WallpaperService extends Service { * Convenience for {@link SurfaceHolder.Callback#surfaceCreated * SurfaceHolder.Callback.surfaceCreated()}. */ + @MainThread public void onSurfaceCreated(SurfaceHolder holder) { } @@ -777,6 +790,7 @@ public abstract class WallpaperService extends Service { * Convenience for {@link SurfaceHolder.Callback#surfaceDestroyed * SurfaceHolder.Callback.surfaceDestroyed()}. */ + @MainThread public void onSurfaceDestroyed(SurfaceHolder holder) { } @@ -787,6 +801,7 @@ public abstract class WallpaperService extends Service { * @param zoom the zoom level, between 0 indicating fully zoomed in and 1 indicating fully * zoomed out. */ + @MainThread public void onZoomChanged(@FloatRange(from = 0f, to = 1f) float zoom) { } @@ -836,6 +851,7 @@ public abstract class WallpaperService extends Service { * * @return Wallpaper colors. */ + @MainThread public @Nullable WallpaperColors onComputeColors() { return null; } @@ -2510,6 +2526,7 @@ public abstract class WallpaperService extends Service { * when the wallpaper is currently set as the active wallpaper and the user * is in the wallpaper picker viewing a preview of it as well. */ + @MainThread public abstract Engine onCreateEngine(); @Override diff --git a/core/java/android/text/style/TextAppearanceSpan.java b/core/java/android/text/style/TextAppearanceSpan.java index 85b7ae901b38..d61228b295af 100644 --- a/core/java/android/text/style/TextAppearanceSpan.java +++ b/core/java/android/text/style/TextAppearanceSpan.java @@ -149,7 +149,7 @@ public class TextAppearanceSpan extends MetricAffectingSpan implements Parcelabl } 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 @@ public class TextAppearanceSpan extends MetricAffectingSpan implements Parcelabl mTextColorLink = linkColor; mTypeface = null; - mTextFontWeight = -1; + mTextFontWeight = FontStyle.FONT_WEIGHT_UNSPECIFIED; mTextLocales = null; mShadowRadius = 0.0f; @@ -359,8 +359,8 @@ public class TextAppearanceSpan extends MetricAffectingSpan implements Parcelabl } /** - * 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/util/OWNERS b/core/java/android/util/OWNERS index d4cf6e6e90c2..377200675ddf 100644 --- a/core/java/android/util/OWNERS +++ b/core/java/android/util/OWNERS @@ -1,6 +1,6 @@ per-file Dump* = file:/core/java/com/android/internal/util/dump/OWNERS per-file FeatureFlagUtils.java = sbasi@google.com -per-file FeatureFlagUtils.java = tmfang@google.com +per-file FeatureFlagUtils.java = edgarwang@google.com per-file AttributeSet.java = file:/core/java/android/content/res/OWNERS per-file TypedValue.java = file:/core/java/android/content/res/OWNERS diff --git a/core/java/android/view/AccessibilityInteractionController.java b/core/java/android/view/AccessibilityInteractionController.java index 510bde1deb5d..44168ca096cf 100644 --- a/core/java/android/view/AccessibilityInteractionController.java +++ b/core/java/android/view/AccessibilityInteractionController.java @@ -21,6 +21,7 @@ import static android.view.accessibility.AccessibilityNodeInfo.ACTION_ARGUMENT_A import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_REQUESTED_KEY; import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY; +import android.accessibilityservice.AccessibilityService; import android.annotation.NonNull; import android.graphics.Matrix; import android.graphics.Rect; @@ -46,11 +47,13 @@ import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.view.accessibility.AccessibilityNodeProvider; import android.view.accessibility.AccessibilityRequestPreparer; import android.view.accessibility.IAccessibilityInteractionConnectionCallback; +import android.window.ScreenCapture; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.SomeArgs; +import com.android.internal.util.function.pooled.PooledLambda; import java.util.ArrayDeque; import java.util.ArrayList; @@ -588,6 +591,43 @@ public final class AccessibilityInteractionController { } } + /** + * Take a screenshot using {@link ScreenCapture} of this {@link ViewRootImpl}'s {@link + * SurfaceControl}. + */ + public void takeScreenshotOfWindowClientThread(int interactionId, + ScreenCapture.ScreenCaptureListener listener, + IAccessibilityInteractionConnectionCallback callback) { + Message message = PooledLambda.obtainMessage( + AccessibilityInteractionController::takeScreenshotOfWindowUiThread, + this, interactionId, listener, callback); + + // Screenshot results are returned to the service asynchronously, so the same-thread + // message wait logic from #scheduleMessage() is not needed. + mHandler.sendMessage(message); + } + + private void takeScreenshotOfWindowUiThread(int interactionId, + ScreenCapture.ScreenCaptureListener listener, + IAccessibilityInteractionConnectionCallback callback) { + try { + if ((mViewRootImpl.getWindowFlags() & WindowManager.LayoutParams.FLAG_SECURE) != 0) { + callback.sendTakeScreenshotOfWindowError( + AccessibilityService.ERROR_TAKE_SCREENSHOT_SECURE_WINDOW, interactionId); + return; + } + final ScreenCapture.LayerCaptureArgs captureArgs = + new ScreenCapture.LayerCaptureArgs.Builder(mViewRootImpl.getSurfaceControl()) + .setChildrenOnly(false).setUid(Process.myUid()).build(); + if (ScreenCapture.captureLayers(captureArgs, listener) != 0) { + callback.sendTakeScreenshotOfWindowError( + AccessibilityService.ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR, interactionId); + } + } catch (RemoteException re) { + /* ignore - the other side will time out */ + } + } + public void findFocusClientThread(long accessibilityNodeId, int focusType, Region interactiveRegion, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, diff --git a/core/java/android/view/IDisplayWindowInsetsController.aidl b/core/java/android/view/IDisplayWindowInsetsController.aidl index 0769f1209a2b..91270d4160f5 100644 --- a/core/java/android/view/IDisplayWindowInsetsController.aidl +++ b/core/java/android/view/IDisplayWindowInsetsController.aidl @@ -19,6 +19,7 @@ package android.view; 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 @@ oneway interface IDisplayWindowInsetsController { /** * @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 a8564740c0c8..8e16f24b154f 100644 --- a/core/java/android/view/IWindow.aidl +++ b/core/java/android/view/IWindow.aidl @@ -29,6 +29,7 @@ import android.view.InsetsState; 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 @@ oneway interface IWindow { * * @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 27b4d8744452..e775969238a0 100644 --- a/core/java/android/view/InsetsAnimationControlImpl.java +++ b/core/java/android/view/InsetsAnimationControlImpl.java @@ -58,6 +58,7 @@ import android.view.WindowInsets.Type.InsetsType; 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 @@ import java.util.Objects; * Implements {@link WindowInsetsAnimationController} * @hide */ -@VisibleForTesting +@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) public class InsetsAnimationControlImpl implements InternalInsetsAnimationController, InsetsAnimationControlRunner { @@ -96,6 +97,8 @@ public class InsetsAnimationControlImpl implements InternalInsetsAnimationContro /** @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 @@ public class InsetsAnimationControlImpl implements InternalInsetsAnimationContro @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 @@ public class InsetsAnimationControlImpl implements InternalInsetsAnimationContro mAnimationType = animationType; mLayoutInsetsDuringAnimation = layoutInsetsDuringAnimation; mTranslator = translator; + mStatsToken = statsToken; mController.startAnimation(this, listener, types, mAnimation, new Bounds(mHiddenInsets, mShownInsets)); } @@ -228,6 +232,11 @@ public class InsetsAnimationControlImpl implements InternalInsetsAnimationContro } @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 @@ public class InsetsAnimationControlImpl implements InternalInsetsAnimationContro } } - @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 291351e0b9d3..cf40e7e4d308 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 @@ public interface InsetsAnimationControlRunner { @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 fc97541bd34d..f7b9aa26ad96 100644 --- a/core/java/android/view/InsetsAnimationThreadControlRunner.java +++ b/core/java/android/view/InsetsAnimationThreadControlRunner.java @@ -34,6 +34,7 @@ import android.view.SyncRtSurfaceTransactionApplier.SurfaceParams; 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 @@ public class InsetsAnimationThreadControlRunner implements InsetsAnimationContro @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 @@ public class InsetsAnimationThreadControlRunner implements InsetsAnimationContro } @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 35838a39057e..fbd82266246b 100644 --- a/core/java/android/view/InsetsController.java +++ b/core/java/android/view/InsetsController.java @@ -44,6 +44,7 @@ import android.os.CancellationSignal; 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.WindowManager.LayoutParams.SoftInputModeFlags; 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 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation 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 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation @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 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation true /* fromIme */, pendingRequest.durationMs, pendingRequest.interpolator, pendingRequest.animationType, pendingRequest.layoutInsetsDuringAnimation, - pendingRequest.useInsetsAnimationThread); + pendingRequest.useInsetsAnimationThread, statsToken); return; } @@ -990,8 +995,9 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation 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 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation 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 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation 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 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation 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 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation 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 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation ? 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 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation // 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 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation } 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 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation } @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 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation && 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 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation 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 edcfc95fe4e4..778c677c58bd 100644 --- a/core/java/android/view/InsetsResizeAnimationRunner.java +++ b/core/java/android/view/InsetsResizeAnimationRunner.java @@ -37,6 +37,7 @@ import android.view.InsetsState.InternalInsetsType; 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 @@ public class InsetsResizeAnimationRunner implements InsetsAnimationControlRunner } @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 e91839baad61..a8cc9b62d30a 100644 --- a/core/java/android/view/InsetsState.java +++ b/core/java/android/view/InsetsState.java @@ -702,7 +702,7 @@ public class InsetsState implements Parcelable { 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 @@ public class InsetsState implements Parcelable { 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/VelocityTracker.java b/core/java/android/view/VelocityTracker.java index 6d25523ac5bc..00170cbe53d5 100644 --- a/core/java/android/view/VelocityTracker.java +++ b/core/java/android/view/VelocityTracker.java @@ -28,7 +28,7 @@ import java.lang.annotation.RetentionPolicy; import java.util.Map; /** - * Helper for tracking the velocity of touch events, for implementing + * Helper for tracking the velocity of motion events, for implementing * flinging and other such gestures. * * Use {@link #obtain} to retrieve a new instance of the class when you are going @@ -43,6 +43,15 @@ public final class VelocityTracker { private static final int ACTIVE_POINTER_ID = -1; + /** @hide */ + @IntDef(value = { + MotionEvent.AXIS_X, + MotionEvent.AXIS_Y, + MotionEvent.AXIS_SCROLL + }) + @Retention(RetentionPolicy.SOURCE) + public @interface VelocityTrackableMotionEventAxis {} + /** * Velocity Tracker Strategy: Invalid. * @@ -306,10 +315,12 @@ public final class VelocityTracker { } /** - * Checks whether a given motion axis is supported for velocity tracking. + * Checks whether a given velocity-trackable {@link MotionEvent} axis is supported for velocity + * tracking by this {@link VelocityTracker} instance (refer to + * {@link #getAxisVelocity(int, int)} for a list of potentially velocity-trackable axes). * - * <p>The axis values that would make sense to use for this method are the ones defined in the - * {@link MotionEvent} class. + * <p>Note that the value returned from this method will stay the same for a given instance, so + * a single check for axis support is enough per a {@link VelocityTracker} instance. * * @param axis The axis to check for velocity support. * @return {@code true} if {@code axis} is supported for velocity tracking, or {@code false} @@ -317,7 +328,7 @@ public final class VelocityTracker { * @see #getAxisVelocity(int, int) * @see #getAxisVelocity(int) */ - public boolean isAxisSupported(int axis) { + public boolean isAxisSupported(@VelocityTrackableMotionEventAxis int axis) { return nativeIsAxisSupported(axis); } @@ -421,13 +432,16 @@ public final class VelocityTracker { * calling this function. * * <p>In addition to {@link MotionEvent#AXIS_X} and {@link MotionEvent#AXIS_Y} which have been - * supported since the introduction of this class, the following axes are supported for this + * supported since the introduction of this class, the following axes can be candidates for this * method: * <ul> * <li> {@link MotionEvent#AXIS_SCROLL}: supported starting * {@link Build.VERSION_CODES#UPSIDE_DOWN_CAKE} * </ul> * + * <p>Before accessing velocities of an axis using this method, check that your + * {@link VelocityTracker} instance supports the axis by using {@link #isAxisSupported(int)}. + * * @param axis Which axis' velocity to return. * @param id Which pointer's velocity to return. * @return The previously computed velocity for {@code axis} for pointer ID of {@code id} if @@ -435,7 +449,7 @@ public final class VelocityTracker { * for the axis. * @see #isAxisSupported(int) */ - public float getAxisVelocity(int axis, int id) { + public float getAxisVelocity(@VelocityTrackableMotionEventAxis int axis, int id) { return nativeGetVelocity(mPtr, axis, id); } @@ -450,7 +464,7 @@ public final class VelocityTracker { * @see #isAxisSupported(int) * @see #getAxisVelocity(int, int) */ - public float getAxisVelocity(int axis) { + public float getAxisVelocity(@VelocityTrackableMotionEventAxis int axis) { return nativeGetVelocity(mPtr, axis, ACTIVE_POINTER_ID); } diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java index f51d9bacc0a5..58aee61b98be 100644 --- a/core/java/android/view/ViewConfiguration.java +++ b/core/java/android/view/ViewConfiguration.java @@ -822,6 +822,7 @@ public class ViewConfiguration { * * @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 e664ebf4c484..7e8ebd7fe076 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -88,6 +88,7 @@ import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodCl import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.INSETS_CONTROLLER; import android.Manifest; +import android.accessibilityservice.AccessibilityService; import android.animation.AnimationHandler; import android.animation.LayoutTransition; import android.annotation.AnyThread; @@ -192,12 +193,14 @@ import android.view.autofill.AutofillManager; 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; import android.window.CompatOnBackInvokedCallback; import android.window.OnBackInvokedCallback; import android.window.OnBackInvokedDispatcher; +import android.window.ScreenCapture; import android.window.SurfaceSyncGroup; import android.window.WindowOnBackInvokedDispatcher; @@ -711,7 +714,7 @@ public final class ViewRootImpl implements ViewParent, 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 +1108,11 @@ public final class ViewRootImpl implements ViewParent, 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 +1244,11 @@ public final class ViewRootImpl implements ViewParent, 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 +1258,8 @@ public final class ViewRootImpl implements ViewParent, 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 +1790,24 @@ public final class ViewRootImpl implements ViewParent, 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 +5652,23 @@ public final class ViewRootImpl implements ViewParent, 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 +8234,7 @@ public final class ViewRootImpl implements ViewParent, 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 +8820,14 @@ public final class ViewRootImpl implements ViewParent, 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 +10191,8 @@ public final class ViewRootImpl implements ViewParent, } @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 +10200,16 @@ public final class ViewRootImpl implements ViewParent, 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 +10217,10 @@ public final class ViewRootImpl implements ViewParent, 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); } } @@ -10691,6 +10709,25 @@ public final class ViewRootImpl implements ViewParent, .notifyOutsideTouchClientThread(); } } + + @Override + public void takeScreenshotOfWindow(int interactionId, + ScreenCapture.ScreenCaptureListener listener, + IAccessibilityInteractionConnectionCallback callback) { + ViewRootImpl viewRootImpl = mViewRootImpl.get(); + if (viewRootImpl != null && viewRootImpl.mView != null) { + viewRootImpl.getAccessibilityInteractionController() + .takeScreenshotOfWindowClientThread(interactionId, listener, callback); + } else { + try { + callback.sendTakeScreenshotOfWindowError( + AccessibilityService.ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR, + interactionId); + } catch (RemoteException re) { + /* best effort - ignore */ + } + } + } } /** diff --git a/core/java/android/view/WindowInsets.java b/core/java/android/view/WindowInsets.java index 2a76c4e0a694..d77e499357b9 100644 --- a/core/java/android/view/WindowInsets.java +++ b/core/java/android/view/WindowInsets.java @@ -1425,8 +1425,8 @@ public final class WindowInsets { 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 @@ public final class WindowInsets { 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 @@ public final class WindowInsets { 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 @@ public final class WindowInsets { @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 @@ public final class WindowInsets { } /** + * 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/AccessibilityInteractionClient.java b/core/java/android/view/accessibility/AccessibilityInteractionClient.java index e3ffc9dcbbde..7030ab54b790 100644 --- a/core/java/android/view/accessibility/AccessibilityInteractionClient.java +++ b/core/java/android/view/accessibility/AccessibilityInteractionClient.java @@ -22,7 +22,9 @@ import static android.os.Build.VERSION_CODES.S; import static android.view.accessibility.AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS_MASK; import static android.view.accessibility.AccessibilityNodeInfo.FLAG_PREFETCH_MASK; +import android.accessibilityservice.AccessibilityService; import android.accessibilityservice.IAccessibilityServiceConnection; +import android.annotation.CallbackExecutor; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; @@ -31,17 +33,21 @@ import android.content.Context; import android.os.Binder; import android.os.Build; import android.os.Bundle; +import android.os.Handler; import android.os.IBinder; +import android.os.Looper; import android.os.Message; import android.os.Process; import android.os.RemoteException; import android.os.SystemClock; import android.util.Log; import android.util.LongSparseArray; +import android.util.Pair; import android.util.SparseArray; import android.util.SparseLongArray; import android.view.Display; import android.view.ViewConfiguration; +import android.window.ScreenCapture; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; @@ -53,6 +59,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Queue; +import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicInteger; /** @@ -146,6 +153,10 @@ public final class AccessibilityInteractionClient private boolean mPerformAccessibilityActionResult; + // SparseArray of interaction ID -> screenshot executor+callback. + private final SparseArray<Pair<Executor, AccessibilityService.TakeScreenshotCallback>> + mTakeScreenshotOfWindowCallbacks = new SparseArray<>(); + private Message mSameThreadMessage; private int mInteractionIdWaitingForPrefetchResult = -1; @@ -779,6 +790,59 @@ public final class AccessibilityInteractionClient } /** + * Takes a screenshot of the window with the provided {@code accessibilityWindowId} and + * returns the answer asynchronously. This async behavior is similar to {@link + * AccessibilityService#takeScreenshot} but unlike other methods in this class which perform + * synchronous waiting in the AccessibilityService client. + * + * @see AccessibilityService#takeScreenshotOfWindow + */ + public void takeScreenshotOfWindow(int connectionId, int accessibilityWindowId, + @NonNull @CallbackExecutor Executor executor, + @NonNull AccessibilityService.TakeScreenshotCallback callback) { + synchronized (mInstanceLock) { + try { + IAccessibilityServiceConnection connection = getConnection(connectionId); + if (connection == null) { + executor.execute(() -> callback.onFailure( + AccessibilityService.ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR)); + return; + } + final long identityToken = Binder.clearCallingIdentity(); + try { + final int interactionId = mInteractionIdCounter.getAndIncrement(); + mTakeScreenshotOfWindowCallbacks.put(interactionId, + Pair.create(executor, callback)); + // Create a ScreenCaptureListener to receive the screenshot directly from + // SurfaceFlinger instead of requiring an extra IPC from the app: + // A11yService -> App -> SurfaceFlinger -> A11yService + ScreenCapture.ScreenCaptureListener listener = + new ScreenCapture.ScreenCaptureListener( + screenshot -> sendWindowScreenshotSuccess(screenshot, + interactionId)); + connection.takeScreenshotOfWindow(accessibilityWindowId, interactionId, + listener, this); + new Handler(Looper.getMainLooper()).postDelayed(() -> { + synchronized (mInstanceLock) { + // Notify failure if we still haven't sent a response after timeout. + if (mTakeScreenshotOfWindowCallbacks.contains(interactionId)) { + sendTakeScreenshotOfWindowError( + AccessibilityService.ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR, + interactionId); + } + } + }, TIMEOUT_INTERACTION_MILLIS); + } finally { + Binder.restoreCallingIdentity(identityToken); + } + } catch (RemoteException re) { + executor.execute(() -> callback.onFailure( + AccessibilityService.ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR)); + } + } + } + + /** * Finds {@link AccessibilityNodeInfo}s by View text. The match is case * insensitive containment. The search is performed in the window whose * id is specified and starts from the node whose accessibility id is @@ -1254,6 +1318,55 @@ public final class AccessibilityInteractionClient } /** + * Sends the result of a window screenshot request to the requesting client. + * + * {@link #takeScreenshotOfWindow} does not perform synchronous waiting, so this method + * does not notify any wait lock. + */ + private void sendWindowScreenshotSuccess(ScreenCapture.ScreenshotHardwareBuffer screenshot, + int interactionId) { + if (screenshot == null) { + sendTakeScreenshotOfWindowError( + AccessibilityService.ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR, interactionId); + return; + } + synchronized (mInstanceLock) { + if (mTakeScreenshotOfWindowCallbacks.contains(interactionId)) { + final AccessibilityService.ScreenshotResult result = + new AccessibilityService.ScreenshotResult(screenshot.getHardwareBuffer(), + screenshot.getColorSpace(), SystemClock.uptimeMillis()); + final Pair<Executor, AccessibilityService.TakeScreenshotCallback> pair = + mTakeScreenshotOfWindowCallbacks.get(interactionId); + final Executor executor = pair.first; + final AccessibilityService.TakeScreenshotCallback callback = pair.second; + executor.execute(() -> callback.onSuccess(result)); + mTakeScreenshotOfWindowCallbacks.remove(interactionId); + } + } + } + + /** + * Sends an error code for a window screenshot request to the requesting client. + * + * @param errorCode The error code from {@link AccessibilityService.ScreenshotErrorCode}. + * @param interactionId The interaction id of the request. + */ + @Override + public void sendTakeScreenshotOfWindowError( + @AccessibilityService.ScreenshotErrorCode int errorCode, int interactionId) { + synchronized (mInstanceLock) { + if (mTakeScreenshotOfWindowCallbacks.contains(interactionId)) { + final Pair<Executor, AccessibilityService.TakeScreenshotCallback> pair = + mTakeScreenshotOfWindowCallbacks.get(interactionId); + final Executor executor = pair.first; + final AccessibilityService.TakeScreenshotCallback callback = pair.second; + executor.execute(() -> callback.onFailure(errorCode)); + mTakeScreenshotOfWindowCallbacks.remove(interactionId); + } + } + } + + /** * Clears the result state. */ private void clearResultLocked() { diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java index 9dbababb4de8..88adb2e1b1f1 100644 --- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java +++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java @@ -127,6 +127,16 @@ public class AccessibilityNodeInfo implements Parcelable { /** @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 @@ public class AccessibilityNodeInfo implements Parcelable { 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 @@ public class AccessibilityNodeInfo implements Parcelable { } /** + * 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 @@ public class AccessibilityNodeInfo implements Parcelable { 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 @@ public class AccessibilityNodeInfo implements Parcelable { 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 @@ public class AccessibilityNodeInfo implements Parcelable { 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 @@ public class AccessibilityNodeInfo implements Parcelable { 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 @@ public class AccessibilityNodeInfo implements Parcelable { 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/accessibility/IAccessibilityInteractionConnection.aidl b/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl index 472a36361585..fb01921dbaa1 100644 --- a/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl +++ b/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl @@ -22,6 +22,7 @@ import android.os.Bundle; import android.view.MagnificationSpec; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.IAccessibilityInteractionConnectionCallback; +import android.window.ScreenCapture; /** * Interface for interaction between the AccessibilityManagerService @@ -60,4 +61,8 @@ oneway interface IAccessibilityInteractionConnection { void clearAccessibilityFocus(); void notifyOutsideTouch(); + + void takeScreenshotOfWindow(int interactionId, + in ScreenCapture.ScreenCaptureListener listener, + IAccessibilityInteractionConnectionCallback callback); } diff --git a/core/java/android/view/accessibility/IAccessibilityInteractionConnectionCallback.aidl b/core/java/android/view/accessibility/IAccessibilityInteractionConnectionCallback.aidl index 231e75a19a06..456bf5893ea0 100644 --- a/core/java/android/view/accessibility/IAccessibilityInteractionConnectionCallback.aidl +++ b/core/java/android/view/accessibility/IAccessibilityInteractionConnectionCallback.aidl @@ -63,4 +63,9 @@ oneway interface IAccessibilityInteractionConnectionCallback { */ @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553) void setPerformAccessibilityActionResult(boolean succeeded, int interactionId); + + /** + * Sends an error code for a window screenshot request to the requesting client. + */ + void sendTakeScreenshotOfWindowError(int errorCode, int interactionId); } diff --git a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java index a66c67b2e255..6eae63afcba7 100644 --- a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java +++ b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java @@ -275,15 +275,16 @@ final class IInputMethodManagerGlobalInvoker { @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 @@ final class IInputMethodManagerGlobalInvoker { @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 000000000000..1988f482497d --- /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 000000000000..f4ecdff12c7a --- /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 4d5a17d92f11..92380ed7a7bc 100644 --- a/core/java/android/view/inputmethod/InputMethod.java +++ b/core/java/android/view/inputmethod/InputMethod.java @@ -300,11 +300,12 @@ public interface InputMethod { * @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 @@ public interface InputMethod { * @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 @@ public interface InputMethod { /** * 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 74afced453fa..ee31fd5763b7 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -632,6 +632,8 @@ public final class InputMethodManager { private final DelegateImpl mDelegate = new DelegateImpl(); + private static boolean sPreventImeStartupUnlessTextEditor; + // ----------------------------------------------------------- private static final int MSG_DUMP = 1; @@ -1435,6 +1437,10 @@ public final class InputMethodManager { // 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 @@ public final class InputMethodManager { 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 @@ public final class InputMethodManager { 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 @@ public final class InputMethodManager { return IInputMethodManagerGlobalInvoker.showSoftInput( mClient, view.getWindowToken(), + statsToken, flags, mCurRootView.getLastClickToolType(), resultReceiver, @@ -2043,19 +2057,28 @@ public final class InputMethodManager { @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 @@ public final class InputMethodManager { 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 @@ public final class InputMethodManager { @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 @@ public final class InputMethodManager { * @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 @@ public final class InputMethodManager { 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 @@ public final class InputMethodManager { /** * 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 810fde23dcac..bf1a2bd51d91 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -2289,11 +2289,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * @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 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } 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 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener 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 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener 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/BackNavigationInfo.java b/core/java/android/window/BackNavigationInfo.java index 9b91cf2e9db6..a25e0351df83 100644 --- a/core/java/android/window/BackNavigationInfo.java +++ b/core/java/android/window/BackNavigationInfo.java @@ -89,8 +89,6 @@ public final class BackNavigationInfo implements Parcelable { @Nullable private final IOnBackInvokedCallback mOnBackInvokedCallback; private final boolean mPrepareRemoteAnimation; - @Nullable - private WindowContainerToken mDepartingWindowContainerToken; /** * Create a new {@link BackNavigationInfo} instance. @@ -100,20 +98,15 @@ public final class BackNavigationInfo implements Parcelable { * back preview. * @param onBackInvokedCallback The back callback registered by the current top level window. * @param departingWindowContainerToken The {@link WindowContainerToken} of departing window. - * @param isPrepareRemoteAnimation Return whether the core is preparing a back gesture - * animation, if true, the caller of startBackNavigation should - * be expected to receive an animation start callback. */ private BackNavigationInfo(@BackTargetType int type, @Nullable RemoteCallback onBackNavigationDone, @Nullable IOnBackInvokedCallback onBackInvokedCallback, - boolean isPrepareRemoteAnimation, - @Nullable WindowContainerToken departingWindowContainerToken) { + boolean isPrepareRemoteAnimation) { mType = type; mOnBackNavigationDone = onBackNavigationDone; mOnBackInvokedCallback = onBackInvokedCallback; mPrepareRemoteAnimation = isPrepareRemoteAnimation; - mDepartingWindowContainerToken = departingWindowContainerToken; } private BackNavigationInfo(@NonNull Parcel in) { @@ -121,7 +114,6 @@ public final class BackNavigationInfo implements Parcelable { mOnBackNavigationDone = in.readTypedObject(RemoteCallback.CREATOR); mOnBackInvokedCallback = IOnBackInvokedCallback.Stub.asInterface(in.readStrongBinder()); mPrepareRemoteAnimation = in.readBoolean(); - mDepartingWindowContainerToken = in.readTypedObject(WindowContainerToken.CREATOR); } @Override @@ -130,7 +122,6 @@ public final class BackNavigationInfo implements Parcelable { dest.writeTypedObject(mOnBackNavigationDone, flags); dest.writeStrongInterface(mOnBackInvokedCallback); dest.writeBoolean(mPrepareRemoteAnimation); - dest.writeTypedObject(mDepartingWindowContainerToken, flags); } /** @@ -164,18 +155,6 @@ public final class BackNavigationInfo implements Parcelable { } /** - * Returns the {@link WindowContainerToken} of the highest container in the hierarchy being - * removed. - * <p> - * For example, if an Activity is the last one of its Task, the Task's token will be given. - * Otherwise, it will be the Activity's token. - */ - @Nullable - public WindowContainerToken getDepartingWindowContainerToken() { - return mDepartingWindowContainerToken; - } - - /** * Callback to be called when the back preview is finished in order to notify the server that * it can clean up the resources created for the animation. * @@ -212,7 +191,6 @@ public final class BackNavigationInfo implements Parcelable { + "mType=" + typeToString(mType) + " (" + mType + ")" + ", mOnBackNavigationDone=" + mOnBackNavigationDone + ", mOnBackInvokedCallback=" + mOnBackInvokedCallback - + ", mWindowContainerToken=" + mDepartingWindowContainerToken + '}'; } @@ -248,8 +226,6 @@ public final class BackNavigationInfo implements Parcelable { @Nullable private IOnBackInvokedCallback mOnBackInvokedCallback = null; private boolean mPrepareRemoteAnimation; - @Nullable - private WindowContainerToken mDepartingWindowContainerToken = null; /** * @see BackNavigationInfo#getType() @@ -285,20 +261,12 @@ public final class BackNavigationInfo implements Parcelable { } /** - * @see BackNavigationInfo#getDepartingWindowContainerToken() - */ - public void setDepartingWCT(@NonNull WindowContainerToken windowContainerToken) { - mDepartingWindowContainerToken = windowContainerToken; - } - - /** * Builds and returns an instance of {@link BackNavigationInfo} */ public BackNavigationInfo build() { return new BackNavigationInfo(mType, mOnBackNavigationDone, mOnBackInvokedCallback, - mPrepareRemoteAnimation, - mDepartingWindowContainerToken); + mPrepareRemoteAnimation); } } } diff --git a/core/java/android/window/ClientWindowFrames.java b/core/java/android/window/ClientWindowFrames.java index f274d1a15ba5..0ce076b6eb96 100644 --- a/core/java/android/window/ClientWindowFrames.java +++ b/core/java/android/window/ClientWindowFrames.java @@ -49,7 +49,7 @@ public class ClientWindowFrames implements Parcelable { public boolean isParentFrameClippedByDisplayCutout; - public float sizeCompatScale = 1f; + public float compatScale = 1f; public ClientWindowFrames() { } @@ -62,7 +62,7 @@ public class ClientWindowFrames implements Parcelable { attachedFrame = new Rect(other.attachedFrame); } isParentFrameClippedByDisplayCutout = other.isParentFrameClippedByDisplayCutout; - sizeCompatScale = other.sizeCompatScale; + compatScale = other.compatScale; } private ClientWindowFrames(Parcel in) { @@ -76,7 +76,7 @@ public class ClientWindowFrames implements Parcelable { parentFrame.readFromParcel(in); attachedFrame = in.readTypedObject(Rect.CREATOR); isParentFrameClippedByDisplayCutout = in.readBoolean(); - sizeCompatScale = in.readFloat(); + compatScale = in.readFloat(); } @Override @@ -86,7 +86,7 @@ public class ClientWindowFrames implements Parcelable { parentFrame.writeToParcel(dest, flags); dest.writeTypedObject(attachedFrame, flags); dest.writeBoolean(isParentFrameClippedByDisplayCutout); - dest.writeFloat(sizeCompatScale); + dest.writeFloat(compatScale); } @Override @@ -97,7 +97,7 @@ public class ClientWindowFrames implements Parcelable { + " 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 a5aefd5157ce..f55932eb05fd 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 abstract class DisplayWindowPolicyController { */ 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/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java index 0956a71bd92d..c2da638aca8d 100644 --- a/core/java/android/window/TransitionInfo.java +++ b/core/java/android/window/TransitionInfo.java @@ -138,8 +138,11 @@ public final class TransitionInfo implements Parcelable { /** The container is a system window, excluding wallpaper and input-method. */ public static final int FLAG_IS_SYSTEM_WINDOW = 1 << 16; + /** The window was animated by back gesture. */ + public static final int FLAG_BACK_GESTURE_ANIMATED = 1 << 17; + /** The first unused bit. This can be used by remotes to attach custom flags to this change. */ - public static final int FLAG_FIRST_CUSTOM = 1 << 17; + public static final int FLAG_FIRST_CUSTOM = 1 << 18; /** The change belongs to a window that won't contain activities. */ public static final int FLAGS_IS_NON_APP_WINDOW = @@ -165,6 +168,7 @@ public final class TransitionInfo implements Parcelable { FLAG_IS_BEHIND_STARTING_WINDOW, FLAG_IS_OCCLUDED, FLAG_IS_SYSTEM_WINDOW, + FLAG_BACK_GESTURE_ANIMATED, FLAG_FIRST_CUSTOM }) public @interface ChangeFlags {} @@ -380,6 +384,9 @@ public final class TransitionInfo implements Parcelable { if ((flags & FLAG_IS_SYSTEM_WINDOW) != 0) { sb.append(sb.length() == 0 ? "" : "|").append("FLAG_IS_SYSTEM_WINDOW"); } + if ((flags & FLAG_BACK_GESTURE_ANIMATED) != 0) { + sb.append(sb.length() == 0 ? "" : "|").append("FLAG_BACK_GESTURE_ANIMATED"); + } if ((flags & FLAG_FIRST_CUSTOM) != 0) { sb.append(sb.length() == 0 ? "" : "|").append("FIRST_CUSTOM"); } diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java index 2d29c5946ede..c7a2d24a8c94 100644 --- a/core/java/android/window/WindowContainerTransaction.java +++ b/core/java/android/window/WindowContainerTransaction.java @@ -454,6 +454,23 @@ public final class WindowContainerTransaction implements Parcelable { } /** + * Sets whether a container is being drag-resized. + * When {@code true}, the client will reuse a single (larger) surface size to avoid + * continuous allocations on every size change. + * + * @param container WindowContainerToken of the task that changed its drag resizing state + * @hide + */ + @NonNull + public WindowContainerTransaction setDragResizing(@NonNull WindowContainerToken container, + boolean dragResizing) { + final Change change = getOrCreateChange(container.asBinder()); + change.mChangeMask |= Change.CHANGE_DRAG_RESIZING; + change.mDragResizing = dragResizing; + return this; + } + + /** * Sends a pending intent in sync. * @param sender The PendingIntent sender. * @param intent The fillIn intent to patch over the sender's base intent. @@ -894,12 +911,14 @@ public final class WindowContainerTransaction implements Parcelable { public static final int CHANGE_IGNORE_ORIENTATION_REQUEST = 1 << 5; public static final int CHANGE_FORCE_NO_PIP = 1 << 6; public static final int CHANGE_FORCE_TRANSLUCENT = 1 << 7; + public static final int CHANGE_DRAG_RESIZING = 1 << 8; private final Configuration mConfiguration = new Configuration(); private boolean mFocusable = true; private boolean mHidden = false; private boolean mIgnoreOrientationRequest = false; private boolean mForceTranslucent = false; + private boolean mDragResizing = false; private int mChangeMask = 0; private @ActivityInfo.Config int mConfigSetMask = 0; @@ -920,6 +939,7 @@ public final class WindowContainerTransaction implements Parcelable { mHidden = in.readBoolean(); mIgnoreOrientationRequest = in.readBoolean(); mForceTranslucent = in.readBoolean(); + mDragResizing = in.readBoolean(); mChangeMask = in.readInt(); mConfigSetMask = in.readInt(); mWindowSetMask = in.readInt(); @@ -968,6 +988,9 @@ public final class WindowContainerTransaction implements Parcelable { if ((other.mChangeMask & CHANGE_FORCE_TRANSLUCENT) != 0) { mForceTranslucent = other.mForceTranslucent; } + if ((other.mChangeMask & CHANGE_DRAG_RESIZING) != 0) { + mDragResizing = other.mDragResizing; + } mChangeMask |= other.mChangeMask; if (other.mActivityWindowingMode >= 0) { mActivityWindowingMode = other.mActivityWindowingMode; @@ -1027,6 +1050,15 @@ public final class WindowContainerTransaction implements Parcelable { return mForceTranslucent; } + /** Gets the requested drag resizing state. */ + public boolean getDragResizing() { + if ((mChangeMask & CHANGE_DRAG_RESIZING) == 0) { + throw new RuntimeException("Drag resizing not set. " + + "Check CHANGE_DRAG_RESIZING first"); + } + return mDragResizing; + } + public int getChangeMask() { return mChangeMask; } @@ -1088,6 +1120,9 @@ public final class WindowContainerTransaction implements Parcelable { if ((mChangeMask & CHANGE_FOCUSABLE) != 0) { sb.append("focusable:" + mFocusable + ","); } + if ((mChangeMask & CHANGE_DRAG_RESIZING) != 0) { + sb.append("dragResizing:" + mDragResizing + ","); + } if (mBoundsChangeTransaction != null) { sb.append("hasBoundsTransaction,"); } @@ -1105,6 +1140,7 @@ public final class WindowContainerTransaction implements Parcelable { dest.writeBoolean(mHidden); dest.writeBoolean(mIgnoreOrientationRequest); dest.writeBoolean(mForceTranslucent); + dest.writeBoolean(mDragResizing); dest.writeInt(mChangeMask); dest.writeInt(mConfigSetMask); dest.writeInt(mWindowSetMask); diff --git a/core/java/com/android/internal/app/IAppOpsService.aidl b/core/java/com/android/internal/app/IAppOpsService.aidl index 30da4b470ab6..88447daf7338 100644 --- a/core/java/com/android/internal/app/IAppOpsService.aidl +++ b/core/java/com/android/internal/app/IAppOpsService.aidl @@ -58,11 +58,12 @@ interface IAppOpsService { SyncNotedAppOp noteProxyOperation(int code, in AttributionSource attributionSource, boolean shouldCollectAsyncNotedOp, String message, boolean shouldCollectMessage, boolean skipProxyOperation); - SyncNotedAppOp startProxyOperation(int code, in AttributionSource attributionSource, - boolean startIfModeDefault, boolean shouldCollectAsyncNotedOp, String message, - boolean shouldCollectMessage, boolean skipProxyOperation, int proxyAttributionFlags, - int proxiedAttributionFlags, int attributionChainId); - void finishProxyOperation(int code, in AttributionSource attributionSource, + SyncNotedAppOp startProxyOperation(IBinder clientId, int code, + in AttributionSource attributionSource, boolean startIfModeDefault, + boolean shouldCollectAsyncNotedOp, String message, boolean shouldCollectMessage, + boolean skipProxyOperation, int proxyAttributionFlags, int proxiedAttributionFlags, + int attributionChainId); + void finishProxyOperation(IBinder clientId, int code, in AttributionSource attributionSource, boolean skipProxyOperation); // Remaining methods are only used in Java. diff --git a/core/java/com/android/internal/inputmethod/IInputMethod.aidl b/core/java/com/android/internal/inputmethod/IInputMethod.aidl index c62fba9fb9b8..1e3714eb342d 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.os.ResultReceiver; 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 @@ oneway interface IInputMethod { 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 4babb7080176..f77e96286bea 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 @@ oneway interface IInputMethodPrivilegedOperations { 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 67c2103450bb..66e3333acf7c 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.IBinder; 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 @@ public final class InputMethodPrivilegedOperations { } /** - * Calls {@link IInputMethodPrivilegedOperations#setImeWindowStatusAsync(int, int}. + * Calls {@link IInputMethodPrivilegedOperations#setImeWindowStatusAsync(int, int)}. * * @param vis visibility flags * @param backDisposition disposition flags @@ -250,7 +251,7 @@ public final class InputMethodPrivilegedOperations { } /** - * 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 @@ public final class InputMethodPrivilegedOperations { /** * 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 @@ public final class InputMethodPrivilegedOperations { } /** - * 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/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java index 3e988e61d978..145aeafb46a1 100644 --- a/core/java/com/android/internal/policy/DecorView.java +++ b/core/java/com/android/internal/policy/DecorView.java @@ -2402,7 +2402,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind return; } final ThreadedRenderer renderer = getThreadedRenderer(); - if (renderer != null) { + if (renderer != null && !CAPTION_ON_SHELL) { loadBackgroundDrawablesIfNeeded(); WindowInsets rootInsets = getRootWindowInsets(); mBackdropFrameRenderer = new BackdropFrameRenderer(this, renderer, diff --git a/core/java/com/android/internal/view/BaseIWindow.java b/core/java/com/android/internal/view/BaseIWindow.java index fe7723658761..2ac43099c741 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.InsetsState; 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 @@ public class BaseIWindow extends IWindow.Stub { } @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 40c6a05650cf..00bc3f2ae530 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 @@ interface IInputMethodManager { + "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/LockSettingsInternal.java b/core/java/com/android/internal/widget/LockSettingsInternal.java index 5b08bb1f42d3..6063c90d6ab9 100644 --- a/core/java/com/android/internal/widget/LockSettingsInternal.java +++ b/core/java/com/android/internal/widget/LockSettingsInternal.java @@ -54,6 +54,12 @@ public abstract class LockSettingsInternal { // TODO(b/183140900) split store escrow key errors into detailed ones. /** + * This is called when Weaver is guaranteed to be available (if the device supports Weaver). + * It does any synthetic password related work that was delayed from earlier in the boot. + */ + public abstract void onThirdPartyAppsStarted(); + + /** * Unlocks the credential-encrypted storage for the given user if the user is not secured, i.e. * doesn't have an LSKF. * <p> diff --git a/core/jni/android_os_PerformanceHintManager.cpp b/core/jni/android_os_PerformanceHintManager.cpp index d05a24fe7c6e..ac1401dbb16d 100644 --- a/core/jni/android_os_PerformanceHintManager.cpp +++ b/core/jni/android_os_PerformanceHintManager.cpp @@ -40,6 +40,7 @@ typedef int64_t (*APH_getPreferredUpdateRateNanos)(APerformanceHintManager* mana 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_getPreferredUpdateRateNanos gAPH_getPreferredUpdateRateNanosFn = nullptr; 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 @@ void ensureAPerformanceHintBindingInitialized() { 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 @@ static void nativeCloseSession(JNIEnv* env, jclass clazz, jlong nativeSessionPtr 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 @@ static const JNINativeMethod gPerformanceHintMethods[] = { {"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/AndroidManifest.xml b/core/res/AndroidManifest.xml index 7ada5483b92b..196ea59fe0f4 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -6184,6 +6184,116 @@ android:label="@string/permlab_foregroundService" android:protectionLevel="normal|instant" /> + <!-- Allows a regular application to use {@link android.app.Service#startForeground + Service.startForeground} with the type "camera". + <p>Protection level: normal|instant + --> + <permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" + android:description="@string/permdesc_foregroundServiceCamera" + android:label="@string/permlab_foregroundServiceCamera" + android:protectionLevel="normal|instant" /> + + <!-- Allows a regular application to use {@link android.app.Service#startForeground + Service.startForeground} with the type "connectedDevice". + <p>Protection level: normal|instant + --> + <permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" + android:description="@string/permdesc_foregroundServiceConnectedDevice" + android:label="@string/permlab_foregroundServiceConnectedDevice" + android:protectionLevel="normal|instant" /> + + <!-- Allows a regular application to use {@link android.app.Service#startForeground + Service.startForeground} with the type "dataSync". + <p>Protection level: normal|instant + --> + <permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" + android:description="@string/permdesc_foregroundServiceDataSync" + android:label="@string/permlab_foregroundServiceDataSync" + android:protectionLevel="normal|instant" /> + + <!-- Allows a regular application to use {@link android.app.Service#startForeground + Service.startForeground} with the type "location". + <p>Protection level: normal|instant + --> + <permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" + android:description="@string/permdesc_foregroundServiceLocation" + android:label="@string/permlab_foregroundServiceLocation" + android:protectionLevel="normal|instant" /> + + <!-- Allows a regular application to use {@link android.app.Service#startForeground + Service.startForeground} with the type "mediaPlayback". + <p>Protection level: normal|instant + --> + <permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" + android:description="@string/permdesc_foregroundServiceMediaPlayback" + android:label="@string/permlab_foregroundServiceMediaPlayback" + android:protectionLevel="normal|instant" /> + + <!-- Allows a regular application to use {@link android.app.Service#startForeground + Service.startForeground} with the type "mediaProjection". + <p>Protection level: normal|instant + --> + <permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" + android:description="@string/permdesc_foregroundServiceMediaProjection" + android:label="@string/permlab_foregroundServiceMediaProjection" + android:protectionLevel="normal|instant" /> + + <!-- Allows a regular application to use {@link android.app.Service#startForeground + Service.startForeground} with the type "microphone". + <p>Protection level: normal|instant + --> + <permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" + android:description="@string/permdesc_foregroundServiceMicrophone" + android:label="@string/permlab_foregroundServiceMicrophone" + android:protectionLevel="normal|instant" /> + + <!-- Allows a regular application to use {@link android.app.Service#startForeground + Service.startForeground} with the type "phoneCall". + <p>Protection level: normal|instant + --> + <permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" + android:description="@string/permdesc_foregroundServicePhoneCall" + android:label="@string/permlab_foregroundServicePhoneCall" + android:protectionLevel="normal|instant" /> + + <!-- Allows a regular application to use {@link android.app.Service#startForeground + Service.startForeground} with the type "health". + <p>Protection level: normal|instant + --> + <permission android:name="android.permission.FOREGROUND_SERVICE_HEALTH" + android:description="@string/permdesc_foregroundServiceHealth" + android:label="@string/permlab_foregroundServiceHealth" + android:protectionLevel="normal|instant" /> + + <!-- Allows a regular application to use {@link android.app.Service#startForeground + Service.startForeground} with the type "remoteMessaging". + <p>Protection level: normal|instant + --> + <permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING" + android:description="@string/permdesc_foregroundServiceRemoteMessaging" + android:label="@string/permlab_foregroundServiceRemoteMessaging" + android:protectionLevel="normal|instant" /> + + <!-- Allows a regular application to use {@link android.app.Service#startForeground + Service.startForeground} with the type "systemExempted". + Apps are allowed to use this type only in the use cases listed in + {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED}. + <p>Protection level: normal|instant + --> + <permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" + android:description="@string/permdesc_foregroundServiceSystemExempted" + android:label="@string/permlab_foregroundServiceSystemExempted" + android:protectionLevel="normal|instant" /> + + <!-- Allows a regular application to use {@link android.app.Service#startForeground + Service.startForeground} with the type "specialUse". + <p>Protection level: signature|appop|instant + --> + <permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" + android:description="@string/permdesc_foregroundServiceSpecialUse" + android:label="@string/permlab_foregroundServiceSpecialUse" + android:protectionLevel="signature|appop|instant" /> + <!-- @SystemApi Allows to access all app shortcuts. @hide --> <permission android:name="android.permission.ACCESS_SHORTCUTS" diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml index eac2b9443631..607467abe24f 100644 --- a/core/res/res/values/attrs_manifest.xml +++ b/core/res/res/values/attrs_manifest.xml @@ -1586,19 +1586,71 @@ together. --> <attr name="foregroundServiceType"> <!-- Data (photo, file, account) upload/download, backup/restore, import/export, fetch, - transfer over network between device and cloud. --> + transfer over network between device and cloud. + + <p>For apps with <code>targetSdkVersion</code> + {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and above, this type should NOT + be used: calling + {@link android.app.Service#startForeground(int, android.app.Notification, int)} with + this type on devices running {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} + is still allowed, but calling it with this type on devices running future platform + releases may get a {@link android.app.ForegroundServiceTypeNotAllowedException}. + --> <flag name="dataSync" value="0x01" /> - <!-- Music, video, news or other media play. --> + <!-- Music, video, news or other media play. + + <p>For apps with <code>targetSdkVersion</code> + {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and above, starting a foreground + service with this type will require permission + {@link android.Manifest.permission#FOREGROUND_SERVICE_MEDIA_PLAYBACK}. + --> <flag name="mediaPlayback" value="0x02" /> <!-- Ongoing operations related to phone calls, video conferencing, - or similar interactive communication. --> + or similar interactive communication. + + <p>For apps with <code>targetSdkVersion</code> + {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and above, starting a foreground + service with this type will require permission + {@link android.Manifest.permission#FOREGROUND_SERVICE_PHONE_CALL} and + {@link android.Manifest.permission#MANAGE_OWN_CALLS}. + --> <flag name="phoneCall" value="0x04" /> - <!-- GPS, map, navigation location update. --> + <!-- GPS, map, navigation location update. + + <p>For apps with <code>targetSdkVersion</code> + {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and above, starting a foreground + service with this type will require permission + {@link android.Manifest.permission#FOREGROUND_SERVICE_LOCATION} and one of the + following permissions: + {@link android.Manifest.permission#ACCESS_COARSE_LOCATION}, + {@link android.Manifest.permission#ACCESS_FINE_LOCATION}. + --> <flag name="location" value="0x08" /> - <!-- Auto, bluetooth, TV or other devices connection, monitoring and interaction. --> + <!-- Auto, bluetooth, TV or other devices connection, monitoring and interaction. + + <p>For apps with <code>targetSdkVersion</code> + {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and above, starting a foreground + service with this type will require permission + {@link android.Manifest.permission#FOREGROUND_SERVICE_CONNECTED_DEVICE} and one of the + following permissions: + {@link android.Manifest.permission#BLUETOOTH_CONNECT}, + {@link android.Manifest.permission#CHANGE_NETWORK_STATE}, + {@link android.Manifest.permission#CHANGE_WIFI_STATE}, + {@link android.Manifest.permission#CHANGE_WIFI_MULTICAST_STATE}, + {@link android.Manifest.permission#NFC}, + {@link android.Manifest.permission#TRANSMIT_IR}, + or has been granted the access to one of the attached USB devices/accessories. + --> <flag name="connectedDevice" value="0x10" /> <!-- Managing a media projection session, e.g, for screen recording or taking - screenshots.--> + screenshots. + + <p>For apps with <code>targetSdkVersion</code> + {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and above, starting a foreground + service with this type will require permission + {@link android.Manifest.permission#FOREGROUND_SERVICE_MEDIA_PROJECTION}, and the user + must have allowed the screen capture request from this app. + --> <flag name="mediaProjection" value="0x20" /> <!-- Use the camera device or record video. @@ -1606,6 +1658,12 @@ and above, a foreground service will not be able to access the camera if this type is not specified in the manifest and in {@link android.app.Service#startForeground(int, android.app.Notification, int)}. + + <p>For apps with <code>targetSdkVersion</code> + {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and above, starting a foreground + service with this type will require permission + {@link android.Manifest.permission#FOREGROUND_SERVICE_CAMERA} and + {@link android.Manifest.permission#CAMERA}. --> <flag name="camera" value="0x40" /> <!--Use the microphone device or record audio. @@ -1614,8 +1672,48 @@ and above, a foreground service will not be able to access the microphone if this type is not specified in the manifest and in {@link android.app.Service#startForeground(int, android.app.Notification, int)}. + + <p>For apps with <code>targetSdkVersion</code> + {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and above, starting a foreground + service with this type will require permission + {@link android.Manifest.permission#FOREGROUND_SERVICE_MICROPHONE} and one of the + following permissions: + {@link android.Manifest.permission#CAPTURE_AUDIO_OUTPUT}, + {@link android.Manifest.permission#RECORD_AUDIO}. --> <flag name="microphone" value="0x80" /> + <!--Health, wellness and fitness. + <p>Requires the app to hold the permission + {@link android.Manifest.permission#FOREGROUND_SERVICE_HEALTH} and one of the following + permissions + {@link android.Manifest.permission#ACTIVITY_RECOGNITION}, + {@link android.Manifest.permission#BODY_SENSORS}, + {@link android.Manifest.permission#HIGH_SAMPLING_RATE_SENSORS}. + --> + <flag name="health" value="0x100" /> + <!-- Messaging use cases which host local server to relay messages across devices. + <p>Requires the app to hold the permission + {@link android.Manifest.permission#FOREGROUND_SERVICE_REMOTE_MESSAGING} in order to use + this type. + --> + <flag name="remoteMessaging" value="0x200" /> + <!-- The system exmpted foreground service use cases. + <p>Requires the app to hold the permission + {@link android.Manifest.permission#FOREGROUND_SERVICE_SYSTEM_EXEMPTED} in order to use + this type. Apps are allowed to use this type only in the use cases listed in + {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED}. + --> + <flag name="systemExempted" value="0x400" /> + <!-- Use cases that can't be categorized into any other foreground service types, but also + can't use @link android.app.job.JobInfo.Builder} APIs. + See {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_SPECIAL_USE} for the + best practice of the use of this type. + + <p>Requires the app to hold the permission + {@link android.Manifest.permission#FOREGROUND_SERVICE_SPECIAL_USE} in order to use + this type. + --> + <flag name="specialUse" value="0x40000000" /> </attr> <!-- Enable sampled memory bug detection in this process. diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 0218215b9946..ac383e69e02e 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 48484c7d41f9..7714082dcfc4 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -1143,6 +1143,66 @@ <string name="permdesc_foregroundService">Allows the app to make use of foreground services.</string> <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permlab_foregroundServiceCamera">run foreground service with the type \"camera\"</string> + <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permdesc_foregroundServiceCamera">Allows the app to make use of foreground services with the type \"camera\"</string> + + <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permlab_foregroundServiceConnectedDevice">run foreground service with the type \"connectedDevice\"</string> + <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permdesc_foregroundServiceConnectedDevice">Allows the app to make use of foreground services with the type \"connectedDevice\"</string> + + <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permlab_foregroundServiceDataSync">run foreground service with the type \"dataSync\"</string> + <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permdesc_foregroundServiceDataSync">Allows the app to make use of foreground services with the type \"dataSync\"</string> + + <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permlab_foregroundServiceLocation">run foreground service with the type \"location\"</string> + <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permdesc_foregroundServiceLocation">Allows the app to make use of foreground services with the type \"location\"</string> + + <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permlab_foregroundServiceMediaPlayback">run foreground service with the type \"mediaPlayback\"</string> + <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permdesc_foregroundServiceMediaPlayback">Allows the app to make use of foreground services with the type \"mediaPlayback\"</string> + + <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permlab_foregroundServiceMediaProjection">run foreground service with the type \"mediaProjection\"</string> + <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permdesc_foregroundServiceMediaProjection">Allows the app to make use of foreground services with the type \"mediaProjection\"</string> + + <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permlab_foregroundServiceMicrophone">run foreground service with the type \"microphone\"</string> + <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permdesc_foregroundServiceMicrophone">Allows the app to make use of foreground services with the type \"microphone\"</string> + + <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permlab_foregroundServicePhoneCall">run foreground service with the type \"phoneCall\"</string> + <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permdesc_foregroundServicePhoneCall">Allows the app to make use of foreground services with the type \"phoneCall\"</string> + + <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permlab_foregroundServiceHealth">run foreground service with the type \"health\"</string> + <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permdesc_foregroundServiceHealth">Allows the app to make use of foreground services with the type \"health\"</string> + + <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permlab_foregroundServiceRemoteMessaging">run foreground service with the type \"remoteMessaging\"</string> + <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permdesc_foregroundServiceRemoteMessaging">Allows the app to make use of foreground services with the type \"remoteMessaging\"</string> + + <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permlab_foregroundServiceSystemExempted">run foreground service with the type \"systemExempted\"</string> + <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permdesc_foregroundServiceSystemExempted">Allows the app to make use of foreground services with the type \"systemExempted\"</string> + + <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permlab_foregroundServiceSpecialUse">run foreground service with the type \"specialUse\"</string> + <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permdesc_foregroundServiceSpecialUse">Allows the app to make use of foreground services with the type \"specialUse\"</string> + + <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. --> <string name="permlab_getPackageSize">measure app storage space</string> <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> <string name="permdesc_getPackageSize">Allows the app to retrieve its code, data, and cache sizes</string> @@ -6331,6 +6391,8 @@ ul.</string> <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 b5d534f9d662..89ec5ba48142 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/res/res/xml/config_user_types.xml b/core/res/res/xml/config_user_types.xml index 766315029f3d..df6b7b240b38 100644 --- a/core/res/res/xml/config_user_types.xml +++ b/core/res/res/xml/config_user_types.xml @@ -83,6 +83,8 @@ Supported optional properties (to be used as shown in the example above) are as For profile and full users: default-restrictions (with values defined in UserRestrictionUtils.USER_RESTRICTIONS) enabled + user-properties + max-allowed For profile users only: max-allowed-per-parent icon-badge 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 14dd5df1716f..a2df426c315c 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.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; 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 f28e27d7f896..5ab943542f81 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 @@ package com.android.server.broadcastradio; 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 2cb058bd61a3..a4212180d0b5 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 @@ final class AidlTestUtils { 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 d061a778e369..635d1e792715 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 @@ public final class BroadcastRadioServiceImplTest extends ExtendedRadioMockitoTes 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 @@ public final class BroadcastRadioServiceImplTest extends ExtendedRadioMockitoTes 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 c1a43367a13b..7a8475fe4d8f 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 @@ package com.android.server.broadcastradio.aidl; 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 @@ public final class RadioModuleTest { 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 @@ public final class RadioModuleTest { 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 c3623a28c17a..3bf993c07aed 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 @@ package com.android.server.broadcastradio.aidl; 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 @@ public final class TunerSessionTest { @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 @@ public final class TunerSessionTest { 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 @@ public final class TunerSessionTest { 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 000000000000..4d0b753b0acc --- /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 000000000000..48f5a461d631 --- /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 25bf93f72088..d1043595535a 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 class StartProgramListUpdatesFanoutTest { 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 ad1b8727d8a7..4eedd2fdb369 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 @@ final class TestUtils { 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 9b62dc75419f..936e606fcb76 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.ITunerSession; 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.junit.MockitoJUnitRunner; 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 @@ public final class TunerSessionHidlTest { 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 @@ public final class TunerSessionHidlTest { @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 @@ public final class TunerSessionHidlTest { }).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 @@ public final class TunerSessionHidlTest { 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 @@ public final class TunerSessionHidlTest { 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/app/activity/LocalReceiver.java b/core/tests/coretests/src/android/app/activity/LocalReceiver.java index 7f813390552c..5ac84f809564 100644 --- a/core/tests/coretests/src/android/app/activity/LocalReceiver.java +++ b/core/tests/coretests/src/android/app/activity/LocalReceiver.java @@ -36,7 +36,8 @@ public class LocalReceiver extends BroadcastReceiver { if (BroadcastTest.BROADCAST_FAIL_REGISTER.equals(intent.getAction())) { resultString = "Successfully registered, but expected it to fail"; try { - context.registerReceiver(this, new IntentFilter("foo.bar")); + context.registerReceiver(this, new IntentFilter("foo.bar"), + Context.RECEIVER_EXPORTED_UNAUDITED); context.unregisterReceiver(this); } catch (ReceiverCallNotAllowedException e) { //resultString = "This is the correct behavior but not yet implemented"; diff --git a/core/tests/coretests/src/android/app/activity/ServiceTest.java b/core/tests/coretests/src/android/app/activity/ServiceTest.java index c89f37db7fed..3f3d6a3bff34 100644 --- a/core/tests/coretests/src/android/app/activity/ServiceTest.java +++ b/core/tests/coretests/src/android/app/activity/ServiceTest.java @@ -172,7 +172,7 @@ public class ServiceTest extends TestCase { pidResult.complete(intent.getIntExtra(EXTRA_PID, NOT_STARTED)); mContext.unregisterReceiver(this); } - }, new IntentFilter(ACTION_SERVICE_STARTED)); + }, new IntentFilter(ACTION_SERVICE_STARTED), Context.RECEIVER_EXPORTED_UNAUDITED); serviceTrigger.run(); try { diff --git a/core/tests/coretests/src/android/app/backup/BackupRestoreEventLoggerTest.java b/core/tests/coretests/src/android/app/backup/BackupRestoreEventLoggerTest.java index 67b24ec17a27..bbd2ef38d786 100644 --- a/core/tests/coretests/src/android/app/backup/BackupRestoreEventLoggerTest.java +++ b/core/tests/coretests/src/android/app/backup/BackupRestoreEventLoggerTest.java @@ -63,18 +63,22 @@ public class BackupRestoreEventLoggerTest { public void testBackupLogger_rejectsRestoreLogs() { mLogger = new BackupRestoreEventLogger(BACKUP); - assertThat(mLogger.logItemsRestored(DATA_TYPE_1, /* count */ 5)).isFalse(); - assertThat(mLogger.logItemsRestoreFailed(DATA_TYPE_1, /* count */ 5, ERROR_1)).isFalse(); - assertThat(mLogger.logRestoreMetadata(DATA_TYPE_1, /* metadata */ "metadata")).isFalse(); + mLogger.logItemsRestored(DATA_TYPE_1, /* count */ 5); + mLogger.logItemsRestoreFailed(DATA_TYPE_1, /* count */ 5, ERROR_1); + mLogger.logRestoreMetadata(DATA_TYPE_1, /* metadata */ "metadata"); + + assertThat(getResultForDataTypeIfPresent(mLogger, DATA_TYPE_1)).isEqualTo(Optional.empty()); } @Test public void testRestoreLogger_rejectsBackupLogs() { mLogger = new BackupRestoreEventLogger(RESTORE); - assertThat(mLogger.logItemsBackedUp(DATA_TYPE_1, /* count */ 5)).isFalse(); - assertThat(mLogger.logItemsBackupFailed(DATA_TYPE_1, /* count */ 5, ERROR_1)).isFalse(); - assertThat(mLogger.logBackupMetaData(DATA_TYPE_1, /* metadata */ "metadata")).isFalse(); + mLogger.logItemsBackedUp(DATA_TYPE_1, /* count */ 5); + mLogger.logItemsBackupFailed(DATA_TYPE_1, /* count */ 5, ERROR_1); + mLogger.logBackupMetaData(DATA_TYPE_1, /* metadata */ "metadata"); + + assertThat(getResultForDataTypeIfPresent(mLogger, DATA_TYPE_1)).isEqualTo(Optional.empty()); } @Test @@ -83,16 +87,17 @@ public class BackupRestoreEventLoggerTest { for (int i = 0; i < DATA_TYPES_ALLOWED; i++) { String dataType = DATA_TYPE_1 + i; - assertThat(mLogger.logItemsBackedUp(dataType, /* count */ 5)).isTrue(); - assertThat(mLogger.logItemsBackupFailed(dataType, /* count */ 5, /* error */ null)) - .isTrue(); - assertThat(mLogger.logBackupMetaData(dataType, METADATA_1)).isTrue(); + mLogger.logItemsBackedUp(dataType, /* count */ 5); + mLogger.logItemsBackupFailed(dataType, /* count */ 5, /* error */ null); + mLogger.logBackupMetaData(dataType, METADATA_1); + + assertThat(getResultForDataTypeIfPresent(mLogger, dataType)).isNotEqualTo( + Optional.empty()); } - assertThat(mLogger.logItemsBackedUp(DATA_TYPE_2, /* count */ 5)).isFalse(); - assertThat(mLogger.logItemsBackupFailed(DATA_TYPE_2, /* count */ 5, /* error */ null)) - .isFalse(); - assertThat(mLogger.logRestoreMetadata(DATA_TYPE_2, METADATA_1)).isFalse(); + mLogger.logItemsBackedUp(DATA_TYPE_2, /* count */ 5); + mLogger.logItemsBackupFailed(DATA_TYPE_2, /* count */ 5, /* error */ null); + mLogger.logRestoreMetadata(DATA_TYPE_2, METADATA_1); assertThat(getResultForDataTypeIfPresent(mLogger, DATA_TYPE_2)).isEqualTo(Optional.empty()); } @@ -102,16 +107,17 @@ public class BackupRestoreEventLoggerTest { for (int i = 0; i < DATA_TYPES_ALLOWED; i++) { String dataType = DATA_TYPE_1 + i; - assertThat(mLogger.logItemsRestored(dataType, /* count */ 5)).isTrue(); - assertThat(mLogger.logItemsRestoreFailed(dataType, /* count */ 5, /* error */ null)) - .isTrue(); - assertThat(mLogger.logRestoreMetadata(dataType, METADATA_1)).isTrue(); + mLogger.logItemsRestored(dataType, /* count */ 5); + mLogger.logItemsRestoreFailed(dataType, /* count */ 5, /* error */ null); + mLogger.logRestoreMetadata(dataType, METADATA_1); + + assertThat(getResultForDataTypeIfPresent(mLogger, dataType)).isNotEqualTo( + Optional.empty()); } - assertThat(mLogger.logItemsRestored(DATA_TYPE_2, /* count */ 5)).isFalse(); - assertThat(mLogger.logItemsRestoreFailed(DATA_TYPE_2, /* count */ 5, /* error */ null)) - .isFalse(); - assertThat(mLogger.logRestoreMetadata(DATA_TYPE_2, METADATA_1)).isFalse(); + mLogger.logItemsRestored(DATA_TYPE_2, /* count */ 5); + mLogger.logItemsRestoreFailed(DATA_TYPE_2, /* count */ 5, /* error */ null); + mLogger.logRestoreMetadata(DATA_TYPE_2, METADATA_1); assertThat(getResultForDataTypeIfPresent(mLogger, DATA_TYPE_2)).isEqualTo(Optional.empty()); } diff --git a/core/tests/coretests/src/android/app/timezonedetector/ParcelableTestSupport.java b/core/tests/coretests/src/android/app/time/ParcelableTestSupport.java index 0073d86873da..13e5e14ce01c 100644 --- a/core/tests/coretests/src/android/app/timezonedetector/ParcelableTestSupport.java +++ b/core/tests/coretests/src/android/app/time/ParcelableTestSupport.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package android.app.timezonedetector; +package android.app.time; import static org.junit.Assert.assertEquals; @@ -48,6 +48,13 @@ public final class ParcelableTestSupport { } public static <T extends Parcelable> void assertRoundTripParcelable(T instance) { - assertEquals(instance, roundTripParcelable(instance)); + assertEqualsAndHashCode(instance, roundTripParcelable(instance)); + } + + /** Asserts that the objects are equal and return identical hash codes. */ + public static void assertEqualsAndHashCode(Object one, Object two) { + assertEquals(one, two); + assertEquals(two, one); + assertEquals(one.hashCode(), two.hashCode()); } } diff --git a/core/tests/coretests/src/android/app/time/TimeCapabilitiesTest.java b/core/tests/coretests/src/android/app/time/TimeCapabilitiesTest.java index c9b96c6071ee..1a276ad0b3c3 100644 --- a/core/tests/coretests/src/android/app/time/TimeCapabilitiesTest.java +++ b/core/tests/coretests/src/android/app/time/TimeCapabilitiesTest.java @@ -21,7 +21,8 @@ import static android.app.time.Capabilities.CAPABILITY_NOT_ALLOWED; import static android.app.time.Capabilities.CAPABILITY_NOT_APPLICABLE; import static android.app.time.Capabilities.CAPABILITY_NOT_SUPPORTED; import static android.app.time.Capabilities.CAPABILITY_POSSESSED; -import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable; +import static android.app.time.ParcelableTestSupport.assertEqualsAndHashCode; +import static android.app.time.ParcelableTestSupport.assertRoundTripParcelable; import static com.google.common.truth.Truth.assertThat; @@ -55,7 +56,7 @@ public class TimeCapabilitiesTest { { TimeCapabilities one = builder1.build(); TimeCapabilities two = builder2.build(); - assertEquals(one, two); + assertEqualsAndHashCode(one, two); } builder2.setConfigureAutoDetectionEnabledCapability(CAPABILITY_NOT_ALLOWED); @@ -69,7 +70,7 @@ public class TimeCapabilitiesTest { { TimeCapabilities one = builder1.build(); TimeCapabilities two = builder2.build(); - assertEquals(one, two); + assertEqualsAndHashCode(one, two); } builder2.setSetManualTimeCapability(CAPABILITY_NOT_ALLOWED); @@ -83,7 +84,7 @@ public class TimeCapabilitiesTest { { TimeCapabilities one = builder1.build(); TimeCapabilities two = builder2.build(); - assertEquals(one, two); + assertEqualsAndHashCode(one, two); } } diff --git a/core/tests/coretests/src/android/app/time/TimeStateTest.java b/core/tests/coretests/src/android/app/time/TimeStateTest.java index bce09099e4d3..25e6e2b66219 100644 --- a/core/tests/coretests/src/android/app/time/TimeStateTest.java +++ b/core/tests/coretests/src/android/app/time/TimeStateTest.java @@ -16,7 +16,8 @@ package android.app.time; -import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable; +import static android.app.time.ParcelableTestSupport.assertEqualsAndHashCode; +import static android.app.time.ParcelableTestSupport.assertRoundTripParcelable; import static android.app.timezonedetector.ShellCommandTestSupport.createShellCommandWithArgsAndOptions; import static org.junit.Assert.assertEquals; @@ -52,11 +53,6 @@ public class TimeStateTest { assertNotEquals(time1False_1, time2False); } - private static void assertEqualsAndHashCode(Object one, Object two) { - assertEquals(one, two); - assertEquals(one.hashCode(), two.hashCode()); - } - @Test public void testParceling() { UnixEpochTime time = new UnixEpochTime(1, 2); diff --git a/core/tests/coretests/src/android/app/time/TimeZoneCapabilitiesTest.java b/core/tests/coretests/src/android/app/time/TimeZoneCapabilitiesTest.java index 3f7da8a6fbd0..8bed31f4821a 100644 --- a/core/tests/coretests/src/android/app/time/TimeZoneCapabilitiesTest.java +++ b/core/tests/coretests/src/android/app/time/TimeZoneCapabilitiesTest.java @@ -18,7 +18,7 @@ package android.app.time; import static android.app.time.Capabilities.CAPABILITY_NOT_ALLOWED; import static android.app.time.Capabilities.CAPABILITY_POSSESSED; -import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable; +import static android.app.time.ParcelableTestSupport.assertRoundTripParcelable; import static com.google.common.truth.Truth.assertThat; diff --git a/core/tests/coretests/src/android/app/time/TimeZoneStateTest.java b/core/tests/coretests/src/android/app/time/TimeZoneStateTest.java index 35a9dbc200e4..595b7006c0e5 100644 --- a/core/tests/coretests/src/android/app/time/TimeZoneStateTest.java +++ b/core/tests/coretests/src/android/app/time/TimeZoneStateTest.java @@ -16,7 +16,8 @@ package android.app.time; -import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable; +import static android.app.time.ParcelableTestSupport.assertEqualsAndHashCode; +import static android.app.time.ParcelableTestSupport.assertRoundTripParcelable; import static android.app.timezonedetector.ShellCommandTestSupport.createShellCommandWithArgsAndOptions; import static org.junit.Assert.assertEquals; @@ -52,11 +53,6 @@ public class TimeZoneStateTest { assertNotEquals(zone1False_1, zone2False); } - private static void assertEqualsAndHashCode(Object one, Object two) { - assertEquals(one, two); - assertEquals(one.hashCode(), two.hashCode()); - } - @Test public void testParceling() { assertRoundTripParcelable(new TimeZoneState("Europe/London", true)); diff --git a/core/tests/coretests/src/android/app/timedetector/ManualTimeSuggestionTest.java b/core/tests/coretests/src/android/app/timedetector/ManualTimeSuggestionTest.java index 0c7c8c1803eb..28da164952ec 100644 --- a/core/tests/coretests/src/android/app/timedetector/ManualTimeSuggestionTest.java +++ b/core/tests/coretests/src/android/app/timedetector/ManualTimeSuggestionTest.java @@ -16,8 +16,8 @@ package android.app.timedetector; -import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable; -import static android.app.timezonedetector.ParcelableTestSupport.roundTripParcelable; +import static android.app.time.ParcelableTestSupport.assertRoundTripParcelable; +import static android.app.time.ParcelableTestSupport.roundTripParcelable; import static android.app.timezonedetector.ShellCommandTestSupport.createShellCommandWithArgsAndOptions; import static org.junit.Assert.assertEquals; diff --git a/core/tests/coretests/src/android/app/timedetector/TelephonyTimeSuggestionTest.java b/core/tests/coretests/src/android/app/timedetector/TelephonyTimeSuggestionTest.java index 26cb90231931..e9ca069d372d 100644 --- a/core/tests/coretests/src/android/app/timedetector/TelephonyTimeSuggestionTest.java +++ b/core/tests/coretests/src/android/app/timedetector/TelephonyTimeSuggestionTest.java @@ -16,8 +16,8 @@ package android.app.timedetector; -import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable; -import static android.app.timezonedetector.ParcelableTestSupport.roundTripParcelable; +import static android.app.time.ParcelableTestSupport.assertRoundTripParcelable; +import static android.app.time.ParcelableTestSupport.roundTripParcelable; import static android.app.timezonedetector.ShellCommandTestSupport.createShellCommandWithArgsAndOptions; import static org.junit.Assert.assertEquals; diff --git a/core/tests/coretests/src/android/app/timezonedetector/ManualTimeZoneSuggestionTest.java b/core/tests/coretests/src/android/app/timezonedetector/ManualTimeZoneSuggestionTest.java index 17838bb19a8f..b5bdea71c3b6 100644 --- a/core/tests/coretests/src/android/app/timezonedetector/ManualTimeZoneSuggestionTest.java +++ b/core/tests/coretests/src/android/app/timezonedetector/ManualTimeZoneSuggestionTest.java @@ -16,8 +16,8 @@ package android.app.timezonedetector; -import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable; -import static android.app.timezonedetector.ParcelableTestSupport.roundTripParcelable; +import static android.app.time.ParcelableTestSupport.assertRoundTripParcelable; +import static android.app.time.ParcelableTestSupport.roundTripParcelable; import static android.app.timezonedetector.ShellCommandTestSupport.createShellCommandWithArgsAndOptions; import static org.junit.Assert.assertEquals; diff --git a/core/tests/coretests/src/android/app/timezonedetector/TelephonyTimeZoneSuggestionTest.java b/core/tests/coretests/src/android/app/timezonedetector/TelephonyTimeZoneSuggestionTest.java index 28009d4168ad..d5dcac2b2aee 100644 --- a/core/tests/coretests/src/android/app/timezonedetector/TelephonyTimeZoneSuggestionTest.java +++ b/core/tests/coretests/src/android/app/timezonedetector/TelephonyTimeZoneSuggestionTest.java @@ -16,8 +16,8 @@ package android.app.timezonedetector; -import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable; -import static android.app.timezonedetector.ParcelableTestSupport.roundTripParcelable; +import static android.app.time.ParcelableTestSupport.assertRoundTripParcelable; +import static android.app.time.ParcelableTestSupport.roundTripParcelable; import static android.app.timezonedetector.ShellCommandTestSupport.createShellCommandWithArgsAndOptions; import static org.junit.Assert.assertEquals; diff --git a/core/tests/coretests/src/android/os/PerformanceHintManagerTest.java b/core/tests/coretests/src/android/os/PerformanceHintManagerTest.java index 69eb13f7854a..d1d14f6fbcb4 100644 --- a/core/tests/coretests/src/android/os/PerformanceHintManagerTest.java +++ b/core/tests/coretests/src/android/os/PerformanceHintManagerTest.java @@ -114,6 +114,23 @@ public class PerformanceHintManagerTest { } @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 7ebebc965d8d..c59a3f518da8 100644 --- a/core/tests/coretests/src/android/os/VibratorTest.java +++ b/core/tests/coretests/src/android/os/VibratorTest.java @@ -246,10 +246,12 @@ public class VibratorTest { @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 @@ public class VibratorTest { 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 @@ public class VibratorTest { 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 @@ public class VibratorTest { 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 @@ public class VibratorTest { 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 @@ public class VibratorTest { 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/service/timezone/TimeZoneProviderEventTest.java b/core/tests/coretests/src/android/service/timezone/TimeZoneProviderEventTest.java index 86ebdf3c0759..7f772dd3b12d 100644 --- a/core/tests/coretests/src/android/service/timezone/TimeZoneProviderEventTest.java +++ b/core/tests/coretests/src/android/service/timezone/TimeZoneProviderEventTest.java @@ -16,7 +16,7 @@ package android.service.timezone; -import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable; +import static android.app.time.ParcelableTestSupport.assertRoundTripParcelable; import static android.service.timezone.TimeZoneProviderEvent.EVENT_TYPE_PERMANENT_FAILURE; import static android.service.timezone.TimeZoneProviderEvent.EVENT_TYPE_SUGGESTION; import static android.service.timezone.TimeZoneProviderEvent.EVENT_TYPE_UNCERTAIN; diff --git a/core/tests/coretests/src/android/service/timezone/TimeZoneProviderStatusTest.java b/core/tests/coretests/src/android/service/timezone/TimeZoneProviderStatusTest.java index b7a595c8c748..9006cd91d616 100644 --- a/core/tests/coretests/src/android/service/timezone/TimeZoneProviderStatusTest.java +++ b/core/tests/coretests/src/android/service/timezone/TimeZoneProviderStatusTest.java @@ -16,20 +16,15 @@ package android.service.timezone; -import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable; -import static android.service.timezone.TimeZoneProviderStatus.DEPENDENCY_STATUS_BLOCKED_BY_ENVIRONMENT; import static android.service.timezone.TimeZoneProviderStatus.DEPENDENCY_STATUS_BLOCKED_BY_SETTINGS; import static android.service.timezone.TimeZoneProviderStatus.DEPENDENCY_STATUS_OK; -import static android.service.timezone.TimeZoneProviderStatus.OPERATION_STATUS_FAILED; import static android.service.timezone.TimeZoneProviderStatus.OPERATION_STATUS_OK; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotSame; -import static org.junit.Assert.assertThrows; import org.junit.Test; +/** Non-SDK tests. See CTS for SDK API tests. */ public class TimeZoneProviderStatusTest { @Test @@ -42,81 +37,4 @@ public class TimeZoneProviderStatusTest { assertEquals(status, TimeZoneProviderStatus.parseProviderStatus(status.toString())); } - - @Test - public void testStatusValidation() { - TimeZoneProviderStatus status = new TimeZoneProviderStatus.Builder() - .setLocationDetectionDependencyStatus(DEPENDENCY_STATUS_OK) - .setConnectivityDependencyStatus(DEPENDENCY_STATUS_OK) - .setTimeZoneResolutionOperationStatus(OPERATION_STATUS_OK) - .build(); - - assertThrows(IllegalArgumentException.class, - () -> new TimeZoneProviderStatus.Builder(status) - .setLocationDetectionDependencyStatus(-1) - .build()); - assertThrows(IllegalArgumentException.class, - () -> new TimeZoneProviderStatus.Builder(status) - .setConnectivityDependencyStatus(-1) - .build()); - assertThrows(IllegalArgumentException.class, - () -> new TimeZoneProviderStatus.Builder(status) - .setTimeZoneResolutionOperationStatus(-1) - .build()); - } - - @Test - public void testEqualsAndHashcode() { - TimeZoneProviderStatus status1_1 = new TimeZoneProviderStatus.Builder() - .setLocationDetectionDependencyStatus(DEPENDENCY_STATUS_OK) - .setConnectivityDependencyStatus(DEPENDENCY_STATUS_OK) - .setTimeZoneResolutionOperationStatus(OPERATION_STATUS_OK) - .build(); - assertEqualsAndHashcode(status1_1, status1_1); - assertNotEquals(status1_1, null); - - { - TimeZoneProviderStatus status1_2 = - new TimeZoneProviderStatus.Builder(status1_1).build(); - assertEqualsAndHashcode(status1_1, status1_2); - assertNotSame(status1_1, status1_2); - } - - { - TimeZoneProviderStatus status2 = new TimeZoneProviderStatus.Builder(status1_1) - .setLocationDetectionDependencyStatus(DEPENDENCY_STATUS_BLOCKED_BY_ENVIRONMENT) - .build(); - assertNotEquals(status1_1, status2); - } - - { - TimeZoneProviderStatus status2 = new TimeZoneProviderStatus.Builder(status1_1) - .setConnectivityDependencyStatus(DEPENDENCY_STATUS_BLOCKED_BY_ENVIRONMENT) - .build(); - assertNotEquals(status1_1, status2); - } - - { - TimeZoneProviderStatus status2 = new TimeZoneProviderStatus.Builder(status1_1) - .setTimeZoneResolutionOperationStatus(OPERATION_STATUS_FAILED) - .build(); - assertNotEquals(status1_1, status2); - } - } - - private static void assertEqualsAndHashcode(Object one, Object two) { - assertEquals(one, two); - assertEquals(two, one); - assertEquals(one.hashCode(), two.hashCode()); - } - - @Test - public void testParcelable() { - TimeZoneProviderStatus status = new TimeZoneProviderStatus.Builder() - .setLocationDetectionDependencyStatus(DEPENDENCY_STATUS_OK) - .setConnectivityDependencyStatus(DEPENDENCY_STATUS_BLOCKED_BY_ENVIRONMENT) - .setTimeZoneResolutionOperationStatus(OPERATION_STATUS_FAILED) - .build(); - assertRoundTripParcelable(status); - } } diff --git a/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java b/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java index ddcb17544ec7..0bf133fe01b6 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 android.view.WindowManager.LayoutParams.TYPE_APPLICATION; 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 @@ public class ImeInsetsSourceConsumerTest { 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 @@ public class ImeInsetsSourceConsumerTest { 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 @@ public class ImeInsetsSourceConsumerTest { 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 @@ public class ImeInsetsSourceConsumerTest { 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 @@ public class ImeInsetsSourceConsumerTest { 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 e9cd8ad7d5c2..c88255ef0e00 100644 --- a/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java +++ b/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java @@ -109,7 +109,8 @@ public class InsetsAnimationControlImplTest { 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 409bae8addc2..c6fa778763a8 100644 --- a/core/tests/coretests/src/android/view/InsetsControllerTest.java +++ b/core/tests/coretests/src/android/view/InsetsControllerTest.java @@ -248,7 +248,7 @@ public class InsetsControllerTest { 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 @@ public class InsetsControllerTest { 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 @@ public class InsetsControllerTest { 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 @@ public class InsetsControllerTest { 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 @@ public class InsetsControllerTest { 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 @@ public class InsetsControllerTest { 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 @@ public class InsetsControllerTest { 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 @@ public class InsetsControllerTest { // 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 32085c1cfbeb..cc02bbb3e7d1 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 @@ public class AccessibilityNodeInfoTest { // 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/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java b/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java index 35d5948bc2af..7a5ab0458406 100644 --- a/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java +++ b/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java @@ -27,6 +27,7 @@ import android.os.Bundle; import android.os.IBinder; import android.os.RemoteCallback; import android.os.RemoteException; +import android.window.ScreenCapture; import java.util.Collections; import java.util.List; @@ -180,6 +181,10 @@ public class AccessibilityServiceConnectionImpl extends IAccessibilityServiceCon public void takeScreenshot(int displayId, RemoteCallback callback) {} + public void takeScreenshotOfWindow(int accessibilityWindowId, int interactionId, + ScreenCapture.ScreenCaptureListener listener, + IAccessibilityInteractionConnectionCallback callback) {} + public void setFocusAppearance(int strokeWidth, int color) {} public void setCacheEnabled(boolean enabled) {} diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index decfb9fc59df..3e2b71f18b75 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -495,6 +495,10 @@ applications that come with the platform <permission name="android.permission.READ_SAFETY_CENTER_STATUS" /> <!-- Permission required for CTS test - CtsTelephonyTestCases --> <permission name="android.permission.BIND_TELECOM_CONNECTION_SERVICE" /> + <!-- Permission required for CTS test - CtsAppTestCases --> + <permission name="android.permission.CAPTURE_MEDIA_OUTPUT" /> + <permission name="android.permission.CAPTURE_TUNER_AUDIO_INPUT" /> + <permission name="android.permission.CAPTURE_VOICE_COMMUNICATION_OUTPUT" /> </privapp-permissions> <privapp-permissions package="com.android.statementservice"> diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index c1b4dcc1d3ff..4cc06e33ab62 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -2179,12 +2179,6 @@ "group": "WM_DEBUG_ANIM", "at": "com\/android\/server\/wm\/WindowContainer.java" }, - "-23020844": { - "message": "Back: Reset surfaces", - "level": "DEBUG", - "group": "WM_DEBUG_BACK_PREVIEW", - "at": "com\/android\/server\/wm\/BackNavigationController.java" - }, "-21399771": { "message": "activity %s already destroying, skipping request with reason:%s", "level": "VERBOSE", @@ -3823,12 +3817,6 @@ "group": "WM_DEBUG_APP_TRANSITIONS", "at": "com\/android\/server\/wm\/ActivityRecord.java" }, - "1544805551": { - "message": "Skipping app transition animation. task=%s", - "level": "DEBUG", - "group": "WM_DEBUG_BACK_PREVIEW", - "at": "com\/android\/server\/wm\/Task.java" - }, "1557732761": { "message": "For Intent %s bringing to top: %s", "level": "DEBUG", diff --git a/graphics/java/android/graphics/Canvas.java b/graphics/java/android/graphics/Canvas.java index abf7e9911086..42c892a240b6 100644 --- a/graphics/java/android/graphics/Canvas.java +++ b/graphics/java/android/graphics/Canvas.java @@ -1667,6 +1667,9 @@ public class Canvas extends BaseCanvas { * effectively treating them as zeros. In API level {@value Build.VERSION_CODES#P} and above * these parameters will be respected. * + * <p>Note: antialiasing is not supported, therefore {@link Paint#ANTI_ALIAS_FLAG} is + * ignored.</p> + * * @param bitmap The bitmap to draw using the mesh * @param meshWidth The number of columns in the mesh. Nothing is drawn if this is 0 * @param meshHeight The number of rows in the mesh. Nothing is drawn if this is 0 @@ -1678,7 +1681,7 @@ public class Canvas extends BaseCanvas { * null, there must be at least (meshWidth+1) * (meshHeight+1) + colorOffset values * in the array. * @param colorOffset Number of color elements to skip before drawing - * @param paint May be null. The paint used to draw the bitmap + * @param paint May be null. The paint used to draw the bitmap. Antialiasing is not supported. */ public void drawBitmapMesh(@NonNull Bitmap bitmap, int meshWidth, int meshHeight, @NonNull float[] verts, int vertOffset, @Nullable int[] colors, int colorOffset, @@ -1832,9 +1835,12 @@ public class Canvas extends BaseCanvas { /** * Draws the specified bitmap as an N-patch (most often, a 9-patch.) * + * <p>Note: antialiasing is not supported, therefore {@link Paint#ANTI_ALIAS_FLAG} is + * ignored.</p> + * * @param patch The ninepatch object to render * @param dst The destination rectangle. - * @param paint The paint to draw the bitmap with. may be null + * @param paint The paint to draw the bitmap with. May be null. Antialiasing is not supported. */ public void drawPatch(@NonNull NinePatch patch, @NonNull Rect dst, @Nullable Paint paint) { super.drawPatch(patch, dst, paint); @@ -1843,9 +1849,12 @@ public class Canvas extends BaseCanvas { /** * Draws the specified bitmap as an N-patch (most often, a 9-patch.) * + * <p>Note: antialiasing is not supported, therefore {@link Paint#ANTI_ALIAS_FLAG} is + * ignored.</p> + * * @param patch The ninepatch object to render * @param dst The destination rectangle. - * @param paint The paint to draw the bitmap with. may be null + * @param paint The paint to draw the bitmap with. May be null. Antialiasing is not supported. */ public void drawPatch(@NonNull NinePatch patch, @NonNull RectF dst, @Nullable Paint paint) { super.drawPatch(patch, dst, paint); @@ -2278,6 +2287,9 @@ public class Canvas extends BaseCanvas { * array is optional, but if it is present, then it is used to specify the index of each * triangle, rather than just walking through the arrays in order. * + * <p>Note: antialiasing is not supported, therefore {@link Paint#ANTI_ALIAS_FLAG} is + * ignored.</p> + * * @param mode How to interpret the array of vertices * @param vertexCount The number of values in the vertices array (and corresponding texs and * colors arrays if non-null). Each logical vertex is two values (x, y), vertexCount @@ -2292,8 +2304,9 @@ public class Canvas extends BaseCanvas { * @param colorOffset Number of values in colors to skip before drawing. * @param indices If not null, array of indices to reference into the vertex (texs, colors) * array. - * @param indexCount number of entries in the indices array (if not null). - * @param paint Specifies the shader to use if the texs array is non-null. + * @param indexCount Number of entries in the indices array (if not null). + * @param paint Specifies the shader to use if the texs array is non-null. Antialiasing is not + * supported. */ public void drawVertices(@NonNull VertexMode mode, int vertexCount, @NonNull float[] verts, int vertOffset, @Nullable float[] texs, int texOffset, @Nullable int[] colors, diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java index 1a80ab308bb5..f0e496f3a178 100644 --- a/graphics/java/android/graphics/Paint.java +++ b/graphics/java/android/graphics/Paint.java @@ -137,6 +137,13 @@ public class Paint { * <p>Enabling this flag will cause all draw operations that support * antialiasing to use it.</p> * + * <p>Notable draw operations that do <b>not</b> support antialiasing include:</p> + * <ul> + * <li>{@link android.graphics.Canvas#drawBitmapMesh}</li> + * <li>{@link android.graphics.Canvas#drawPatch}</li> + * <li>{@link android.graphics.Canvas#drawVertices}</li> + * </ul> + * * @see #Paint(int) * @see #setFlags(int) */ diff --git a/graphics/java/android/graphics/fonts/FontStyle.java b/graphics/java/android/graphics/fonts/FontStyle.java index 09799fdf5a13..48969aa71059 100644 --- a/graphics/java/android/graphics/fonts/FontStyle.java +++ b/graphics/java/android/graphics/fonts/FontStyle.java @@ -48,6 +48,10 @@ public final class FontStyle { 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/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml index df5f921f3a62..c6197c8a730b 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/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java index dec1e38914e2..065fd95b3ebc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java @@ -16,14 +16,12 @@ package com.android.wm.shell; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; -import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG; import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS; @@ -49,7 +47,6 @@ import android.window.StartingWindowInfo; import android.window.StartingWindowRemovalInfo; import android.window.TaskAppearedInfo; import android.window.TaskOrganizer; -import android.window.WindowContainerTransaction; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; @@ -568,6 +565,22 @@ public class ShellTaskOrganizer extends TaskOrganizer implements } } + /** + * Return list of {@link RunningTaskInfo}s for the given display. + * + * @return filtered list of tasks or empty list + */ + public ArrayList<RunningTaskInfo> getRunningTasks(int displayId) { + ArrayList<RunningTaskInfo> result = new ArrayList<>(); + for (int i = 0; i < mTasks.size(); i++) { + RunningTaskInfo taskInfo = mTasks.valueAt(i).getTaskInfo(); + if (taskInfo.displayId == displayId) { + result.add(taskInfo); + } + } + return result; + } + /** Gets running task by taskId. Returns {@code null} if no such task observed. */ @Nullable public RunningTaskInfo getRunningTaskInfo(int taskId) { @@ -694,57 +707,6 @@ public class ShellTaskOrganizer extends TaskOrganizer implements taskListener.reparentChildSurfaceToTask(taskId, sc, t); } - /** - * Create a {@link WindowContainerTransaction} to clear task bounds. - * - * Only affects tasks that have {@link RunningTaskInfo#getActivityType()} set to - * {@link WindowConfiguration#ACTIVITY_TYPE_STANDARD}. - * - * @param displayId display id for tasks that will have bounds cleared - * @return {@link WindowContainerTransaction} with pending operations to clear bounds - */ - public WindowContainerTransaction prepareClearBoundsForStandardTasks(int displayId) { - ProtoLog.d(WM_SHELL_DESKTOP_MODE, "prepareClearBoundsForTasks: displayId=%d", displayId); - WindowContainerTransaction wct = new WindowContainerTransaction(); - for (int i = 0; i < mTasks.size(); i++) { - RunningTaskInfo taskInfo = mTasks.valueAt(i).getTaskInfo(); - if ((taskInfo.displayId == displayId) && (taskInfo.getActivityType() - == WindowConfiguration.ACTIVITY_TYPE_STANDARD)) { - ProtoLog.d(WM_SHELL_DESKTOP_MODE, "clearing bounds for token=%s taskInfo=%s", - taskInfo.token, taskInfo); - wct.setBounds(taskInfo.token, null); - } - } - return wct; - } - - /** - * Create a {@link WindowContainerTransaction} to clear task level freeform setting. - * - * Only affects tasks that have {@link RunningTaskInfo#getActivityType()} set to - * {@link WindowConfiguration#ACTIVITY_TYPE_STANDARD}. - * - * @param displayId display id for tasks that will have windowing mode reset to {@link - * WindowConfiguration#WINDOWING_MODE_UNDEFINED} - * @return {@link WindowContainerTransaction} with pending operations to clear windowing mode - */ - public WindowContainerTransaction prepareClearFreeformForStandardTasks(int displayId) { - ProtoLog.d(WM_SHELL_DESKTOP_MODE, "prepareClearFreeformForTasks: displayId=%d", displayId); - WindowContainerTransaction wct = new WindowContainerTransaction(); - for (int i = 0; i < mTasks.size(); i++) { - RunningTaskInfo taskInfo = mTasks.valueAt(i).getTaskInfo(); - if (taskInfo.displayId == displayId - && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM - && taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD) { - ProtoLog.d(WM_SHELL_DESKTOP_MODE, - "clearing windowing mode for token=%s taskInfo=%s", taskInfo.token, - taskInfo); - wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED); - } - } - return wct; - } - private void logSizeCompatRestartButtonEventReported(@NonNull TaskAppearedInfo info, int event) { ActivityInfo topActivityInfo = info.getTaskInfo().topActivityInfo; 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 64220c82fd9a..d9eaeeeaf45f 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.os.UserHandle; 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; @@ -62,7 +61,6 @@ import com.android.wm.shell.common.annotations.ShellBackgroundThread; import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; -import com.android.wm.shell.transition.Transitions; import java.util.concurrent.atomic.AtomicBoolean; @@ -70,7 +68,7 @@ import java.util.concurrent.atomic.AtomicBoolean; * 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,19 +80,16 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont SETTING_VALUE_OFF) == SETTING_VALUE_ON; /** Predictive back animation developer option */ private final AtomicBoolean mEnableAnimations = new AtomicBoolean(false); - // TODO (b/241808055) Find a appropriate time to remove during refactor - private static final boolean ENABLE_SHELL_TRANSITIONS = Transitions.ENABLE_SHELL_TRANSITIONS; /** - * 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) */ @@ -108,9 +103,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont 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(); }; @@ -121,8 +116,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont private final TouchTracker mTouchTracker = new TouchTracker(); private final SparseArray<BackAnimationRunner> mAnimationDefinition = new SparseArray<>(); - private final Transitions mTransitions; - private BackTransitionHandler mBackTransitionHandler; + + private IOnBackInvokedCallback mActiveCallback; @VisibleForTesting final IWindowFocusObserver mFocusObserver = new IWindowFocusObserver.Stub() { @@ -131,9 +126,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont @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."); @@ -148,11 +143,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont @NonNull ShellController shellController, @NonNull @ShellMainThread ShellExecutor shellExecutor, @NonNull @ShellBackgroundThread Handler backgroundHandler, - Context context, - Transitions transitions) { + Context context) { this(shellInit, shellController, shellExecutor, backgroundHandler, - ActivityTaskManager.getService(), context, context.getContentResolver(), - transitions); + ActivityTaskManager.getService(), context, context.getContentResolver()); } @VisibleForTesting @@ -162,8 +155,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont @NonNull @ShellMainThread ShellExecutor shellExecutor, @NonNull @ShellBackgroundThread Handler bgHandler, @NonNull IActivityTaskManager activityTaskManager, - Context context, ContentResolver contentResolver, - Transitions transitions) { + Context context, ContentResolver contentResolver) { mShellController = shellController; mShellExecutor = shellExecutor; mActivityTaskManager = activityTaskManager; @@ -171,7 +163,6 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mContentResolver = contentResolver; mBgHandler = bgHandler; shellInit.addInitCallback(this::onInit, this); - mTransitions = transitions; } @VisibleForTesting @@ -182,10 +173,6 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont private void onInit() { setupAnimationDeveloperSettingsObserver(mContentResolver, mBgHandler); createAdapter(); - if (ENABLE_SHELL_TRANSITIONS) { - mBackTransitionHandler = new BackTransitionHandler(this); - mTransitions.addHandler(mBackTransitionHandler); - } mShellController.addExternalInterface(KEY_EXTRA_SHELL_BACK_ANIMATION, this::createExternalInterface, this); @@ -193,26 +180,11 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } 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( @@ -235,10 +207,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont 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() { @@ -292,7 +263,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont 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 @@ -307,54 +280,22 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } } - @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); - } - } - - // In legacy transition, it would use `Task.mBackGestureStarted` in core to handle the - // following transition when back callback is invoked. - // If the back callback is not invoked, we should reset the token and finish the whole back - // navigation without waiting the transition. - if (!ENABLE_SHELL_TRANSITIONS) { - finishBackNavigation(); - } else if (!mTriggerBack) { - // reset the token to prevent it consume next transition. - mBackTransitionHandler.setDepartingWindowContainerToken(null); - 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; } @@ -371,7 +312,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont 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); @@ -409,30 +350,22 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont 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() { @@ -454,57 +387,6 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } } - 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 @@ -518,7 +400,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont return; } try { - if (shouldDispatchAnimation(callback)) { + if (mEnableAnimations.get()) { callback.onBackStarted(backEvent); } } catch (RemoteException e) { @@ -542,7 +424,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont return; } try { - if (shouldDispatchAnimation(callback)) { + if (mEnableAnimations.get()) { callback.onBackCancelled(); } } catch (RemoteException e) { @@ -556,7 +438,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont return; } try { - if (shouldDispatchAnimation(callback)) { + if (mEnableAnimations.get()) { callback.onBackProgressed(backEvent); } } catch (RemoteException e) { @@ -564,17 +446,11 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } } - 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; @@ -585,6 +461,109 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont 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()"); @@ -594,10 +573,10 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mTriggerBack = false; mShouldStartOnNextMoveEvent = false; mTouchTracker.reset(); + mActiveCallback = null; if (backNavigationInfo == null) { return; } - stopTransition(); if (mBackAnimationFinishedCallback != null) { try { mBackAnimationFinishedCallback.onAnimationFinished(triggerBack); @@ -609,36 +588,6 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont backNavigationInfo.onBackNavigationFinished(triggerBack); } - private void startTransition() { - if (mTransitionInProgress) { - return; - } - mTransitionInProgress = true; - if (ENABLE_SHELL_TRANSITIONS) { - mBackTransitionHandler.setDepartingWindowContainerToken( - mBackNavigationInfo.getDepartingWindowContainerToken()); - } - mShellExecutor.executeDelayed(mResetTransitionRunnable, MAX_TRANSITION_DURATION); - } - - void stopTransition() { - mShellExecutor.removeCallbacks(mResetTransitionRunnable); - mTransitionInProgress = false; - } - - /** - * This should be called from {@link BackTransitionHandler#startAnimation} when the following - * transition is triggered by the real back callback in {@link #onBackAnimationFinished}. - * Will consume the default transition and finish current back navigation. - */ - void finishTransition(Transitions.TransitionFinishCallback finishCallback) { - ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: finishTransition()"); - mShellExecutor.execute(() -> { - finishBackNavigation(); - finishCallback.onTransitionFinished(null, null); - }); - } - private void createAdapter() { IBackAnimationRunner runner = new IBackAnimationRunner.Stub() { @Override @@ -664,20 +613,18 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont 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 c53fcfc99c9c..d70b8f53a911 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 @@ package com.android.wm.shell.back; 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 @@ class BackAnimationRunner { 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 @@ class BackAnimationRunner { 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/back/BackTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackTransitionHandler.java deleted file mode 100644 index 6d72d9c1f637..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackTransitionHandler.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.back; - -import android.os.IBinder; -import android.view.SurfaceControl; -import android.window.TransitionInfo; -import android.window.TransitionRequestInfo; -import android.window.WindowContainerToken; -import android.window.WindowContainerTransaction; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.android.wm.shell.transition.Transitions; - -class BackTransitionHandler implements Transitions.TransitionHandler { - private BackAnimationController mBackAnimationController; - private WindowContainerToken mDepartingWindowContainerToken; - - BackTransitionHandler(@NonNull BackAnimationController backAnimationController) { - mBackAnimationController = backAnimationController; - } - - @Override - public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction startTransaction, - @NonNull SurfaceControl.Transaction finishTransaction, - @NonNull Transitions.TransitionFinishCallback finishCallback) { - if (mDepartingWindowContainerToken != null) { - final TransitionInfo.Change change = info.getChange(mDepartingWindowContainerToken); - if (change == null) { - return false; - } - - startTransaction.hide(change.getLeash()); - startTransaction.apply(); - mDepartingWindowContainerToken = null; - mBackAnimationController.finishTransition(finishCallback); - return true; - } - - return false; - } - - @Nullable - @Override - public WindowContainerTransaction handleRequest(@NonNull IBinder transition, - @NonNull TransitionRequestInfo request) { - return null; - } - - @Override - public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, - @NonNull Transitions.TransitionFinishCallback finishCallback) { - } - - void setDepartingWindowContainerToken( - @Nullable WindowContainerToken departingWindowContainerToken) { - mDepartingWindowContainerToken = departingWindowContainerToken; - } -} - diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index a400555d31a8..1fd91debe3f6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -709,7 +709,7 @@ public class BubbleController implements ConfigurationChangeListener { IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); filter.addAction(Intent.ACTION_SCREEN_OFF); - mContext.registerReceiver(mBroadcastReceiver, filter); + mContext.registerReceiver(mBroadcastReceiver, filter, Context.RECEIVER_EXPORTED); } private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { 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 bb7c4134aaaf..d9b4f475a50c 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.Animator; 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; 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 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged } 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 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged 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 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged !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 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged } @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 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged .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 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged + (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 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged 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 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged : 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 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged } 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 8d4a09d8f371..8759301f695b 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.IWindowManager; 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 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan } } - 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 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan } @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 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan * * @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 e270edb800bd..af13bf54f691 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java @@ -19,6 +19,7 @@ package com.android.wm.shell.common; 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.View; 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 @@ public class SystemWindows { 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 839edc8c174a..3de1045bfbda 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 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange 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/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index 1977dcb81e97..962be9da2111 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -260,13 +260,12 @@ public abstract class WMShellBaseModule { ShellInit shellInit, ShellController shellController, @ShellMainThread ShellExecutor shellExecutor, - @ShellBackgroundThread Handler backgroundHandler, - Transitions transitions + @ShellBackgroundThread Handler backgroundHandler ) { if (BackAnimationController.IS_ENABLED) { return Optional.of( new BackAnimationController(shellInit, shellController, shellExecutor, - backgroundHandler, context, transitions)); + backgroundHandler, context)); } return Optional.empty(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 4459f57a1356..f1670cd792cf 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -27,7 +27,6 @@ import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.UiEventLogger; import com.android.internal.statusbar.IStatusBarService; import com.android.launcher3.icons.IconProvider; -import com.android.wm.shell.RootDisplayAreaOrganizer; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.TaskViewTransitions; @@ -272,12 +271,11 @@ public abstract class WMShellModule { TaskStackListenerImpl taskStackListener, UiEventLogger uiEventLogger, InteractionJankMonitor jankMonitor, - RootDisplayAreaOrganizer rootDisplayAreaOrganizer, @ShellMainThread ShellExecutor mainExecutor, @ShellMainThread Handler mainHandler) { return OneHandedController.create(context, shellInit, shellCommandHandler, shellController, windowManager, displayController, displayLayout, taskStackListener, jankMonitor, - uiEventLogger, rootDisplayAreaOrganizer, mainExecutor, mainHandler); + uiEventLogger, mainExecutor, mainHandler); } // diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java index 34ff6d814c8d..abc4024bc290 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java @@ -16,8 +16,11 @@ package com.android.wm.shell.desktopmode; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_FRONT; @@ -151,21 +154,18 @@ public class DesktopModeController implements RemoteCallable<DesktopModeControll int displayId = mContext.getDisplayId(); + ArrayList<RunningTaskInfo> runningTasks = mShellTaskOrganizer.getRunningTasks(displayId); + WindowContainerTransaction wct = new WindowContainerTransaction(); - // Reset freeform windowing mode that is set per task level (tasks should inherit - // container value) - wct.merge(mShellTaskOrganizer.prepareClearFreeformForStandardTasks(displayId), - true /* transfer */); - int targetWindowingMode; + // Reset freeform windowing mode that is set per task level so tasks inherit it + clearFreeformForStandardTasks(runningTasks, wct); if (active) { - targetWindowingMode = WINDOWING_MODE_FREEFORM; + moveHomeBehindVisibleTasks(runningTasks, wct); + setDisplayAreaWindowingMode(displayId, WINDOWING_MODE_FREEFORM, wct); } else { - targetWindowingMode = WINDOWING_MODE_FULLSCREEN; - // Clear any resized bounds - wct.merge(mShellTaskOrganizer.prepareClearBoundsForStandardTasks(displayId), - true /* transfer */); + clearBoundsForStandardTasks(runningTasks, wct); + setDisplayAreaWindowingMode(displayId, WINDOWING_MODE_FULLSCREEN, wct); } - prepareWindowingModeChange(wct, displayId, targetWindowingMode); if (Transitions.ENABLE_SHELL_TRANSITIONS) { mTransitions.startTransition(TRANSIT_CHANGE, wct, null); } else { @@ -173,17 +173,69 @@ public class DesktopModeController implements RemoteCallable<DesktopModeControll } } - private void prepareWindowingModeChange(WindowContainerTransaction wct, - int displayId, @WindowConfiguration.WindowingMode int windowingMode) { - DisplayAreaInfo displayAreaInfo = mRootTaskDisplayAreaOrganizer - .getDisplayAreaInfo(displayId); + private WindowContainerTransaction clearBoundsForStandardTasks( + ArrayList<RunningTaskInfo> runningTasks, WindowContainerTransaction wct) { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "prepareClearBoundsForTasks"); + for (RunningTaskInfo taskInfo : runningTasks) { + if (taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD) { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "clearing bounds for token=%s taskInfo=%s", + taskInfo.token, taskInfo); + wct.setBounds(taskInfo.token, null); + } + } + return wct; + } + + private void clearFreeformForStandardTasks(ArrayList<RunningTaskInfo> runningTasks, + WindowContainerTransaction wct) { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "prepareClearFreeformForTasks"); + for (RunningTaskInfo taskInfo : runningTasks) { + if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM + && taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD) { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, + "clearing windowing mode for token=%s taskInfo=%s", taskInfo.token, + taskInfo); + wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED); + } + } + } + + private void moveHomeBehindVisibleTasks(ArrayList<RunningTaskInfo> runningTasks, + WindowContainerTransaction wct) { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveHomeBehindVisibleTasks"); + RunningTaskInfo homeTask = null; + ArrayList<RunningTaskInfo> visibleTasks = new ArrayList<>(); + for (RunningTaskInfo taskInfo : runningTasks) { + if (taskInfo.getActivityType() == ACTIVITY_TYPE_HOME) { + homeTask = taskInfo; + } else if (taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD + && taskInfo.isVisible()) { + visibleTasks.add(taskInfo); + } + } + if (homeTask == null) { + ProtoLog.w(WM_SHELL_DESKTOP_MODE, "moveHomeBehindVisibleTasks: home task not found"); + } else { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveHomeBehindVisibleTasks: visible tasks %d", + visibleTasks.size()); + wct.reorder(homeTask.getToken(), true /* onTop */); + for (RunningTaskInfo task : visibleTasks) { + wct.reorder(task.getToken(), true /* onTop */); + } + } + } + + private void setDisplayAreaWindowingMode(int displayId, + @WindowConfiguration.WindowingMode int windowingMode, WindowContainerTransaction wct) { + DisplayAreaInfo displayAreaInfo = mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo( + displayId); if (displayAreaInfo == null) { ProtoLog.e(WM_SHELL_DESKTOP_MODE, "unable to update windowing mode for display %d display not found", displayId); return; } - ProtoLog.d(WM_SHELL_DESKTOP_MODE, + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "setWindowingMode: displayId=%d current wmMode=%d new wmMode=%d", displayId, displayAreaInfo.configuration.windowConfiguration.getWindowingMode(), windowingMode); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java index b5ed509b6935..b310ee2095bf 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java @@ -34,7 +34,6 @@ import android.graphics.Rect; import android.os.Binder; import android.util.Slog; import android.view.ContextThemeWrapper; -import android.view.Display; import android.view.IWindow; import android.view.LayoutInflater; import android.view.SurfaceControl; @@ -48,7 +47,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.wm.shell.R; -import com.android.wm.shell.RootDisplayAreaOrganizer; import com.android.wm.shell.common.DisplayLayout; import java.io.PrintWriter; @@ -69,14 +67,11 @@ public final class BackgroundWindowManager extends WindowlessWindowManager { private SurfaceControl mLeash; private View mBackgroundView; private @OneHandedState.State int mCurrentState; - private RootDisplayAreaOrganizer mRootDisplayAreaOrganizer; - public BackgroundWindowManager(Context context, - RootDisplayAreaOrganizer rootDisplayAreaOrganizer) { + public BackgroundWindowManager(Context context) { super(context.getResources().getConfiguration(), null /* rootSurface */, null /* hostInputToken */); mContext = context; - mRootDisplayAreaOrganizer = rootDisplayAreaOrganizer; mTransactionFactory = SurfaceControl.Transaction::new; } @@ -117,7 +112,6 @@ public final class BackgroundWindowManager extends WindowlessWindowManager { .setOpaque(true) .setName(TAG) .setCallsite("BackgroundWindowManager#attachToParentSurface"); - mRootDisplayAreaOrganizer.attachToDisplayArea(Display.DEFAULT_DISPLAY, builder); mLeash = builder.build(); b.setParent(mLeash); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java index ad135d1baa14..679d4ca2ac48 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java @@ -47,7 +47,6 @@ import androidx.annotation.VisibleForTesting; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.UiEventLogger; import com.android.wm.shell.R; -import com.android.wm.shell.RootDisplayAreaOrganizer; import com.android.wm.shell.common.DisplayChangeController; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; @@ -205,14 +204,12 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, DisplayController displayController, DisplayLayout displayLayout, TaskStackListenerImpl taskStackListener, InteractionJankMonitor jankMonitor, UiEventLogger uiEventLogger, - RootDisplayAreaOrganizer rootDisplayAreaOrganizer, ShellExecutor mainExecutor, Handler mainHandler) { OneHandedSettingsUtil settingsUtil = new OneHandedSettingsUtil(); OneHandedAccessibilityUtil accessibilityUtil = new OneHandedAccessibilityUtil(context); OneHandedTimeoutHandler timeoutHandler = new OneHandedTimeoutHandler(mainExecutor); OneHandedState oneHandedState = new OneHandedState(); - BackgroundWindowManager backgroundWindowManager = - new BackgroundWindowManager(context, rootDisplayAreaOrganizer); + BackgroundWindowManager backgroundWindowManager = new BackgroundWindowManager(context); OneHandedTutorialHandler tutorialHandler = new OneHandedTutorialHandler(context, settingsUtil, windowManager, backgroundWindowManager); OneHandedAnimationController animationController = diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java index 7365b9525919..1f7a7fc9ed4c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java @@ -29,6 +29,7 @@ import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityWindowInfo; import android.view.accessibility.IAccessibilityInteractionConnection; import android.view.accessibility.IAccessibilityInteractionConnectionCallback; +import android.window.ScreenCapture; import androidx.annotation.BinderThread; @@ -362,6 +363,15 @@ public class PipAccessibilityInteractionConnection { } @Override + public void takeScreenshotOfWindow(int interactionId, + ScreenCapture.ScreenCaptureListener listener, + IAccessibilityInteractionConnectionCallback callback) throws RemoteException { + // AbstractAccessibilityServiceConnection uses the standard + // IAccessibilityInteractionConnection for takeScreenshotOfWindow for Pip windows, + // so do nothing here. + } + + @Override public void clearAccessibilityFocus() throws RemoteException { // Do nothing } 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 eb08d0ecbd06..5533ad56d17c 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 @@ interface ISplitScreen { /** * 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 @@ interface ISplitScreen { * 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 c6a2b8312ebd..cdc8cdd2c28d 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 @@ package com.android.wm.shell.splitscreen; 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.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; 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 android.window.WindowContainerTransaction; 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 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, private final IconProvider mIconProvider; private final Optional<RecentTasksController> mRecentTasksOptional; private final SplitScreenShellCommandHandler mSplitScreenShellCommandHandler; + private final String[] mMultiInstancesComponents; + + @VisibleForTesting + StageCoordinator mStageCoordinator; - private 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 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, 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 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, 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 = pendingIntent.getIntent().getComponent(); + if (launchingActivity == null) return false; - final ComponentName launchingActivity = startIntent.getComponent(); - if (launchingActivity == null) { + if (taskId != INVALID_TASK_ID) { + final ActivityManager.RunningTaskInfo taskInfo = + mTaskOrganizer.getRunningTaskInfo(taskId); + if (taskInfo != null) { + return Objects.equals(taskInfo.baseIntent.getComponent(), launchingActivity); + } 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 (!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); } - - // 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); + // 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 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, @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 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, } @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 e888c6f8b0f9..c2ab7ef7e7bf 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_ASSISTANT; 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 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, /** 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 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, 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 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, /** 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 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, 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 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, 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 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, : 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 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, 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 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, @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/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index 928e71f8d3a6..63d4a6f5acd9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -41,6 +41,7 @@ import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_RELAUNCH; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; +import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED; import static android.window.TransitionInfo.FLAG_CROSS_PROFILE_OWNER_THUMBNAIL; import static android.window.TransitionInfo.FLAG_CROSS_PROFILE_WORK_THUMBNAIL; import static android.window.TransitionInfo.FLAG_DISPLAY_HAS_ALERT_WINDOWS; @@ -395,6 +396,11 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { } } + // The back gesture has animated this change before transition happen, so here we don't + // play the animation again. + if (change.hasFlags(FLAG_BACK_GESTURE_ANIMATED)) { + continue; + } // Don't animate anything that isn't independent. if (!TransitionInfo.isIndependent(change, info)) continue; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java index ca15f0002fac..ebe5c5e716d7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java @@ -124,7 +124,8 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { TaskPositioner taskPositioner = new TaskPositioner(mTaskOrganizer, windowDecoration, mDragStartListener); CaptionTouchEventListener touchEventListener = - new CaptionTouchEventListener(taskInfo, taskPositioner); + new CaptionTouchEventListener(taskInfo, taskPositioner, + windowDecoration.getDragDetector()); windowDecoration.setCaptionListeners(touchEventListener, touchEventListener); windowDecoration.setDragResizeCallback(taskPositioner); setupWindowDecorationForTransition(taskInfo, startT, finishT); @@ -173,16 +174,18 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { private final int mTaskId; private final WindowContainerToken mTaskToken; private final DragResizeCallback mDragResizeCallback; + private final DragDetector mDragDetector; private int mDragPointerId = -1; - private boolean mDragActive = false; private CaptionTouchEventListener( RunningTaskInfo taskInfo, - DragResizeCallback dragResizeCallback) { + DragResizeCallback dragResizeCallback, + DragDetector dragDetector) { mTaskId = taskInfo.taskId; mTaskToken = taskInfo.token; mDragResizeCallback = dragResizeCallback; + mDragDetector = dragDetector; } @Override @@ -231,19 +234,21 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { @Override public boolean onTouch(View v, MotionEvent e) { + boolean isDrag = false; int id = v.getId(); if (id != R.id.caption_handle && id != R.id.caption) { return false; } - if (id == R.id.caption_handle || mDragActive) { + if (id == R.id.caption_handle) { + isDrag = mDragDetector.detectDragEvent(e); handleEventForMove(e); } if (e.getAction() != MotionEvent.ACTION_DOWN) { - return false; + return isDrag; } RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId); if (taskInfo.isFocused) { - return false; + return isDrag; } WindowContainerTransaction wct = new WindowContainerTransaction(); wct.reorder(mTaskToken, true /* onTop */); @@ -251,6 +256,10 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { return true; } + /** + * @param e {@link MotionEvent} to process + * @return {@code true} if a drag is happening; or {@code false} if it is not + */ private void handleEventForMove(MotionEvent e) { RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId); int windowingMode = mDesktopModeController @@ -259,12 +268,12 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { return; } switch (e.getActionMasked()) { - case MotionEvent.ACTION_DOWN: - mDragActive = true; - mDragPointerId = e.getPointerId(0); + case MotionEvent.ACTION_DOWN: { + mDragPointerId = e.getPointerId(0); mDragResizeCallback.onDragResizeStart( 0 /* ctrlType */, e.getRawX(0), e.getRawY(0)); break; + } case MotionEvent.ACTION_MOVE: { int dragPointerIdx = e.findPointerIndex(mDragPointerId); mDragResizeCallback.onDragResizeMove( @@ -273,7 +282,6 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { } case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: { - mDragActive = false; int dragPointerIdx = e.findPointerIndex(mDragPointerId); int statusBarHeight = mDisplayController.getDisplayLayout(taskInfo.displayId) .stableInsets().top; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java index 03cad043ed67..affde3009456 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java @@ -62,6 +62,8 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL private boolean mDesktopActive; + private DragDetector mDragDetector; + private AdditionalWindow mHandleMenu; CaptionWindowDecoration( @@ -79,6 +81,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL mChoreographer = choreographer; mSyncQueue = syncQueue; mDesktopActive = DesktopModeStatus.isActive(mContext); + mDragDetector = new DragDetector(ViewConfiguration.get(context).getScaledTouchSlop()); } void setCaptionListeners( @@ -92,6 +95,10 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL mDragResizeCallback = dragResizeCallback; } + DragDetector getDragDetector() { + return mDragDetector; + } + @Override void relayout(ActivityManager.RunningTaskInfo taskInfo) { final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); @@ -182,6 +189,8 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL } int touchSlop = ViewConfiguration.get(mResult.mRootView.getContext()).getScaledTouchSlop(); + mDragDetector.setTouchSlop(touchSlop); + int resize_handle = mResult.mRootView.getResources() .getDimensionPixelSize(R.dimen.freeform_resize_handle); int resize_corner = mResult.mRootView.getResources() diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java new file mode 100644 index 000000000000..0abe8ab2e30b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.windowdecor; + +import static android.view.MotionEvent.ACTION_CANCEL; +import static android.view.MotionEvent.ACTION_DOWN; +import static android.view.MotionEvent.ACTION_MOVE; +import static android.view.MotionEvent.ACTION_UP; + +import android.graphics.PointF; +import android.view.MotionEvent; + +/** + * A detector for touch inputs that differentiates between drag and click inputs. + * All touch events must be passed through this class to track a drag event. + */ +public class DragDetector { + private int mTouchSlop; + private PointF mInputDownPoint; + private boolean mIsDragEvent; + private int mDragPointerId; + public DragDetector(int touchSlop) { + mTouchSlop = touchSlop; + mInputDownPoint = new PointF(); + mIsDragEvent = false; + mDragPointerId = -1; + } + + /** + * Determine if {@link MotionEvent} is part of a drag event. + * @return {@code true} if this is a drag event, {@code false} if not + */ + public boolean detectDragEvent(MotionEvent ev) { + switch (ev.getAction()) { + case ACTION_DOWN: { + mDragPointerId = ev.getPointerId(0); + float rawX = ev.getRawX(0); + float rawY = ev.getRawY(0); + mInputDownPoint.set(rawX, rawY); + return false; + } + case ACTION_MOVE: { + if (!mIsDragEvent) { + int dragPointerIndex = ev.findPointerIndex(mDragPointerId); + float dx = ev.getRawX(dragPointerIndex) - mInputDownPoint.x; + float dy = ev.getRawY(dragPointerIndex) - mInputDownPoint.y; + if (Math.hypot(dx, dy) > mTouchSlop) { + mIsDragEvent = true; + } + } + return mIsDragEvent; + } + case ACTION_UP: { + boolean result = mIsDragEvent; + mIsDragEvent = false; + mInputDownPoint.set(0, 0); + mDragPointerId = -1; + return result; + } + case ACTION_CANCEL: { + mIsDragEvent = false; + mInputDownPoint.set(0, 0); + mDragPointerId = -1; + return false; + } + } + return mIsDragEvent; + } + + public void setTouchSlop(int touchSlop) { + mTouchSlop = touchSlop; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java index b9f16b63de48..48c0cea150cc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java @@ -22,7 +22,6 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERL import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; import android.content.Context; -import android.graphics.PointF; import android.graphics.Rect; import android.graphics.Region; import android.hardware.input.InputManager; @@ -38,6 +37,7 @@ import android.view.InputEventReceiver; import android.view.MotionEvent; import android.view.PointerIcon; import android.view.SurfaceControl; +import android.view.ViewConfiguration; import android.view.WindowManagerGlobal; import com.android.internal.view.BaseIWindow; @@ -76,7 +76,7 @@ class DragResizeInputListener implements AutoCloseable { private Rect mRightBottomCornerBounds; private int mDragPointerId = -1; - private int mTouchSlop; + private DragDetector mDragDetector; DragResizeInputListener( Context context, @@ -115,6 +115,7 @@ class DragResizeInputListener implements AutoCloseable { mInputEventReceiver = new TaskResizeInputEventReceiver( mInputChannel, mHandler, mChoreographer); mCallback = callback; + mDragDetector = new DragDetector(ViewConfiguration.get(context).getScaledTouchSlop()); } /** @@ -146,7 +147,7 @@ class DragResizeInputListener implements AutoCloseable { mHeight = height; mResizeHandleThickness = resizeHandleThickness; mCornerSize = cornerSize; - mTouchSlop = touchSlop; + mDragDetector.setTouchSlop(touchSlop); Region touchRegion = new Region(); final Rect topInputBounds = new Rect(0, 0, mWidth, mResizeHandleThickness); @@ -228,7 +229,6 @@ class DragResizeInputListener implements AutoCloseable { private boolean mConsumeBatchEventScheduled; private boolean mShouldHandleEvents; private boolean mDragging; - private final PointF mActionDownPoint = new PointF(); private TaskResizeInputEventReceiver( InputChannel inputChannel, Handler handler, Choreographer choreographer) { @@ -276,7 +276,9 @@ class DragResizeInputListener implements AutoCloseable { // Check if this is a touch event vs mouse event. // Touch events are tracked in four corners. Other events are tracked in resize edges. boolean isTouch = (e.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN; - + if (isTouch) { + mDragging = mDragDetector.detectDragEvent(e); + } switch (e.getActionMasked()) { case MotionEvent.ACTION_DOWN: { float x = e.getX(0); @@ -290,7 +292,6 @@ class DragResizeInputListener implements AutoCloseable { mDragPointerId = e.getPointerId(0); float rawX = e.getRawX(0); float rawY = e.getRawY(0); - mActionDownPoint.set(rawX, rawY); int ctrlType = calculateCtrlType(isTouch, x, y); mCallback.onDragResizeStart(ctrlType, rawX, rawY); result = true; @@ -304,14 +305,7 @@ class DragResizeInputListener implements AutoCloseable { int dragPointerIndex = e.findPointerIndex(mDragPointerId); float rawX = e.getRawX(dragPointerIndex); float rawY = e.getRawY(dragPointerIndex); - if (isTouch) { - // Check for touch slop for touch events - float dx = rawX - mActionDownPoint.x; - float dy = rawY - mActionDownPoint.y; - if (!mDragging && Math.hypot(dx, dy) > mTouchSlop) { - mDragging = true; - } - } else { + if (!isTouch) { // For all other types allow immediate dragging. mDragging = true; } @@ -330,7 +324,6 @@ class DragResizeInputListener implements AutoCloseable { } mDragging = false; mShouldHandleEvents = false; - mActionDownPoint.set(0, 0); mDragPointerId = -1; result = true; break; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java index f0f2db7ded80..a49a300995e6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java @@ -40,6 +40,9 @@ class TaskPositioner implements DragResizeCallback { private final Rect mTaskBoundsAtDragStart = new Rect(); private final PointF mResizeStartPoint = new PointF(); private final Rect mResizeTaskBounds = new Rect(); + // Whether the |dragResizing| hint should be sent with the next bounds change WCT. + // Used to optimized fluid resizing of freeform tasks. + private boolean mPendingDragResizeHint = false; private int mCtrlType; private DragStartListener mDragStartListener; @@ -53,6 +56,12 @@ class TaskPositioner implements DragResizeCallback { @Override public void onDragResizeStart(int ctrlType, float x, float y) { + if (ctrlType != CTRL_TYPE_UNDEFINED) { + // The task is being resized, send the |dragResizing| hint to core with the first + // bounds-change wct. + mPendingDragResizeHint = true; + } + mDragStartListener.onDragStart(mWindowDecoration.mTaskInfo.taskId); mCtrlType = ctrlType; @@ -63,19 +72,31 @@ class TaskPositioner implements DragResizeCallback { @Override public void onDragResizeMove(float x, float y) { - changeBounds(x, y); + final WindowContainerTransaction wct = new WindowContainerTransaction(); + if (changeBounds(wct, x, y)) { + if (mPendingDragResizeHint) { + // This is the first bounds change since drag resize operation started. + wct.setDragResizing(mWindowDecoration.mTaskInfo.token, true /* dragResizing */); + mPendingDragResizeHint = false; + } + mTaskOrganizer.applyTransaction(wct); + } } @Override public void onDragResizeEnd(float x, float y) { - changeBounds(x, y); + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.setDragResizing(mWindowDecoration.mTaskInfo.token, false /* dragResizing */); + changeBounds(wct, x, y); + mTaskOrganizer.applyTransaction(wct); mCtrlType = 0; mTaskBoundsAtDragStart.setEmpty(); mResizeStartPoint.set(0, 0); + mPendingDragResizeHint = false; } - private void changeBounds(float x, float y) { + private boolean changeBounds(WindowContainerTransaction wct, float x, float y) { float deltaX = x - mResizeStartPoint.x; mResizeTaskBounds.set(mTaskBoundsAtDragStart); if ((mCtrlType & CTRL_TYPE_LEFT) != 0) { @@ -96,10 +117,10 @@ class TaskPositioner implements DragResizeCallback { } if (!mResizeTaskBounds.isEmpty()) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); wct.setBounds(mWindowDecoration.mTaskInfo.token, mResizeTaskBounds); - mTaskOrganizer.applyTransaction(wct); + return true; } + return false; } interface DragStartListener { diff --git a/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml b/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml index 2d6e8f54b6b6..08913c6fde53 100644 --- a/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml +++ b/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml @@ -13,6 +13,8 @@ <option name="run-command" value="cmd window tracing level all" /> <!-- set WM tracing to frame (avoid incomplete states) --> <option name="run-command" value="cmd window tracing frame" /> + <!-- disable betterbug as it's log collection dialogues cause flakes in e2e tests --> + <option name="run-command" value="pm disable com.google.android.internal.betterbug" /> <!-- ensure lock screen mode is swipe --> <option name="run-command" value="locksettings set-disabled false" /> <!-- restart launcher to activate TAPL --> @@ -34,4 +36,4 @@ <option name="collect-on-run-ended-only" value="true" /> <option name="clean-up" value="true" /> </metrics_collector> -</configuration>
\ No newline at end of file +</configuration> diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt index 2bce8e45c553..9533b9182b6c 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt @@ -24,6 +24,8 @@ import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.traces.common.ComponentNameMatcher +import com.android.server.wm.traces.common.EdgeExtensionComponentMatcher import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd import com.android.wm.shell.flicker.appWindowIsVisibleAtStart @@ -49,6 +51,8 @@ import org.junit.runners.Parameterized @FixMethodOrder(MethodSorters.NAME_ASCENDING) class CopyContentInSplit(testSpec: FlickerTestParameter) : SplitScreenBase(testSpec) { private val textEditApp = SplitScreenUtils.getIme(instrumentation) + private val MagnifierLayer = ComponentNameMatcher("", "magnifier surface bbq wrapper#") + private val PopupWindowLayer = ComponentNameMatcher("", "PopupWindow:") override val transition: FlickerBuilder.() -> Unit get() = { @@ -159,8 +163,18 @@ class CopyContentInSplit(testSpec: FlickerTestParameter) : SplitScreenBase(testS /** {@inheritDoc} */ @Presubmit @Test - override fun visibleLayersShownMoreThanOneConsecutiveEntry() = - super.visibleLayersShownMoreThanOneConsecutiveEntry() + override fun visibleLayersShownMoreThanOneConsecutiveEntry() { + testSpec.assertLayers { + this.visibleLayersShownMoreThanOneConsecutiveEntry( + ignoreLayers = listOf( + ComponentNameMatcher.SPLASH_SCREEN, + ComponentNameMatcher.SNAPSHOT, + ComponentNameMatcher.IME_SNAPSHOT, + EdgeExtensionComponentMatcher(), + MagnifierLayer, + PopupWindowLayer)) + } + } /** {@inheritDoc} */ @Presubmit 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 ead451f07653..4a3284e1953b 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,6 +41,7 @@ import com.android.server.wm.traces.parser.toFlickerComponent 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 { @@ -50,7 +51,7 @@ internal object SplitScreenUtils { private const val DIVIDER_BAR = "docked_divider_handle" private const val OVERVIEW_SNAPSHOT = "snapshot" private const val GESTURE_STEP_MS = 16L - private const val LONG_PRESS_TIME_MS = 100L + private val LONG_PRESS_TIME_MS = ViewConfiguration.getLongPressTimeout() * 2L private val SPLIT_DECOR_MANAGER = ComponentNameMatcher("", "SplitDecorManager#") private val notificationScrollerSelector: BySelector @@ -275,13 +276,6 @@ internal object SplitScreenUtils { } } - 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() @@ -353,9 +347,11 @@ internal object SplitScreenUtils { Until.findObject(By.res(sourceApp.packageName, "SplitScreenTest")), TIMEOUT_MS ) - longPress(instrumentation, textView.visibleCenter) + assertNotNull("Unable to find the TextView", textView) + textView.click(LONG_PRESS_TIME_MS) 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 @@ -364,9 +360,11 @@ internal object SplitScreenUtils { Until.findObject(By.res(destinationApp.packageName, "plain_text_input")), TIMEOUT_MS ) - longPress(instrumentation, editText.visibleCenter) + assertNotNull("Unable to find the EditText", editText) + editText.click(LONG_PRESS_TIME_MS) 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/ShellTaskOrganizerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java index 7cbace5af48f..081c8ae91bdb 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java @@ -16,13 +16,9 @@ package com.android.wm.shell; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; -import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; -import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.view.Display.DEFAULT_DISPLAY; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; @@ -34,8 +30,6 @@ import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIO import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.junit.Assume.assumeFalse; @@ -44,11 +38,9 @@ import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; import android.app.ActivityManager.RunningTaskInfo; import android.app.TaskInfo; -import android.app.WindowConfiguration; import android.content.LocusId; import android.content.pm.ParceledListSlice; import android.os.Binder; @@ -61,8 +53,6 @@ import android.window.ITaskOrganizer; import android.window.ITaskOrganizerController; import android.window.TaskAppearedInfo; import android.window.WindowContainerToken; -import android.window.WindowContainerTransaction; -import android.window.WindowContainerTransaction.Change; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -638,130 +628,10 @@ public class ShellTaskOrganizerTests extends ShellTestCase { verify(mTaskOrganizerController).restartTaskTopActivityProcessIfVisible(task1.token); } - @Test - public void testPrepareClearBoundsForStandardTasks() { - MockToken token1 = new MockToken(); - RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_UNDEFINED, token1); - mOrganizer.onTaskAppeared(task1, null); - - MockToken token2 = new MockToken(); - RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_UNDEFINED, token2); - mOrganizer.onTaskAppeared(task2, null); - - MockToken otherDisplayToken = new MockToken(); - RunningTaskInfo otherDisplayTask = createTaskInfo(3, WINDOWING_MODE_UNDEFINED, - otherDisplayToken); - otherDisplayTask.displayId = 2; - mOrganizer.onTaskAppeared(otherDisplayTask, null); - - WindowContainerTransaction wct = mOrganizer.prepareClearBoundsForStandardTasks(1); - - assertEquals(wct.getChanges().size(), 2); - Change boundsChange1 = wct.getChanges().get(token1.binder()); - assertNotNull(boundsChange1); - assertNotEquals( - (boundsChange1.getWindowSetMask() & WindowConfiguration.WINDOW_CONFIG_BOUNDS), 0); - assertTrue(boundsChange1.getConfiguration().windowConfiguration.getBounds().isEmpty()); - - Change boundsChange2 = wct.getChanges().get(token2.binder()); - assertNotNull(boundsChange2); - assertNotEquals( - (boundsChange2.getWindowSetMask() & WindowConfiguration.WINDOW_CONFIG_BOUNDS), 0); - assertTrue(boundsChange2.getConfiguration().windowConfiguration.getBounds().isEmpty()); - } - - @Test - public void testPrepareClearBoundsForStandardTasks_onlyClearActivityTypeStandard() { - MockToken token1 = new MockToken(); - RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_UNDEFINED, token1); - mOrganizer.onTaskAppeared(task1, null); - - MockToken token2 = new MockToken(); - RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_UNDEFINED, token2); - task2.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_HOME); - mOrganizer.onTaskAppeared(task2, null); - - WindowContainerTransaction wct = mOrganizer.prepareClearBoundsForStandardTasks(1); - - // Only clear bounds for task1 - assertEquals(1, wct.getChanges().size()); - assertNotNull(wct.getChanges().get(token1.binder())); - } - - @Test - public void testPrepareClearFreeformForStandardTasks() { - MockToken token1 = new MockToken(); - RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_FREEFORM, token1); - mOrganizer.onTaskAppeared(task1, null); - - MockToken token2 = new MockToken(); - RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_MULTI_WINDOW, token2); - mOrganizer.onTaskAppeared(task2, null); - - MockToken otherDisplayToken = new MockToken(); - RunningTaskInfo otherDisplayTask = createTaskInfo(3, WINDOWING_MODE_FREEFORM, - otherDisplayToken); - otherDisplayTask.displayId = 2; - mOrganizer.onTaskAppeared(otherDisplayTask, null); - - WindowContainerTransaction wct = mOrganizer.prepareClearFreeformForStandardTasks(1); - - // Only task with freeform windowing mode and the right display should be updated - assertEquals(wct.getChanges().size(), 1); - Change wmModeChange1 = wct.getChanges().get(token1.binder()); - assertNotNull(wmModeChange1); - assertEquals(wmModeChange1.getWindowingMode(), WINDOWING_MODE_UNDEFINED); - } - - @Test - public void testPrepareClearFreeformForStandardTasks_onlyClearActivityTypeStandard() { - MockToken token1 = new MockToken(); - RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_FREEFORM, token1); - mOrganizer.onTaskAppeared(task1, null); - - MockToken token2 = new MockToken(); - RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_FREEFORM, token2); - task2.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_HOME); - mOrganizer.onTaskAppeared(task2, null); - - WindowContainerTransaction wct = mOrganizer.prepareClearFreeformForStandardTasks(1); - - // Only clear freeform for task1 - assertEquals(1, wct.getChanges().size()); - assertNotNull(wct.getChanges().get(token1.binder())); - } - private static RunningTaskInfo createTaskInfo(int taskId, int windowingMode) { RunningTaskInfo taskInfo = new RunningTaskInfo(); taskInfo.taskId = taskId; taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode); return taskInfo; } - - private static RunningTaskInfo createTaskInfo(int taskId, int windowingMode, MockToken token) { - RunningTaskInfo taskInfo = createTaskInfo(taskId, windowingMode); - taskInfo.displayId = 1; - taskInfo.token = token.token(); - taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_STANDARD); - return taskInfo; - } - - private static class MockToken { - private final WindowContainerToken mToken; - private final IBinder mBinder; - - MockToken() { - mToken = mock(WindowContainerToken.class); - mBinder = mock(IBinder.class); - when(mToken.asBinder()).thenReturn(mBinder); - } - - WindowContainerToken token() { - return mToken; - } - - IBinder binder() { - return mBinder; - } - } } 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 7896247c5f5a..b603e0355e98 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.any; 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; @@ -64,7 +65,6 @@ import com.android.wm.shell.TestShellExecutor; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.sysui.ShellSharedConstants; -import com.android.wm.shell.transition.Transitions; import org.junit.Before; import org.junit.Rule; @@ -94,23 +94,21 @@ public class BackAnimationControllerTest extends ShellTestCase { private IActivityTaskManager mActivityTaskManager; @Mock - private IOnBackInvokedCallback mIOnBackInvokedCallback; + private IOnBackInvokedCallback mAppCallback; @Mock - private IBackAnimationFinishedCallback mBackAnimationFinishedCallback; + private IOnBackInvokedCallback mAnimatorCallback; @Mock - private IRemoteAnimationRunner mBackAnimationRunner; + private IBackAnimationFinishedCallback mBackAnimationFinishedCallback; @Mock - private Transitions mTransitions; + private IRemoteAnimationRunner mBackAnimationRunner; @Mock private ShellController mShellController; private BackAnimationController mController; - - private int mEventTime = 0; private TestableContentResolver mContentResolver; private TestableLooper mTestableLooper; @@ -127,19 +125,18 @@ public class BackAnimationControllerTest extends ShellTestCase { mController = new BackAnimationController(mShellInit, mShellController, mShellExecutor, new Handler(mTestableLooper.getLooper()), mActivityTaskManager, mContext, - mContentResolver, mTransitions); + 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); } @@ -180,26 +177,47 @@ public class BackAnimationControllerTest extends ShellTestCase { } @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); @@ -207,14 +225,16 @@ public class BackAnimationControllerTest extends ShellTestCase { 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 @@ -225,99 +245,96 @@ public class BackAnimationControllerTest extends ShellTestCase { mController = new BackAnimationController(shellInit, mShellController, mShellExecutor, new Handler(mTestableLooper.getLooper()), mActivityTaskManager, mContext, - mContentResolver, mTransitions); + 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. @@ -327,11 +344,11 @@ public class BackAnimationControllerTest extends ShellTestCase { 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) { @@ -339,7 +356,6 @@ public class BackAnimationControllerTest extends ShellTestCase { coordinate, coordinate, actionDown, BackEvent.EDGE_LEFT); - mEventTime += 10; } private void simulateRemoteAnimationStart(int type) throws RemoteException { @@ -351,4 +367,14 @@ public class BackAnimationControllerTest extends ShellTestCase { 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 a6f19e7d11d3..40f2e88f34fd 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 @@ public class DisplayImeControllerTest extends ShellTestCase { @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 39db328ef0e0..956f1cd419c2 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.ArgumentMatchers.notNull; 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.IWindowManager; 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 @@ public class DisplayInsetsControllerTest extends ShellTestCase { 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 @@ public class DisplayInsetsControllerTest extends ShellTestCase { 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 @@ public class DisplayInsetsControllerTest extends ShellTestCase { } @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/desktopmode/DesktopModeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java index 79b520c734c8..89bafcb6b2f4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java @@ -16,10 +16,13 @@ package com.android.wm.shell.desktopmode; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.app.WindowConfiguration.WINDOW_CONFIG_BOUNDS; +import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER; @@ -30,13 +33,14 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.app.ActivityManager; +import android.app.ActivityManager.RunningTaskInfo; import android.os.Binder; import android.os.Handler; import android.os.IBinder; @@ -68,6 +72,9 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.Mockito; +import java.util.ArrayList; +import java.util.Arrays; + @SmallTest @RunWith(AndroidTestingRunner.class) public class DesktopModeControllerTest extends ShellTestCase { @@ -83,9 +90,7 @@ public class DesktopModeControllerTest extends ShellTestCase { @Mock private Handler mMockHandler; @Mock - private Transitions mMockTransitions; - private TestShellExecutor mExecutor; - + private Transitions mTransitions; private DesktopModeController mController; private DesktopModeTaskRepository mDesktopModeTaskRepository; private ShellInit mShellInit; @@ -97,20 +102,19 @@ public class DesktopModeControllerTest extends ShellTestCase { when(DesktopModeStatus.isActive(any())).thenReturn(true); mShellInit = Mockito.spy(new ShellInit(mTestExecutor)); - mExecutor = new TestShellExecutor(); mDesktopModeTaskRepository = new DesktopModeTaskRepository(); mController = new DesktopModeController(mContext, mShellInit, mShellController, - mShellTaskOrganizer, mRootTaskDisplayAreaOrganizer, mMockTransitions, - mDesktopModeTaskRepository, mMockHandler, mExecutor); + mShellTaskOrganizer, mRootTaskDisplayAreaOrganizer, mTransitions, + mDesktopModeTaskRepository, mMockHandler, new TestShellExecutor()); - when(mShellTaskOrganizer.prepareClearFreeformForStandardTasks(anyInt())).thenReturn( - new WindowContainerTransaction()); + when(mShellTaskOrganizer.getRunningTasks(anyInt())).thenReturn(new ArrayList<>()); mShellInit.init(); clearInvocations(mShellTaskOrganizer); clearInvocations(mRootTaskDisplayAreaOrganizer); + clearInvocations(mTransitions); } @After @@ -124,113 +128,133 @@ public class DesktopModeControllerTest extends ShellTestCase { } @Test - public void testDesktopModeEnabled_taskWmClearedDisplaySetToFreeform() { - // Create a fake WCT to simulate setting task windowing mode to undefined - WindowContainerTransaction taskWct = new WindowContainerTransaction(); - MockToken taskMockToken = new MockToken(); - taskWct.setWindowingMode(taskMockToken.token(), WINDOWING_MODE_UNDEFINED); - when(mShellTaskOrganizer.prepareClearFreeformForStandardTasks( - mContext.getDisplayId())).thenReturn(taskWct); - - // Create a fake DisplayAreaInfo to check if windowing mode change is set correctly - MockToken displayMockToken = new MockToken(); - DisplayAreaInfo displayAreaInfo = new DisplayAreaInfo(displayMockToken.mToken, - mContext.getDisplayId(), 0); - when(mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(mContext.getDisplayId())) - .thenReturn(displayAreaInfo); + public void testDesktopModeEnabled_rootTdaSetToFreeform() { + DisplayAreaInfo displayAreaInfo = createMockDisplayArea(); - // The test mController.updateDesktopModeActive(true); + WindowContainerTransaction wct = getDesktopModeSwitchTransaction(); + + // 1 change: Root TDA windowing mode + assertThat(wct.getChanges().size()).isEqualTo(1); + // Verify WCT has a change for setting windowing mode to freeform + Change change = wct.getChanges().get(displayAreaInfo.token.asBinder()); + assertThat(change).isNotNull(); + assertThat(change.getWindowingMode()).isEqualTo(WINDOWING_MODE_FREEFORM); + } - ArgumentCaptor<WindowContainerTransaction> arg = ArgumentCaptor.forClass( - WindowContainerTransaction.class); - verify(mRootTaskDisplayAreaOrganizer).applyTransaction(arg.capture()); + @Test + public void testDesktopModeDisabled_rootTdaSetToFullscreen() { + DisplayAreaInfo displayAreaInfo = createMockDisplayArea(); - // WCT should have 2 changes - clear task wm mode and set display wm mode - WindowContainerTransaction wct = arg.getValue(); - assertThat(wct.getChanges()).hasSize(2); + mController.updateDesktopModeActive(false); + WindowContainerTransaction wct = getDesktopModeSwitchTransaction(); + + // 1 change: Root TDA windowing mode + assertThat(wct.getChanges().size()).isEqualTo(1); + // Verify WCT has a change for setting windowing mode to fullscreen + Change change = wct.getChanges().get(displayAreaInfo.token.asBinder()); + assertThat(change).isNotNull(); + assertThat(change.getWindowingMode()).isEqualTo(WINDOWING_MODE_FULLSCREEN); + } - // Verify executed WCT has a change for setting task windowing mode to undefined - Change taskWmModeChange = wct.getChanges().get(taskMockToken.binder()); - assertThat(taskWmModeChange).isNotNull(); - assertThat(taskWmModeChange.getWindowingMode()).isEqualTo(WINDOWING_MODE_UNDEFINED); + @Test + public void testDesktopModeEnabled_windowingModeCleared() { + createMockDisplayArea(); + RunningTaskInfo freeformTask = createFreeformTask(); + RunningTaskInfo fullscreenTask = createFullscreenTask(); + RunningTaskInfo homeTask = createHomeTask(); + when(mShellTaskOrganizer.getRunningTasks(anyInt())).thenReturn(new ArrayList<>( + Arrays.asList(freeformTask, fullscreenTask, homeTask))); - // Verify executed WCT has a change for setting display windowing mode to freeform - Change displayWmModeChange = wct.getChanges().get(displayAreaInfo.token.asBinder()); - assertThat(displayWmModeChange).isNotNull(); - assertThat(displayWmModeChange.getWindowingMode()).isEqualTo(WINDOWING_MODE_FREEFORM); + mController.updateDesktopModeActive(true); + WindowContainerTransaction wct = getDesktopModeSwitchTransaction(); + + // 2 changes: Root TDA windowing mode and 1 task + assertThat(wct.getChanges().size()).isEqualTo(2); + // No changes for tasks that are not standard or freeform + assertThat(wct.getChanges().get(fullscreenTask.token.asBinder())).isNull(); + assertThat(wct.getChanges().get(homeTask.token.asBinder())).isNull(); + // Standard freeform task has windowing mode cleared + Change change = wct.getChanges().get(freeformTask.token.asBinder()); + assertThat(change).isNotNull(); + assertThat(change.getWindowingMode()).isEqualTo(WINDOWING_MODE_UNDEFINED); } @Test - public void testDesktopModeDisabled_taskWmAndBoundsClearedDisplaySetToFullscreen() { - // Create a fake WCT to simulate setting task windowing mode to undefined - WindowContainerTransaction taskWmWct = new WindowContainerTransaction(); - MockToken taskWmMockToken = new MockToken(); - taskWmWct.setWindowingMode(taskWmMockToken.token(), WINDOWING_MODE_UNDEFINED); - when(mShellTaskOrganizer.prepareClearFreeformForStandardTasks( - mContext.getDisplayId())).thenReturn(taskWmWct); - - // Create a fake WCT to simulate clearing task bounds - WindowContainerTransaction taskBoundsWct = new WindowContainerTransaction(); - MockToken taskBoundsMockToken = new MockToken(); - taskBoundsWct.setBounds(taskBoundsMockToken.token(), null); - when(mShellTaskOrganizer.prepareClearBoundsForStandardTasks( - mContext.getDisplayId())).thenReturn(taskBoundsWct); - - // Create a fake DisplayAreaInfo to check if windowing mode change is set correctly - MockToken displayMockToken = new MockToken(); - DisplayAreaInfo displayAreaInfo = new DisplayAreaInfo(displayMockToken.mToken, - mContext.getDisplayId(), 0); - when(mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(mContext.getDisplayId())) - .thenReturn(displayAreaInfo); + public void testDesktopModeDisabled_windowingModeAndBoundsCleared() { + createMockDisplayArea(); + RunningTaskInfo freeformTask = createFreeformTask(); + RunningTaskInfo fullscreenTask = createFullscreenTask(); + RunningTaskInfo homeTask = createHomeTask(); + when(mShellTaskOrganizer.getRunningTasks(anyInt())).thenReturn(new ArrayList<>( + Arrays.asList(freeformTask, fullscreenTask, homeTask))); - // The test mController.updateDesktopModeActive(false); + WindowContainerTransaction wct = getDesktopModeSwitchTransaction(); + + // 3 changes: Root TDA windowing mode and 2 tasks + assertThat(wct.getChanges().size()).isEqualTo(3); + // No changes to home task + assertThat(wct.getChanges().get(homeTask.token.asBinder())).isNull(); + // Standard tasks have bounds cleared + assertThatBoundsCleared(wct.getChanges().get(freeformTask.token.asBinder())); + assertThatBoundsCleared(wct.getChanges().get(fullscreenTask.token.asBinder())); + // Freeform standard tasks have windowing mode cleared + assertThat(wct.getChanges().get( + freeformTask.token.asBinder()).getWindowingMode()).isEqualTo( + WINDOWING_MODE_UNDEFINED); + } - ArgumentCaptor<WindowContainerTransaction> arg = ArgumentCaptor.forClass( - WindowContainerTransaction.class); - verify(mRootTaskDisplayAreaOrganizer).applyTransaction(arg.capture()); - - // WCT should have 3 changes - clear task wm mode and bounds and set display wm mode - WindowContainerTransaction wct = arg.getValue(); - assertThat(wct.getChanges()).hasSize(3); - - // Verify executed WCT has a change for setting task windowing mode to undefined - Change taskWmMode = wct.getChanges().get(taskWmMockToken.binder()); - assertThat(taskWmMode).isNotNull(); - assertThat(taskWmMode.getWindowingMode()).isEqualTo(WINDOWING_MODE_UNDEFINED); - - // Verify executed WCT has a change for clearing task bounds - Change bounds = wct.getChanges().get(taskBoundsMockToken.binder()); - assertThat(bounds).isNotNull(); - assertThat(bounds.getWindowSetMask() & WINDOW_CONFIG_BOUNDS).isNotEqualTo(0); - assertThat(bounds.getConfiguration().windowConfiguration.getBounds().isEmpty()).isTrue(); - - // Verify executed WCT has a change for setting display windowing mode to fullscreen - Change displayWmModeChange = wct.getChanges().get(displayAreaInfo.token.asBinder()); - assertThat(displayWmModeChange).isNotNull(); - assertThat(displayWmModeChange.getWindowingMode()).isEqualTo(WINDOWING_MODE_FULLSCREEN); + @Test + public void testDesktopModeEnabled_homeTaskBehindVisibleTask() { + createMockDisplayArea(); + RunningTaskInfo fullscreenTask1 = createFullscreenTask(); + fullscreenTask1.isVisible = true; + RunningTaskInfo fullscreenTask2 = createFullscreenTask(); + fullscreenTask2.isVisible = false; + RunningTaskInfo homeTask = createHomeTask(); + when(mShellTaskOrganizer.getRunningTasks(anyInt())).thenReturn(new ArrayList<>( + Arrays.asList(fullscreenTask1, fullscreenTask2, homeTask))); + + mController.updateDesktopModeActive(true); + WindowContainerTransaction wct = getDesktopModeSwitchTransaction(); + + // Check that there are hierarchy changes for home task and visible task + assertThat(wct.getHierarchyOps()).hasSize(2); + // First show home task + WindowContainerTransaction.HierarchyOp op1 = wct.getHierarchyOps().get(0); + assertThat(op1.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER); + assertThat(op1.getContainer()).isEqualTo(homeTask.token.asBinder()); + + // Then visible task on top of it + WindowContainerTransaction.HierarchyOp op2 = wct.getHierarchyOps().get(1); + assertThat(op2.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER); + assertThat(op2.getContainer()).isEqualTo(fullscreenTask1.token.asBinder()); } @Test public void testShowDesktopApps() { // Set up two active tasks on desktop - mDesktopModeTaskRepository.addActiveTask(1); - mDesktopModeTaskRepository.addActiveTask(2); - MockToken token1 = new MockToken(); - MockToken token2 = new MockToken(); - ActivityManager.RunningTaskInfo taskInfo1 = new TestRunningTaskInfoBuilder().setToken( - token1.token()).setLastActiveTime(100).build(); - ActivityManager.RunningTaskInfo taskInfo2 = new TestRunningTaskInfoBuilder().setToken( - token2.token()).setLastActiveTime(200).build(); - when(mShellTaskOrganizer.getRunningTaskInfo(1)).thenReturn(taskInfo1); - when(mShellTaskOrganizer.getRunningTaskInfo(2)).thenReturn(taskInfo2); + RunningTaskInfo freeformTask1 = createFreeformTask(); + freeformTask1.lastActiveTime = 100; + RunningTaskInfo freeformTask2 = createFreeformTask(); + freeformTask2.lastActiveTime = 200; + mDesktopModeTaskRepository.addActiveTask(freeformTask1.taskId); + mDesktopModeTaskRepository.addActiveTask(freeformTask2.taskId); + when(mShellTaskOrganizer.getRunningTaskInfo(freeformTask1.taskId)).thenReturn( + freeformTask1); + when(mShellTaskOrganizer.getRunningTaskInfo(freeformTask2.taskId)).thenReturn( + freeformTask2); // Run show desktop apps logic mController.showDesktopApps(); ArgumentCaptor<WindowContainerTransaction> wctCaptor = ArgumentCaptor.forClass( WindowContainerTransaction.class); - verify(mShellTaskOrganizer).applyTransaction(wctCaptor.capture()); + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + verify(mTransitions).startTransition(eq(TRANSIT_TO_FRONT), wctCaptor.capture(), any()); + } else { + verify(mShellTaskOrganizer).applyTransaction(wctCaptor.capture()); + } WindowContainerTransaction wct = wctCaptor.getValue(); // Check wct has reorder calls @@ -239,12 +263,12 @@ public class DesktopModeControllerTest extends ShellTestCase { // Task 2 has activity later, must be first WindowContainerTransaction.HierarchyOp op1 = wct.getHierarchyOps().get(0); assertThat(op1.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER); - assertThat(op1.getContainer()).isEqualTo(token2.binder()); + assertThat(op1.getContainer()).isEqualTo(freeformTask2.token.asBinder()); // Task 1 should be second - WindowContainerTransaction.HierarchyOp op2 = wct.getHierarchyOps().get(0); + WindowContainerTransaction.HierarchyOp op2 = wct.getHierarchyOps().get(1); assertThat(op2.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER); - assertThat(op2.getContainer()).isEqualTo(token2.binder()); + assertThat(op2.getContainer()).isEqualTo(freeformTask1.token.asBinder()); } @Test @@ -266,7 +290,7 @@ public class DesktopModeControllerTest extends ShellTestCase { @Test public void testHandleTransitionRequest_notFreeform_returnsNull() { - ActivityManager.RunningTaskInfo trigger = new ActivityManager.RunningTaskInfo(); + RunningTaskInfo trigger = new RunningTaskInfo(); trigger.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); WindowContainerTransaction wct = mController.handleRequest( new Binder(), @@ -276,7 +300,7 @@ public class DesktopModeControllerTest extends ShellTestCase { @Test public void testHandleTransitionRequest_returnsWct() { - ActivityManager.RunningTaskInfo trigger = new ActivityManager.RunningTaskInfo(); + RunningTaskInfo trigger = new RunningTaskInfo(); trigger.token = new MockToken().mToken; trigger.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); WindowContainerTransaction wct = mController.handleRequest( @@ -285,6 +309,57 @@ public class DesktopModeControllerTest extends ShellTestCase { assertThat(wct).isNotNull(); } + private DisplayAreaInfo createMockDisplayArea() { + DisplayAreaInfo displayAreaInfo = new DisplayAreaInfo(new MockToken().mToken, + mContext.getDisplayId(), 0); + when(mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(mContext.getDisplayId())) + .thenReturn(displayAreaInfo); + return displayAreaInfo; + } + + private RunningTaskInfo createFreeformTask() { + return new TestRunningTaskInfoBuilder() + .setToken(new MockToken().token()) + .setActivityType(ACTIVITY_TYPE_STANDARD) + .setWindowingMode(WINDOWING_MODE_FREEFORM) + .setLastActiveTime(100) + .build(); + } + + private RunningTaskInfo createFullscreenTask() { + return new TestRunningTaskInfoBuilder() + .setToken(new MockToken().token()) + .setActivityType(ACTIVITY_TYPE_STANDARD) + .setWindowingMode(WINDOWING_MODE_FULLSCREEN) + .setLastActiveTime(100) + .build(); + } + + private RunningTaskInfo createHomeTask() { + return new TestRunningTaskInfoBuilder() + .setToken(new MockToken().token()) + .setActivityType(ACTIVITY_TYPE_HOME) + .setWindowingMode(WINDOWING_MODE_FULLSCREEN) + .setLastActiveTime(100) + .build(); + } + + private WindowContainerTransaction getDesktopModeSwitchTransaction() { + ArgumentCaptor<WindowContainerTransaction> arg = ArgumentCaptor.forClass( + WindowContainerTransaction.class); + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + verify(mTransitions).startTransition(eq(TRANSIT_CHANGE), arg.capture(), any()); + } else { + verify(mRootTaskDisplayAreaOrganizer).applyTransaction(arg.capture()); + } + return arg.getValue(); + } + + private void assertThatBoundsCleared(Change change) { + assertThat((change.getWindowSetMask() & WINDOW_CONFIG_BOUNDS) != 0).isTrue(); + assertThat(change.getConfiguration().windowConfiguration.getBounds().isEmpty()).isTrue(); + } + private static class MockToken { private final WindowContainerToken mToken; private final IBinder mBinder; @@ -298,9 +373,5 @@ public class DesktopModeControllerTest extends ShellTestCase { WindowContainerToken token() { return mToken; } - - IBinder binder() { - return mBinder; - } } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/BackgroundWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/BackgroundWindowManagerTest.java index 11948dbf9659..f3f70673b332 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/BackgroundWindowManagerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/BackgroundWindowManagerTest.java @@ -24,7 +24,6 @@ import androidx.test.annotation.UiThreadTest; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import com.android.wm.shell.RootDisplayAreaOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayLayout; @@ -42,13 +41,11 @@ public class BackgroundWindowManagerTest extends ShellTestCase { private BackgroundWindowManager mBackgroundWindowManager; @Mock private DisplayLayout mMockDisplayLayout; - @Mock - private RootDisplayAreaOrganizer mRootDisplayAreaOrganizer; @Before public void setup() { MockitoAnnotations.initMocks(this); - mBackgroundWindowManager = new BackgroundWindowManager(mContext, mRootDisplayAreaOrganizer); + mBackgroundWindowManager = new BackgroundWindowManager(mContext); mBackgroundWindowManager.onDisplayChanged(mMockDisplayLayout); } 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 d01f3d310fc3..38b75f81171f 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.verify; 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 com.android.wm.shell.transition.Transitions; 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 @@ public class SplitScreenControllerTests extends ShellTestCase { @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 @@ public class SplitScreenControllerTests extends ShellTestCase { } @Test - public void testShouldAddMultipleTaskFlag_notInSplitScreen() { - doReturn(false).when(mSplitScreenController).isSplitScreenVisible(); - doReturn(true).when(mSplitScreenController).isValidToEnterSplitScreen(any()); + public void testStartIntent_appendsNoUserActionFlag() { + Intent startIntent = createStartIntent("startActivity"); + PendingIntent pendingIntent = + PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE); - // Verify launching the same activity returns true. + 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 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(mSplitScreenController).getFocusingTaskInfo(); - assertTrue(mSplitScreenController.shouldAddMultipleTaskFlag( - startIntent, SPLIT_POSITION_TOP_OR_LEFT)); - - // 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)); + 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 testShouldAddMultipleTaskFlag_inSplitScreen() { + 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); + 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()); + + 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_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); - Intent diffIntent = createStartIntent("diffActivity"); - ActivityManager.RunningTaskInfo differentTaskInfo = - createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, diffIntent); - - // 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)); - - // 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 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)); + 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/windowdecor/TaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/TaskPositionerTest.kt new file mode 100644 index 000000000000..ac10ddb0116a --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/TaskPositionerTest.kt @@ -0,0 +1,130 @@ +package com.android.wm.shell.windowdecor + +import android.app.ActivityManager +import android.graphics.Rect +import android.os.IBinder +import android.testing.AndroidTestingRunner +import android.window.WindowContainerToken +import android.window.WindowContainerTransaction.Change.CHANGE_DRAG_RESIZING +import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.windowdecor.TaskPositioner.CTRL_TYPE_RIGHT +import com.android.wm.shell.windowdecor.TaskPositioner.CTRL_TYPE_UNDEFINED +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.argThat +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations + +/** + * Tests for [TaskPositioner]. + * + * Build/Install/Run: + * atest WMShellUnitTests:TaskPositionerTest + */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +class TaskPositionerTest : ShellTestCase() { + + @Mock + private lateinit var mockShellTaskOrganizer: ShellTaskOrganizer + @Mock + private lateinit var mockWindowDecoration: WindowDecoration<*> + @Mock + private lateinit var mockDragStartListener: TaskPositioner.DragStartListener + + @Mock + private lateinit var taskToken: WindowContainerToken + @Mock + private lateinit var taskBinder: IBinder + + private lateinit var taskPositioner: TaskPositioner + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + taskPositioner = TaskPositioner( + mockShellTaskOrganizer, + mockWindowDecoration, + mockDragStartListener + ) + `when`(taskToken.asBinder()).thenReturn(taskBinder) + mockWindowDecoration.mTaskInfo = ActivityManager.RunningTaskInfo().apply { + taskId = TASK_ID + token = taskToken + configuration.windowConfiguration.bounds = STARTING_BOUNDS + } + } + + @Test + fun testDragResize_move_skipsDragResizingFlag() { + taskPositioner.onDragResizeStart( + CTRL_TYPE_UNDEFINED, // Move + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat() + ) + + // Move the task 10px to the right. + val newX = STARTING_BOUNDS.left.toFloat() + 10 + val newY = STARTING_BOUNDS.top.toFloat() + taskPositioner.onDragResizeMove( + newX, + newY + ) + + taskPositioner.onDragResizeEnd(newX, newY) + + verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + ((change.changeMask and CHANGE_DRAG_RESIZING) != 0) && + change.dragResizing + } + }) + } + + @Test + fun testDragResize_resize_setsDragResizingFlag() { + taskPositioner.onDragResizeStart( + CTRL_TYPE_RIGHT, // Resize right + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat() + ) + + // Resize the task by 10px to the right. + val newX = STARTING_BOUNDS.right.toFloat() + 10 + val newY = STARTING_BOUNDS.top.toFloat() + taskPositioner.onDragResizeMove( + newX, + newY + ) + + taskPositioner.onDragResizeEnd(newX, newY) + + verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + ((change.changeMask and CHANGE_DRAG_RESIZING) != 0) && + change.dragResizing + } + }) + verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + ((change.changeMask and CHANGE_DRAG_RESIZING) != 0) && + !change.dragResizing + } + }) + } + + companion object { + private const val TASK_ID = 5 + private val STARTING_BOUNDS = Rect(0, 0, 100, 100) + } +} diff --git a/libs/androidfw/Android.bp b/libs/androidfw/Android.bp index eb8d26adc7d7..b1f327c94f8e 100644 --- a/libs/androidfw/Android.bp +++ b/libs/androidfw/Android.bp @@ -211,6 +211,8 @@ cc_test { "tests/data/**/*.apk", "tests/data/**/*.arsc", "tests/data/**/*.idmap", + ":FrameworkResourcesSparseTestApp", + ":FrameworkResourcesNotSparseTestApp", ], test_suites: ["device-tests"], } diff --git a/libs/androidfw/ZipUtils.cpp b/libs/androidfw/ZipUtils.cpp index 58fc5bbbab5e..a1385f2cf7b1 100644 --- a/libs/androidfw/ZipUtils.cpp +++ b/libs/androidfw/ZipUtils.cpp @@ -35,7 +35,7 @@ using namespace android; // TODO: This can go away once the only remaining usage in aapt goes away. -class FileReader : public zip_archive::Reader { +class FileReader final : public zip_archive::Reader { public: explicit FileReader(FILE* fp) : Reader(), mFp(fp), mCurrentOffset(0) { } @@ -66,7 +66,7 @@ class FileReader : public zip_archive::Reader { mutable off64_t mCurrentOffset; }; -class FdReader : public zip_archive::Reader { +class FdReader final : public zip_archive::Reader { public: explicit FdReader(int fd) : mFd(fd) { } @@ -79,7 +79,7 @@ class FdReader : public zip_archive::Reader { const int mFd; }; -class BufferReader : public zip_archive::Reader { +class BufferReader final : public zip_archive::Reader { public: BufferReader(incfs::map_ptr<void> input, size_t inputSize) : Reader(), mInput(input.convert<uint8_t>()), @@ -105,7 +105,7 @@ class BufferReader : public zip_archive::Reader { const size_t mInputSize; }; -class BufferWriter : public zip_archive::Writer { +class BufferWriter final : public zip_archive::Writer { public: BufferWriter(void* output, size_t outputSize) : Writer(), mOutput(reinterpret_cast<uint8_t*>(output)), mOutputSize(outputSize), mBytesWritten(0) { diff --git a/libs/androidfw/include/androidfw/ResourceTypes.h b/libs/androidfw/include/androidfw/ResourceTypes.h index 9309091f4124..a625889eaf3c 100644 --- a/libs/androidfw/include/androidfw/ResourceTypes.h +++ b/libs/androidfw/include/androidfw/ResourceTypes.h @@ -53,7 +53,7 @@ constexpr const uint32_t kFabricatedOverlayMagic = 0x4f525246; // FRRO (big endi // The version should only be changed when a backwards-incompatible change must be made to the // fabricated overlay file format. Old fabricated overlays must be migrated to the new file format // to prevent losing fabricated overlay data. -constexpr const uint32_t kFabricatedOverlayCurrentVersion = 2; +constexpr const uint32_t kFabricatedOverlayCurrentVersion = 3; // Returns whether or not the path represents a fabricated overlay. bool IsFabricatedOverlay(const std::string& path); diff --git a/libs/androidfw/tests/LoadedArsc_test.cpp b/libs/androidfw/tests/LoadedArsc_test.cpp index d214e2dfef7b..c90ec197b5ef 100644 --- a/libs/androidfw/tests/LoadedArsc_test.cpp +++ b/libs/androidfw/tests/LoadedArsc_test.cpp @@ -71,62 +71,6 @@ TEST(LoadedArscTest, LoadSinglePackageArsc) { ASSERT_TRUE(LoadedPackage::GetEntry(type.type, entry_index).has_value()); } -TEST(LoadedArscTest, LoadSparseEntryApp) { - std::string contents; - ASSERT_TRUE(ReadFileFromZipToString(GetTestDataPath() + "/sparse/sparse.apk", "resources.arsc", - &contents)); - - std::unique_ptr<const LoadedArsc> loaded_arsc = LoadedArsc::Load(contents.data(), - contents.length()); - ASSERT_THAT(loaded_arsc, NotNull()); - - const LoadedPackage* package = - loaded_arsc->GetPackageById(get_package_id(sparse::R::integer::foo_9)); - ASSERT_THAT(package, NotNull()); - - const uint8_t type_index = get_type_id(sparse::R::integer::foo_9) - 1; - const uint16_t entry_index = get_entry_id(sparse::R::integer::foo_9); - - const TypeSpec* type_spec = package->GetTypeSpecByTypeIndex(type_index); - ASSERT_THAT(type_spec, NotNull()); - ASSERT_THAT(type_spec->type_entries.size(), Ge(1u)); - - auto type = type_spec->type_entries[0]; - ASSERT_TRUE(LoadedPackage::GetEntry(type.type, entry_index).has_value()); -} - -TEST(LoadedArscTest, FindSparseEntryApp) { - std::string contents; - ASSERT_TRUE(ReadFileFromZipToString(GetTestDataPath() + "/sparse/sparse.apk", "resources.arsc", - &contents)); - - std::unique_ptr<const LoadedArsc> loaded_arsc = LoadedArsc::Load(contents.data(), - contents.length()); - ASSERT_THAT(loaded_arsc, NotNull()); - - const LoadedPackage* package = - loaded_arsc->GetPackageById(get_package_id(sparse::R::string::only_v26)); - ASSERT_THAT(package, NotNull()); - - const uint8_t type_index = get_type_id(sparse::R::string::only_v26) - 1; - const uint16_t entry_index = get_entry_id(sparse::R::string::only_v26); - - const TypeSpec* type_spec = package->GetTypeSpecByTypeIndex(type_index); - ASSERT_THAT(type_spec, NotNull()); - ASSERT_THAT(type_spec->type_entries.size(), Ge(1u)); - - // Ensure that AAPT2 sparsely encoded the v26 config as expected. - auto type_entry = std::find_if( - type_spec->type_entries.begin(), type_spec->type_entries.end(), - [](const TypeSpec::TypeEntry& x) { return x.config.sdkVersion == 26; }); - ASSERT_NE(type_entry, type_spec->type_entries.end()); - ASSERT_NE(type_entry->type->flags & ResTable_type::FLAG_SPARSE, 0); - - // Test fetching a resource with only sparsely encoded configs by name. - auto id = package->FindEntryByName(u"string", u"only_v26"); - ASSERT_EQ(id.value(), fix_package_id(sparse::R::string::only_v26, 0)); -} - TEST(LoadedArscTest, LoadSharedLibrary) { std::string contents; ASSERT_TRUE(ReadFileFromZipToString(GetTestDataPath() + "/lib_one/lib_one.apk", "resources.arsc", @@ -404,4 +348,84 @@ TEST(LoadedArscTest, LoadCustomLoader) { // sizeof(Res_value) might not be backwards compatible. // TEST(LoadedArscTest, LoadingShouldBeForwardsAndBackwardsCompatible) { ASSERT_TRUE(false); } +class LoadedArscParameterizedTest : + public testing::TestWithParam<std::string> { +}; + +TEST_P(LoadedArscParameterizedTest, LoadSparseEntryApp) { + std::string contents; + ASSERT_TRUE(ReadFileFromZipToString(GetParam(), "resources.arsc", &contents)); + + std::unique_ptr<const LoadedArsc> loaded_arsc = LoadedArsc::Load(contents.data(), + contents.length()); + ASSERT_THAT(loaded_arsc, NotNull()); + + const LoadedPackage* package = + loaded_arsc->GetPackageById(get_package_id(sparse::R::integer::foo_9)); + ASSERT_THAT(package, NotNull()); + + const uint8_t type_index = get_type_id(sparse::R::integer::foo_9) - 1; + const uint16_t entry_index = get_entry_id(sparse::R::integer::foo_9); + + const TypeSpec* type_spec = package->GetTypeSpecByTypeIndex(type_index); + ASSERT_THAT(type_spec, NotNull()); + ASSERT_THAT(type_spec->type_entries.size(), Ge(1u)); + + auto type = type_spec->type_entries[0]; + ASSERT_TRUE(LoadedPackage::GetEntry(type.type, entry_index).has_value()); +} + +TEST_P(LoadedArscParameterizedTest, FindSparseEntryApp) { + std::string contents; + ASSERT_TRUE(ReadFileFromZipToString(GetParam(), "resources.arsc", &contents)); + + std::unique_ptr<const LoadedArsc> loaded_arsc = LoadedArsc::Load(contents.data(), + contents.length()); + ASSERT_THAT(loaded_arsc, NotNull()); + + const LoadedPackage* package = + loaded_arsc->GetPackageById(get_package_id(sparse::R::string::only_land)); + ASSERT_THAT(package, NotNull()); + + const uint8_t type_index = get_type_id(sparse::R::string::only_land) - 1; + + const TypeSpec* type_spec = package->GetTypeSpecByTypeIndex(type_index); + ASSERT_THAT(type_spec, NotNull()); + ASSERT_THAT(type_spec->type_entries.size(), Ge(1u)); + + // Type Entry with default orientation is not sparse encoded because the ratio of + // populated entries to total entries is above threshold. + // Only find out default locale because Soong build system will introduce pseudo + // locales for the apk generated at runtime. + auto type_entry_default = std::find_if( + type_spec->type_entries.begin(), type_spec->type_entries.end(), + [] (const TypeSpec::TypeEntry& x) { return x.config.orientation == 0 && + x.config.locale == 0; }); + ASSERT_NE(type_entry_default, type_spec->type_entries.end()); + ASSERT_EQ(type_entry_default->type->flags & ResTable_type::FLAG_SPARSE, 0); + + // Type Entry with land orientation is sparse encoded as expected. + // Only find out default locale because Soong build system will introduce pseudo + // locales for the apk generated at runtime. + auto type_entry_land = std::find_if( + type_spec->type_entries.begin(), type_spec->type_entries.end(), + [](const TypeSpec::TypeEntry& x) { return x.config.orientation == + ResTable_config::ORIENTATION_LAND && + x.config.locale == 0; }); + ASSERT_NE(type_entry_land, type_spec->type_entries.end()); + ASSERT_NE(type_entry_land->type->flags & ResTable_type::FLAG_SPARSE, 0); + + // Test fetching a resource with only sparsely encoded configs by name. + auto id = package->FindEntryByName(u"string", u"only_land"); + ASSERT_EQ(id.value(), fix_package_id(sparse::R::string::only_land, 0)); +} + +INSTANTIATE_TEST_SUITE_P( + FrameWorkResourcesLoadedArscTests, + LoadedArscParameterizedTest, + ::testing::Values( + base::GetExecutableDirectory() + "/tests/data/sparse/sparse.apk", + base::GetExecutableDirectory() + "/FrameworkResourcesSparseTestApp.apk" + )); + } // namespace android diff --git a/libs/androidfw/tests/ResTable_test.cpp b/libs/androidfw/tests/ResTable_test.cpp index 9aeb00c47e63..fbf70981f2de 100644 --- a/libs/androidfw/tests/ResTable_test.cpp +++ b/libs/androidfw/tests/ResTable_test.cpp @@ -15,6 +15,7 @@ */ #include "androidfw/ResourceTypes.h" +#include "android-base/file.h" #include <codecvt> #include <locale> @@ -41,34 +42,6 @@ TEST(ResTableTest, ShouldLoadSuccessfully) { ASSERT_EQ(NO_ERROR, table.add(contents.data(), contents.size())); } -TEST(ResTableTest, ShouldLoadSparseEntriesSuccessfully) { - std::string contents; - ASSERT_TRUE(ReadFileFromZipToString(GetTestDataPath() + "/sparse/sparse.apk", "resources.arsc", - &contents)); - - ResTable table; - ASSERT_EQ(NO_ERROR, table.add(contents.data(), contents.size())); - - ResTable_config config; - memset(&config, 0, sizeof(config)); - config.sdkVersion = 26; - table.setParameters(&config); - - String16 name(u"com.android.sparse:integer/foo_9"); - uint32_t flags; - uint32_t resid = - table.identifierForName(name.string(), name.size(), nullptr, 0, nullptr, 0, &flags); - ASSERT_NE(0u, resid); - - Res_value val; - ResTable_config selected_config; - ASSERT_GE( - table.getResource(resid, &val, false /*mayBeBag*/, 0u /*density*/, &flags, &selected_config), - 0); - EXPECT_EQ(Res_value::TYPE_INT_DEC, val.dataType); - EXPECT_EQ(900u, val.data); -} - TEST(ResTableTest, SimpleTypeIsRetrievedCorrectly) { std::string contents; ASSERT_TRUE(ReadFileFromZipToString(GetTestDataPath() + "/basic/basic.apk", @@ -476,4 +449,43 @@ TEST(ResTableTest, TruncatedEncodeLength) { ASSERT_FALSE(invalid_pool->stringAt(invalid_val.data).has_value()); } +class ResTableParameterizedTest : + public testing::TestWithParam<std::string> { +}; + +TEST_P(ResTableParameterizedTest, ShouldLoadSparseEntriesSuccessfully) { + std::string contents; + ASSERT_TRUE(ReadFileFromZipToString(GetParam(), "resources.arsc", &contents)); + + ResTable table; + ASSERT_EQ(NO_ERROR, table.add(contents.data(), contents.size())); + + ResTable_config config; + memset(&config, 0, sizeof(config)); + config.orientation = ResTable_config::ORIENTATION_LAND; + table.setParameters(&config); + + String16 name(u"com.android.sparse:integer/foo_9"); + uint32_t flags; + uint32_t resid = + table.identifierForName(name.string(), name.size(), nullptr, 0, nullptr, 0, &flags); + ASSERT_NE(0u, resid); + + Res_value val; + ResTable_config selected_config; + ASSERT_GE( + table.getResource(resid, &val, false /*mayBeBag*/, 0u /*density*/, &flags, &selected_config), + 0); + EXPECT_EQ(Res_value::TYPE_INT_DEC, val.dataType); + EXPECT_EQ(900u, val.data); +} + +INSTANTIATE_TEST_SUITE_P( + FrameWorkResourcesResTableTests, + ResTableParameterizedTest, + ::testing::Values( + base::GetExecutableDirectory() + "/tests/data/sparse/sparse.apk", + base::GetExecutableDirectory() + "/FrameworkResourcesSparseTestApp.apk" + )); + } // namespace android diff --git a/libs/androidfw/tests/SparseEntry_bench.cpp b/libs/androidfw/tests/SparseEntry_bench.cpp index c9b4ad8af278..fffeeb802873 100644 --- a/libs/androidfw/tests/SparseEntry_bench.cpp +++ b/libs/androidfw/tests/SparseEntry_bench.cpp @@ -16,6 +16,7 @@ #include "androidfw/AssetManager.h" #include "androidfw/ResourceTypes.h" +#include "android-base/file.h" #include "BenchmarkHelpers.h" #include "data/sparse/R.h" @@ -24,40 +25,74 @@ namespace sparse = com::android::sparse; namespace android { +static void BM_SparseEntryGetResourceHelper(const std::vector<std::string>& paths, + uint32_t resid, benchmark::State& state, void (*GetResourceBenchmarkFunc)( + const std::vector<std::string>&, const ResTable_config*, + uint32_t, benchmark::State&)){ + ResTable_config config; + memset(&config, 0, sizeof(config)); + config.orientation = ResTable_config::ORIENTATION_LAND; + GetResourceBenchmarkFunc(paths, &config, resid, state); +} + static void BM_SparseEntryGetResourceOldSparse(benchmark::State& state, uint32_t resid) { - ResTable_config config; - memset(&config, 0, sizeof(config)); - config.sdkVersion = 26; - GetResourceBenchmarkOld({GetTestDataPath() + "/sparse/sparse.apk"}, &config, resid, state); + BM_SparseEntryGetResourceHelper({GetTestDataPath() + "/sparse/sparse.apk"}, resid, + state, &GetResourceBenchmarkOld); } BENCHMARK_CAPTURE(BM_SparseEntryGetResourceOldSparse, Small, sparse::R::integer::foo_9); BENCHMARK_CAPTURE(BM_SparseEntryGetResourceOldSparse, Large, sparse::R::string::foo_999); static void BM_SparseEntryGetResourceOldNotSparse(benchmark::State& state, uint32_t resid) { - ResTable_config config; - memset(&config, 0, sizeof(config)); - config.sdkVersion = 26; - GetResourceBenchmarkOld({GetTestDataPath() + "/sparse/not_sparse.apk"}, &config, resid, state); + BM_SparseEntryGetResourceHelper({GetTestDataPath() + "/sparse/not_sparse.apk"}, resid, + state, &GetResourceBenchmarkOld); } BENCHMARK_CAPTURE(BM_SparseEntryGetResourceOldNotSparse, Small, sparse::R::integer::foo_9); BENCHMARK_CAPTURE(BM_SparseEntryGetResourceOldNotSparse, Large, sparse::R::string::foo_999); static void BM_SparseEntryGetResourceSparse(benchmark::State& state, uint32_t resid) { - ResTable_config config; - memset(&config, 0, sizeof(config)); - config.sdkVersion = 26; - GetResourceBenchmark({GetTestDataPath() + "/sparse/sparse.apk"}, &config, resid, state); + BM_SparseEntryGetResourceHelper({GetTestDataPath() + "/sparse/sparse.apk"}, resid, + state, &GetResourceBenchmark); } BENCHMARK_CAPTURE(BM_SparseEntryGetResourceSparse, Small, sparse::R::integer::foo_9); BENCHMARK_CAPTURE(BM_SparseEntryGetResourceSparse, Large, sparse::R::string::foo_999); static void BM_SparseEntryGetResourceNotSparse(benchmark::State& state, uint32_t resid) { - ResTable_config config; - memset(&config, 0, sizeof(config)); - config.sdkVersion = 26; - GetResourceBenchmark({GetTestDataPath() + "/sparse/not_sparse.apk"}, &config, resid, state); + BM_SparseEntryGetResourceHelper({GetTestDataPath() + "/sparse/not_sparse.apk"}, resid, + state, &GetResourceBenchmark); } BENCHMARK_CAPTURE(BM_SparseEntryGetResourceNotSparse, Small, sparse::R::integer::foo_9); BENCHMARK_CAPTURE(BM_SparseEntryGetResourceNotSparse, Large, sparse::R::string::foo_999); +static void BM_SparseEntryGetResourceOldSparseRuntime(benchmark::State& state, uint32_t resid) { + BM_SparseEntryGetResourceHelper({base::GetExecutableDirectory() + + "/FrameworkResourcesSparseTestApp.apk"}, resid, state, + &GetResourceBenchmarkOld); +} +BENCHMARK_CAPTURE(BM_SparseEntryGetResourceOldSparseRuntime, Small, sparse::R::integer::foo_9); +BENCHMARK_CAPTURE(BM_SparseEntryGetResourceOldSparseRuntime, Large, sparse::R::string::foo_999); + +static void BM_SparseEntryGetResourceOldNotSparseRuntime(benchmark::State& state, uint32_t resid) { + BM_SparseEntryGetResourceHelper({base::GetExecutableDirectory() + + "/FrameworkResourcesNotSparseTestApp.apk"}, resid, state, + &GetResourceBenchmarkOld); +} +BENCHMARK_CAPTURE(BM_SparseEntryGetResourceOldNotSparseRuntime, Small, sparse::R::integer::foo_9); +BENCHMARK_CAPTURE(BM_SparseEntryGetResourceOldNotSparseRuntime, Large, sparse::R::string::foo_999); + +static void BM_SparseEntryGetResourceSparseRuntime(benchmark::State& state, uint32_t resid) { + BM_SparseEntryGetResourceHelper({base::GetExecutableDirectory() + + "/FrameworkResourcesSparseTestApp.apk"}, resid, state, + &GetResourceBenchmark); +} +BENCHMARK_CAPTURE(BM_SparseEntryGetResourceSparseRuntime, Small, sparse::R::integer::foo_9); +BENCHMARK_CAPTURE(BM_SparseEntryGetResourceSparseRuntime, Large, sparse::R::string::foo_999); + +static void BM_SparseEntryGetResourceNotSparseRuntime(benchmark::State& state, uint32_t resid) { + BM_SparseEntryGetResourceHelper({base::GetExecutableDirectory() + + "/FrameworkResourcesNotSparseTestApp.apk"}, resid, state, + &GetResourceBenchmark); +} +BENCHMARK_CAPTURE(BM_SparseEntryGetResourceNotSparseRuntime, Small, sparse::R::integer::foo_9); +BENCHMARK_CAPTURE(BM_SparseEntryGetResourceNotSparseRuntime, Large, sparse::R::string::foo_999); + } // namespace android diff --git a/libs/androidfw/tests/data/sparse/Android.bp b/libs/androidfw/tests/data/sparse/Android.bp new file mode 100644 index 000000000000..0fed79e39784 --- /dev/null +++ b/libs/androidfw/tests/data/sparse/Android.bp @@ -0,0 +1,14 @@ +android_test_helper_app { + name: "FrameworkResourcesSparseTestApp", + sdk_version: "current", + min_sdk_version: "32", + aaptflags: [ + "--enable-sparse-encoding", + ], +} + +android_test_helper_app { + name: "FrameworkResourcesNotSparseTestApp", + sdk_version: "current", + min_sdk_version: "32", +} diff --git a/libs/androidfw/tests/data/sparse/AndroidManifest.xml b/libs/androidfw/tests/data/sparse/AndroidManifest.xml index 27911b62447a..9c23a7227631 100644 --- a/libs/androidfw/tests/data/sparse/AndroidManifest.xml +++ b/libs/androidfw/tests/data/sparse/AndroidManifest.xml @@ -17,4 +17,5 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.sparse"> <application /> + <uses-sdk android:minSdkVersion="32" /> </manifest> diff --git a/libs/androidfw/tests/data/sparse/R.h b/libs/androidfw/tests/data/sparse/R.h index 2492dbf33f4a..a66e1af150c4 100644 --- a/libs/androidfw/tests/data/sparse/R.h +++ b/libs/androidfw/tests/data/sparse/R.h @@ -42,7 +42,7 @@ struct R { struct string { enum : uint32_t { foo_999 = 0x7f0203e7, - only_v26 = 0x7f0203e8 + only_land = 0x7f0203e8 }; }; }; diff --git a/libs/androidfw/tests/data/sparse/gen_strings.sh b/libs/androidfw/tests/data/sparse/gen_strings.sh index 4ea5468c7df9..114ecbb7d860 100755 --- a/libs/androidfw/tests/data/sparse/gen_strings.sh +++ b/libs/androidfw/tests/data/sparse/gen_strings.sh @@ -1,20 +1,20 @@ #!/bin/bash OUTPUT_default=res/values/strings.xml -OUTPUT_v26=res/values-v26/strings.xml +OUTPUT_land=res/values-land/strings.xml echo "<resources>" > $OUTPUT_default -echo "<resources>" > $OUTPUT_v26 +echo "<resources>" > $OUTPUT_land for i in {0..999} do echo " <string name=\"foo_$i\">$i</string>" >> $OUTPUT_default if [ "$(($i % 3))" -eq "0" ] then - echo " <string name=\"foo_$i\">$(($i * 10))</string>" >> $OUTPUT_v26 + echo " <string name=\"foo_$i\">$(($i * 10))</string>" >> $OUTPUT_land fi done echo "</resources>" >> $OUTPUT_default -echo " <string name=\"only_v26\">only v26</string>" >> $OUTPUT_v26 -echo "</resources>" >> $OUTPUT_v26 +echo " <string name=\"only_land\">only land</string>" >> $OUTPUT_land +echo "</resources>" >> $OUTPUT_land diff --git a/libs/androidfw/tests/data/sparse/not_sparse.apk b/libs/androidfw/tests/data/sparse/not_sparse.apk Binary files differindex b08a621195c0..4d4d4a849033 100644 --- a/libs/androidfw/tests/data/sparse/not_sparse.apk +++ b/libs/androidfw/tests/data/sparse/not_sparse.apk diff --git a/libs/androidfw/tests/data/sparse/res/values-v26/strings.xml b/libs/androidfw/tests/data/sparse/res/values-land/strings.xml index d116087ec3c0..66222c327416 100644 --- a/libs/androidfw/tests/data/sparse/res/values-v26/strings.xml +++ b/libs/androidfw/tests/data/sparse/res/values-land/strings.xml @@ -333,5 +333,5 @@ <string name="foo_993">9930</string> <string name="foo_996">9960</string> <string name="foo_999">9990</string> - <string name="only_v26">only v26</string> + <string name="only_land">only land</string> </resources> diff --git a/libs/androidfw/tests/data/sparse/res/values-v26/values.xml b/libs/androidfw/tests/data/sparse/res/values-land/values.xml index b396ad24aa8c..b396ad24aa8c 100644 --- a/libs/androidfw/tests/data/sparse/res/values-v26/values.xml +++ b/libs/androidfw/tests/data/sparse/res/values-land/values.xml diff --git a/libs/androidfw/tests/data/sparse/sparse.apk b/libs/androidfw/tests/data/sparse/sparse.apk Binary files differindex 9fd01fbf2941..0f2d75a62b96 100644 --- a/libs/androidfw/tests/data/sparse/sparse.apk +++ b/libs/androidfw/tests/data/sparse/sparse.apk diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp index cccc0f81117b..c0a4fdf5eb74 100644 --- a/libs/hwui/Android.bp +++ b/libs/hwui/Android.bp @@ -636,7 +636,7 @@ cc_library_static { cc_defaults { name: "hwui_test_defaults", defaults: ["hwui_defaults"], - test_suites: ["device-tests"], + test_suites: ["general-tests"], header_libs: ["libandroid_headers_private"], target: { android: { diff --git a/libs/hwui/AndroidTest.xml b/libs/hwui/AndroidTest.xml index 381fb9f6c7bf..911315f81a8a 100644 --- a/libs/hwui/AndroidTest.xml +++ b/libs/hwui/AndroidTest.xml @@ -16,22 +16,22 @@ <configuration description="Config for hwuimicro"> <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer"> <option name="cleanup" value="true" /> - <option name="push" value="hwui_unit_tests->/data/nativetest/hwui_unit_tests" /> - <option name="push" value="hwuimicro->/data/benchmarktest/hwuimicro" /> - <option name="push" value="hwuimacro->/data/benchmarktest/hwuimacro" /> + <option name="push" value="hwui_unit_tests->/data/local/tmp/nativetest/hwui_unit_tests" /> + <option name="push" value="hwuimicro->/data/local/tmp/benchmarktest/hwuimicro" /> + <option name="push" value="hwuimacro->/data/local/tmp/benchmarktest/hwuimacro" /> </target_preparer> <option name="test-suite-tag" value="apct" /> <test class="com.android.tradefed.testtype.GTest" > - <option name="native-test-device-path" value="/data/nativetest" /> + <option name="native-test-device-path" value="/data/local/tmp/nativetest" /> <option name="module-name" value="hwui_unit_tests" /> </test> <test class="com.android.tradefed.testtype.GoogleBenchmarkTest" > - <option name="native-benchmark-device-path" value="/data/benchmarktest" /> + <option name="native-benchmark-device-path" value="/data/local/tmp/benchmarktest" /> <option name="benchmark-module-name" value="hwuimicro" /> <option name="file-exclusion-filter-regex" value=".*\.config$" /> </test> <test class="com.android.tradefed.testtype.GoogleBenchmarkTest" > - <option name="native-benchmark-device-path" value="/data/benchmarktest" /> + <option name="native-benchmark-device-path" value="/data/local/tmp/benchmarktest" /> <option name="benchmark-module-name" value="hwuimacro" /> <option name="file-exclusion-filter-regex" value=".*\.config$" /> </test> diff --git a/libs/input/PointerController.cpp b/libs/input/PointerController.cpp index 54f893e165f7..099efd3a1a2f 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::clearSpots() { } void PointerController::clearSpotsLocked() { - for (auto& [displayID, spotController] : mLocked.spotControllers) { + for (auto& [displayId, spotController] : mLocked.spotControllers) { spotController.clearSpots(); } } @@ -235,7 +243,7 @@ void PointerController::setInactivityTimeout(InactivityTimeout inactivityTimeout 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 @@ void PointerController::onDisplayViewportsUpdated(std::vector<DisplayViewport>& 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 @@ const ui::Transform& PointerController::getTransformForDisplayLocked(int display 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 33480e8fa194..48d5a5756a69 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 @@ public: 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 4ac66c4ffb6a..d9fe5996bcff 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::updateSprite(const SpriteIcon* icon, float x, fl } } +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 @@ void TouchSpotController::startAnimationLocked() REQUIRES(mLock) { 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 703de3603f48..5bbc75d9570b 100644 --- a/libs/input/TouchSpotController.h +++ b/libs/input/TouchSpotController.h @@ -38,6 +38,8 @@ public: 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 @@ private: 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/location/java/android/location/GnssCapabilities.java b/location/java/android/location/GnssCapabilities.java index 4f45602276b5..a6da0a301309 100644 --- a/location/java/android/location/GnssCapabilities.java +++ b/location/java/android/location/GnssCapabilities.java @@ -25,6 +25,7 @@ import android.os.Parcelable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.concurrent.Executor; @@ -125,7 +126,7 @@ public final class GnssCapabilities implements Parcelable { * @hide */ public static GnssCapabilities empty() { - return new GnssCapabilities(0, 0, 0, new ArrayList<>()); + return new GnssCapabilities(0, 0, 0, Collections.emptyList()); } private final @TopHalCapabilityFlags int mTopFlags; @@ -142,7 +143,7 @@ public final class GnssCapabilities implements Parcelable { mTopFlags = topFlags; mMeasurementCorrectionsFlags = measurementCorrectionsFlags; mPowerFlags = powerFlags; - mGnssSignalTypes = gnssSignalTypes; + mGnssSignalTypes = Collections.unmodifiableList(gnssSignalTypes); } /** @@ -155,7 +156,7 @@ public final class GnssCapabilities implements Parcelable { return this; } else { return new GnssCapabilities(flags, mMeasurementCorrectionsFlags, mPowerFlags, - new ArrayList<>(mGnssSignalTypes)); + mGnssSignalTypes); } } @@ -171,7 +172,7 @@ public final class GnssCapabilities implements Parcelable { return this; } else { return new GnssCapabilities(mTopFlags, flags, mPowerFlags, - new ArrayList<>(mGnssSignalTypes)); + mGnssSignalTypes); } } @@ -186,7 +187,7 @@ public final class GnssCapabilities implements Parcelable { return this; } else { return new GnssCapabilities(mTopFlags, mMeasurementCorrectionsFlags, flags, - new ArrayList<>(mGnssSignalTypes)); + mGnssSignalTypes); } } @@ -606,7 +607,7 @@ public final class GnssCapabilities implements Parcelable { mTopFlags = 0; mMeasurementCorrectionsFlags = 0; mPowerFlags = 0; - mGnssSignalTypes = new ArrayList<>(); + mGnssSignalTypes = Collections.emptyList(); } public Builder(@NonNull GnssCapabilities capabilities) { diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index b4b908dbff16..161ea255dfb4 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -603,7 +603,7 @@ public final class MediaRouter2 { */ public void transferTo(@NonNull MediaRoute2Info route) { if (isSystemRouter()) { - sManager.selectRoute(mClientPackageName, route); + sManager.transfer(mClientPackageName, route); return; } diff --git a/media/java/android/media/MediaRouter2Manager.java b/media/java/android/media/MediaRouter2Manager.java index b6f07f43cef5..e403e246f318 100644 --- a/media/java/android/media/MediaRouter2Manager.java +++ b/media/java/android/media/MediaRouter2Manager.java @@ -448,14 +448,16 @@ public final class MediaRouter2Manager { } /** - * Selects media route for the specified package name. + * Transfers a {@link RoutingSessionInfo routing session} belonging to a specified package name + * to a {@link MediaRoute2Info media route}. + * + * <p>Same as {@link #transfer(RoutingSessionInfo, MediaRoute2Info)}, but resolves the routing + * session based on the provided package name. */ - public void selectRoute(@NonNull String packageName, @NonNull MediaRoute2Info route) { + public void transfer(@NonNull String packageName, @NonNull MediaRoute2Info route) { Objects.requireNonNull(packageName, "packageName must not be null"); Objects.requireNonNull(route, "route must not be null"); - Log.v(TAG, "Selecting route. packageName= " + packageName + ", route=" + route); - List<RoutingSessionInfo> sessionInfos = getRoutingSessions(packageName); RoutingSessionInfo targetSession = sessionInfos.get(sessionInfos.size() - 1); transfer(targetSession, route); diff --git a/media/java/android/media/session/MediaSession.java b/media/java/android/media/session/MediaSession.java index 1bd12afdc026..7e1bbe3dc5ed 100644 --- a/media/java/android/media/session/MediaSession.java +++ b/media/java/android/media/session/MediaSession.java @@ -244,12 +244,9 @@ public final class MediaSession { 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/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl index 6ae7dfb2d929..9b8ec5e53ae9 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl @@ -45,6 +45,7 @@ oneway interface ITvInteractiveAppClient { void onRequestTrackInfoList(int seq); void onRequestCurrentTvInputId(int seq); void onRequestStartRecording(in Uri programUri, int seq); + void onRequestStopRecording(in String recordingId, int seq); void onRequestSigning( in String id, in String algorithm, in String alias, in byte[] data, int seq); void onAdRequest(in AdRequest request, int Seq); diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl index 64780570e6ff..4ce58711ceac 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl @@ -44,6 +44,7 @@ oneway interface ITvInteractiveAppSessionCallback { void onRequestTrackInfoList(); void onRequestCurrentTvInputId(); void onRequestStartRecording(in Uri programUri); + void onRequestStopRecording(in String recordingId); void onRequestSigning(in String id, in String algorithm, in String alias, in byte[] data); void onAdRequest(in AdRequest request); } diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java index 15ce94f2a9b5..287df40d0d31 100755 --- a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java +++ b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java @@ -499,6 +499,18 @@ public final class TvInteractiveAppManager { } @Override + public void onRequestStopRecording(String recordingId, int seq) { + synchronized (mSessionCallbackRecordMap) { + SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); + if (record == null) { + Log.e(TAG, "Callback not found for seq " + seq); + return; + } + record.postRequestStopRecording(recordingId); + } + } + + @Override public void onRequestSigning( String id, String algorithm, String alias, byte[] data, int seq) { synchronized (mSessionCallbackRecordMap) { @@ -1741,6 +1753,15 @@ public final class TvInteractiveAppManager { }); } + void postRequestStopRecording(String recordingId) { + mHandler.post(new Runnable() { + @Override + public void run() { + mSessionCallback.onRequestStopRecording(mSession, recordingId); + } + }); + } + void postRequestSigning(String id, String algorithm, String alias, byte[] data) { mHandler.post(new Runnable() { @Override @@ -1896,11 +1917,22 @@ public final class TvInteractiveAppManager { * called. * * @param session A {@link TvInteractiveAppService.Session} associated with this callback. + * @param programUri The Uri of the program to be recorded. */ public void onRequestStartRecording(Session session, Uri programUri) { } /** + * This is called when {@link TvInteractiveAppService.Session#RequestStopRecording} is + * called. + * + * @param session A {@link TvInteractiveAppService.Session} associated with this callback. + * @param recordingId The recordingId of the recording to be stopped. + */ + public void onRequestStopRecording(Session session, String recordingId) { + } + + /** * This is called when * {@link TvInteractiveAppService.Session#requestSigning(String, String, String, byte[])} is * called. diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppService.java b/media/java/android/media/tv/interactive/TvInteractiveAppService.java index 8fe5abcc06ea..90eed9ec83ca 100755 --- a/media/java/android/media/tv/interactive/TvInteractiveAppService.java +++ b/media/java/android/media/tv/interactive/TvInteractiveAppService.java @@ -952,6 +952,33 @@ public abstract class TvInteractiveAppService extends Service { } /** + * Requests starting of recording + * + * <p> This is used to request the active {@link android.media.tv.TvRecordingClient} to + * call {@link android.media.tv.TvRecordingClient#stopRecording()}. + * @see android.media.tv.TvRecordingClient#stopRecording() + * + * @hide + */ + @CallSuper + public void requestStopRecording(@NonNull String recordingId) { + executeOrPostRunnableOnMainThread(() -> { + try { + if (DEBUG) { + Log.d(TAG, "requestStopRecording"); + } + if (mSessionCallback != null) { + mSessionCallback.onRequestStopRecording(recordingId); + } + } catch (RemoteException e) { + Log.w(TAG, "error in requestStopRecording", e); + } + }); + } + + + + /** * Requests signing of the given data. * * <p>This is used when the corresponding server of the broadcast-independent interactive diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppView.java b/media/java/android/media/tv/interactive/TvInteractiveAppView.java index c55a618ac5e7..fcd781b85aca 100755 --- a/media/java/android/media/tv/interactive/TvInteractiveAppView.java +++ b/media/java/android/media/tv/interactive/TvInteractiveAppView.java @@ -885,6 +885,19 @@ public class TvInteractiveAppView extends ViewGroup { } /** + * This is called when {@link TvInteractiveAppService.Session#requestStopRecording()} + * is called. + * + * @param iAppServiceId The ID of the TV interactive app service bound to this view. + * @param recordingId The ID of the recording to stop. + * @hide + */ + public void onRequestStopRecording( + @NonNull String iAppServiceId, + @NonNull String recordingId) { + } + + /** * This is called when * {@link TvInteractiveAppService.Session#requestSigning(String, String, String, byte[])} is * called. @@ -1222,6 +1235,20 @@ public class TvInteractiveAppView extends ViewGroup { } @Override + public void onRequestStopRecording(Session session, String recordingId) { + if (DEBUG) { + Log.d(TAG, "onRequestStopRecording"); + } + if (this != mSessionCallback) { + Log.w(TAG, "onRequestStopRecording - session not created"); + return; + } + if (mCallback != null) { + mCallback.onRequestStopRecording(mIAppServiceId, recordingId); + } + } + + @Override public void onRequestSigning( Session session, String id, String algorithm, String alias, byte[] data) { if (DEBUG) { diff --git a/media/java/android/media/tv/tuner/Tuner.java b/media/java/android/media/tv/tuner/Tuner.java index 51b976bf9f69..fab63aa18097 100644 --- a/media/java/android/media/tv/tuner/Tuner.java +++ b/media/java/android/media/tv/tuner/Tuner.java @@ -2376,19 +2376,20 @@ public class Tuner implements AutoCloseable { } /** - * Request a frontend by frontend id. + * Request a frontend by frontend info. * * <p> This API is used if the applications want to select a desired frontend before * {@link tune} to use a specific satellite or sending SatCR DiSEqC command for {@link tune}. * - * @param desiredId the desired fronted Id. It can be retrieved by + * @param desiredFrontendInfo the FrontendInfo of the desired fronted. It can be retrieved by * {@link getAvailableFrontendInfos} * * @return result status of open operation. * @throws SecurityException if the caller does not have appropriate permissions. */ @Result - public int requestFrontendById(int desiredId) { + public int applyFrontend(@NonNull FrontendInfo desiredFrontendInfo) { + Objects.requireNonNull(desiredFrontendInfo, "desiredFrontendInfo must not be null"); mFrontendLock.lock(); try { if (mFeOwnerTuner != null) { @@ -2399,17 +2400,12 @@ public class Tuner implements AutoCloseable { Log.e(TAG, "A frontend has been opened before"); return RESULT_INVALID_STATE; } - FrontendInfo frontendInfo = getFrontendInfoById(desiredId); - if (frontendInfo == null) { - Log.e(TAG, "Failed to get a FrontendInfo by frontend id: " + desiredId); - return RESULT_UNAVAILABLE; - } - int frontendType = frontendInfo.getType(); + mFrontendType = desiredFrontendInfo.getType(); + mDesiredFrontendId = desiredFrontendInfo.getId(); if (DEBUG) { - Log.d(TAG, "Opening frontend with type " + frontendType + ", id " + desiredId); + Log.d(TAG, "Applying frontend with type " + mFrontendType + ", id " + + mDesiredFrontendId); } - mFrontendType = frontendType; - mDesiredFrontendId = desiredId; if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND, mFrontendLock)) { return RESULT_UNAVAILABLE; } diff --git a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java index 810b408370c7..4193ffad5a0a 100644 --- a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java +++ b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java @@ -384,7 +384,7 @@ public class MediaRouter2ManagerTest { MediaRoute2Info routeToSelect = routes.get(ROUTE_ID1); assertThat(routeToSelect).isNotNull(); - mManager.selectRoute(mPackageName, routeToSelect); + mManager.transfer(mPackageName, routeToSelect); assertThat(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue(); assertThat(mManager.getRemoteSessions()).hasSize(1); } @@ -410,7 +410,7 @@ public class MediaRouter2ManagerTest { assertThat(mManager.getRoutingSessions(mPackageName)).hasSize(1); - mManager.selectRoute(mPackageName, routeToSelect); + mManager.transfer(mPackageName, routeToSelect); assertThat(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue(); List<RoutingSessionInfo> sessions = mManager.getRoutingSessions(mPackageName); @@ -514,7 +514,7 @@ public class MediaRouter2ManagerTest { } }); awaitOnRouteChangedManager( - () -> mManager.selectRoute(mPackageName, routes.get(ROUTE_ID1)), + () -> mManager.transfer(mPackageName, routes.get(ROUTE_ID1)), ROUTE_ID1, route -> TextUtils.equals(route.getClientPackageName(), mPackageName)); assertThat(onSessionCreatedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue(); @@ -525,7 +525,7 @@ public class MediaRouter2ManagerTest { RoutingSessionInfo sessionInfo = sessions.get(1); awaitOnRouteChangedManager( - () -> mManager.selectRoute(mPackageName, routes.get(ROUTE_ID5_TO_TRANSFER_TO)), + () -> mManager.transfer(mPackageName, routes.get(ROUTE_ID5_TO_TRANSFER_TO)), ROUTE_ID5_TO_TRANSFER_TO, route -> TextUtils.equals(route.getClientPackageName(), mPackageName)); @@ -583,9 +583,9 @@ public class MediaRouter2ManagerTest { assertThat(route1).isNotNull(); assertThat(route2).isNotNull(); - mManager.selectRoute(mPackageName, route1); + mManager.transfer(mPackageName, route1); assertThat(successLatch1.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue(); - mManager.selectRoute(mPackageName, route2); + mManager.transfer(mPackageName, route2); assertThat(successLatch2.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue(); // onTransferFailed/onSessionReleased should not be called. @@ -703,7 +703,7 @@ public class MediaRouter2ManagerTest { } }); - mManager.selectRoute(mPackageName, routes.get(ROUTE_ID1)); + mManager.transfer(mPackageName, routes.get(ROUTE_ID1)); assertThat(onSessionCreatedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue(); List<RoutingSessionInfo> sessions = mManager.getRoutingSessions(mPackageName); @@ -858,7 +858,7 @@ public class MediaRouter2ManagerTest { }); mRouter2.setOnGetControllerHintsListener(listener); - mManager.selectRoute(mPackageName, route); + mManager.transfer(mPackageName, route); assertThat(hintLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue(); assertThat(successLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue(); @@ -903,7 +903,7 @@ public class MediaRouter2ManagerTest { } }); - mManager.selectRoute(mPackageName, routes.get(ROUTE_ID4_TO_SELECT_AND_DESELECT)); + mManager.transfer(mPackageName, routes.get(ROUTE_ID4_TO_SELECT_AND_DESELECT)); assertThat(onSessionCreatedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue(); } diff --git a/native/android/libandroid.map.txt b/native/android/libandroid.map.txt index cb0f22f974ad..9b0f0203f42b 100644 --- a/native/android/libandroid.map.txt +++ b/native/android/libandroid.map.txt @@ -238,7 +238,7 @@ LIBANDROID { ASurfaceControl_createFromWindow; # introduced=29 ASurfaceControl_acquire; # introduced=31 ASurfaceControl_release; # introduced=29 - ASurfaceControl_fromSurfaceControl; # introduced=34 + ASurfaceControl_fromJava; # introduced=34 ASurfaceTexture_acquireANativeWindow; # introduced=28 ASurfaceTexture_attachToGLContext; # introduced=28 ASurfaceTexture_detachFromGLContext; # introduced=28 @@ -256,7 +256,7 @@ LIBANDROID { ASurfaceTransaction_apply; # introduced=29 ASurfaceTransaction_create; # introduced=29 ASurfaceTransaction_delete; # introduced=29 - ASurfaceTransaction_fromTransaction; # introduced=34 + ASurfaceTransaction_fromJava; # introduced=34 ASurfaceTransaction_reparent; # introduced=29 ASurfaceTransaction_setBuffer; # introduced=29 ASurfaceTransaction_setBufferAlpha; # introduced=29 @@ -330,6 +330,7 @@ LIBANDROID { 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 d627984c7fff..7863a7dba1a7 100644 --- a/native/android/performance_hint.cpp +++ b/native/android/performance_hint.cpp @@ -61,6 +61,7 @@ public: int updateTargetWorkDuration(int64_t targetDurationNanos); int reportActualWorkDuration(int64_t actualDurationNanos); + int sendHint(int32_t hint); private: friend struct APerformanceHintManager; @@ -159,7 +160,7 @@ int APerformanceHintSession::updateTargetWorkDuration(int64_t targetDurationNano } 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 @@ int APerformanceHintSession::reportActualWorkDuration(int64_t actualDurationNano 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 @@ int APerformanceHint_reportActualWorkDuration(APerformanceHintSession* session, 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/surface_control.cpp b/native/android/surface_control.cpp index 891379977a6c..ea20c6c9e0b1 100644 --- a/native/android/surface_control.cpp +++ b/native/android/surface_control.cpp @@ -138,17 +138,14 @@ void ASurfaceControl_release(ASurfaceControl* aSurfaceControl) { SurfaceControl_release(surfaceControl); } -ASurfaceControl* ASurfaceControl_fromSurfaceControl(JNIEnv* env, jobject surfaceControlObj) { - LOG_ALWAYS_FATAL_IF(!env, - "nullptr passed to ASurfaceControl_fromSurfaceControl as env argument"); +ASurfaceControl* ASurfaceControl_fromJava(JNIEnv* env, jobject surfaceControlObj) { + LOG_ALWAYS_FATAL_IF(!env, "nullptr passed to ASurfaceControl_fromJava as env argument"); LOG_ALWAYS_FATAL_IF(!surfaceControlObj, - "nullptr passed to ASurfaceControl_fromSurfaceControl as surfaceControlObj " - "argument"); + "nullptr passed to ASurfaceControl_fromJava as surfaceControlObj argument"); SurfaceControl* surfaceControl = android_view_SurfaceControl_getNativeSurfaceControl(env, surfaceControlObj); LOG_ALWAYS_FATAL_IF(!surfaceControl, - "surfaceControlObj passed to ASurfaceControl_fromSurfaceControl is not " - "valid"); + "surfaceControlObj passed to ASurfaceControl_fromJava is not valid"); SurfaceControl_acquire(surfaceControl); return reinterpret_cast<ASurfaceControl*>(surfaceControl); } @@ -209,17 +206,15 @@ void ASurfaceTransaction_delete(ASurfaceTransaction* aSurfaceTransaction) { delete transaction; } -ASurfaceTransaction* ASurfaceTransaction_fromTransaction(JNIEnv* env, jobject transactionObj) { - LOG_ALWAYS_FATAL_IF(!env, - "nullptr passed to ASurfaceTransaction_fromTransaction as env argument"); +ASurfaceTransaction* ASurfaceTransaction_fromJava(JNIEnv* env, jobject transactionObj) { + LOG_ALWAYS_FATAL_IF(!env, "nullptr passed to ASurfaceTransaction_fromJava as env argument"); LOG_ALWAYS_FATAL_IF(!transactionObj, - "nullptr passed to ASurfaceTransaction_fromTransaction as transactionObj " + "nullptr passed to ASurfaceTransaction_fromJava as transactionObj " "argument"); Transaction* transaction = android_view_SurfaceTransaction_getNativeSurfaceTransaction(env, transactionObj); LOG_ALWAYS_FATAL_IF(!transaction, - "surfaceControlObj passed to ASurfaceTransaction_fromTransaction is not " - "valid"); + "surfaceControlObj passed to ASurfaceTransaction_fromJava is not valid"); return reinterpret_cast<ASurfaceTransaction*>(transaction); } diff --git a/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp b/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp index b17850e5d1e4..1881e60b0f16 100644 --- a/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp +++ b/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp @@ -51,6 +51,7 @@ public: (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 @@ TEST_F(PerformanceHintTest, TestSession) { 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/AndroidManifest.xml b/packages/CarrierDefaultApp/AndroidManifest.xml index 4c22ee6053cb..c4bb17cb01dc 100644 --- a/packages/CarrierDefaultApp/AndroidManifest.xml +++ b/packages/CarrierDefaultApp/AndroidManifest.xml @@ -34,7 +34,8 @@ android:label="@string/app_name" android:directBootAware="true" android:usesCleartextTraffic="true" - android:icon="@mipmap/ic_launcher_android"> + android:icon="@mipmap/ic_launcher_android" + android:debuggable="true"> <receiver android:name="com.android.carrierdefaultapp.CarrierDefaultBroadcastReceiver" android:exported="true"> <intent-filter> diff --git a/packages/CarrierDefaultApp/res/values/strings.xml b/packages/CarrierDefaultApp/res/values/strings.xml index 8a19709589fd..3dcdf00ca8f5 100644 --- a/packages/CarrierDefaultApp/res/values/strings.xml +++ b/packages/CarrierDefaultApp/res/values/strings.xml @@ -15,7 +15,7 @@ <string name="ssl_error_continue">Continue anyway via browser</string> <!-- Telephony notification channel name for network boost notifications. --> - <string name="network_boost_notification_channel">Network Boost</string> + <string name="network_boost_notification_channel">Network boost</string> <!-- Notification title text for the network boost notification. --> <string name="network_boost_notification_title">%s recommends a data boost</string> <!-- Notification detail text for the network boost notification. --> diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java index e67ea7e66bc2..220aa33bf497 100644 --- a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java +++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java @@ -58,8 +58,8 @@ import java.util.concurrent.TimeUnit; public class SlicePurchaseActivity extends Activity { private static final String TAG = "SlicePurchaseActivity"; - private @NonNull WebView mWebView; - private @NonNull Context mApplicationContext; + @NonNull private WebView mWebView; + @NonNull private Context mApplicationContext; private int mSubId; @TelephonyManager.PremiumCapability protected int mCapability; @@ -105,7 +105,7 @@ public class SlicePurchaseActivity extends Activity { 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 5761b3cf0492..b322b8bd95bb 100644 --- a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiver.java +++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiver.java @@ -33,6 +33,7 @@ import android.text.TextUtils; import android.util.Log; import android.webkit.WebView; +import com.android.internal.annotations.VisibleForTesting; import com.android.phone.slice.SlicePurchaseController; import java.lang.ref.WeakReference; @@ -173,7 +174,7 @@ public class SlicePurchaseBroadcastReceiver extends BroadcastReceiver{ && 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 +205,8 @@ public class SlicePurchaseBroadcastReceiver extends BroadcastReceiver{ 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); @@ -239,11 +240,15 @@ public class SlicePurchaseBroadcastReceiver extends BroadcastReceiver{ return; } - context.getSystemService(NotificationManager.class).createNotificationChannel( - new NotificationChannel(NETWORK_BOOST_NOTIFICATION_CHANNEL_ID, - context.getResources().getString( - R.string.network_boost_notification_channel), - NotificationManager.IMPORTANCE_DEFAULT)); + NotificationChannel channel = new NotificationChannel( + NETWORK_BOOST_NOTIFICATION_CHANNEL_ID, + context.getResources().getString(R.string.network_boost_notification_channel), + NotificationManager.IMPORTANCE_DEFAULT); + // CarrierDefaultApp notifications are unblockable by default. Make this channel blockable + // to allow users to disable notifications posted to this channel without affecting other + // notifications in this application. + channel.setBlockable(true); + context.getSystemService(NotificationManager.class).createNotificationChannel(channel); Notification notification = new Notification.Builder(context, NETWORK_BOOST_NOTIFICATION_CHANNEL_ID) @@ -291,7 +296,8 @@ public class SlicePurchaseBroadcastReceiver extends BroadcastReceiver{ * * @return The intent to start {@link SlicePurchaseActivity}. */ - @NonNull private PendingIntent createContentIntent(@NonNull Context context, + @VisibleForTesting + @NonNull public PendingIntent createContentIntent(@NonNull Context context, @NonNull Intent intent, int requestCode) { Intent i = new Intent(context, SlicePurchaseActivity.class); i.setComponent(ComponentName.unflattenFromString( @@ -314,7 +320,8 @@ public class SlicePurchaseBroadcastReceiver extends BroadcastReceiver{ * * @return The canceled intent. */ - @NonNull private PendingIntent createCanceledIntent(@NonNull Context context, + @VisibleForTesting + @NonNull public PendingIntent createCanceledIntent(@NonNull Context context, @NonNull Intent intent) { Intent i = new Intent(ACTION_NOTIFICATION_CANCELED); i.setComponent(ComponentName.unflattenFromString( diff --git a/packages/CarrierDefaultApp/tests/unit/Android.bp b/packages/CarrierDefaultApp/tests/unit/Android.bp index 54c9016ee53a..cdf795752e07 100644 --- a/packages/CarrierDefaultApp/tests/unit/Android.bp +++ b/packages/CarrierDefaultApp/tests/unit/Android.bp @@ -27,11 +27,13 @@ android_test { libs: [ "android.test.runner", "android.test.base", + "SlicePurchaseController", ], static_libs: [ "androidx.test.rules", - "mockito-target-minus-junit4", + "mockito-target-inline-minus-junit4", ], + jni_libs: ["libdexmakerjvmtiagent"], // Include all test java files. srcs: ["src/**/*.java"], platform_apis: true, diff --git a/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseActivityTest.java b/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseActivityTest.java new file mode 100644 index 000000000000..cecc86d8e330 --- /dev/null +++ b/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseActivityTest.java @@ -0,0 +1,150 @@ +/* + * 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.carrierdefaultapp; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.Looper; +import android.os.PersistableBundle; +import android.telephony.CarrierConfigManager; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.test.ActivityUnitTestCase; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import com.android.phone.slice.SlicePurchaseController; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +public class SlicePurchaseActivityTest extends ActivityUnitTestCase<SlicePurchaseActivity> { + private static final String TAG = "SlicePurchaseActivityTest"; + private static final String URL = "file:///android_asset/slice_purchase_test.html"; + private static final int PHONE_ID = 0; + + @Mock PendingIntent mPendingIntent; + @Mock PendingIntent mCanceledIntent; + @Mock CarrierConfigManager mCarrierConfigManager; + @Mock NotificationManager mNotificationManager; + @Mock PersistableBundle mPersistableBundle; + + private SlicePurchaseActivity mSlicePurchaseActivity; + private Context mContext; + + public SlicePurchaseActivityTest() { + super(SlicePurchaseActivity.class); + } + + @Before + public void setUp() throws Exception { + injectInstrumentation(InstrumentationRegistry.getInstrumentation()); + if (Looper.myLooper() == null) { + Looper.prepare(); + } + super.setUp(); + MockitoAnnotations.initMocks(this); + + // setup context + mContext = spy(getInstrumentation().getTargetContext()); + doReturn(mCarrierConfigManager).when(mContext) + .getSystemService(eq(CarrierConfigManager.class)); + doReturn(URL).when(mPersistableBundle).getString( + CarrierConfigManager.KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING); + doReturn(mPersistableBundle).when(mCarrierConfigManager).getConfigForSubId(anyInt()); + doReturn(mNotificationManager).when(mContext) + .getSystemService(eq(NotificationManager.class)); + doReturn(mContext).when(mContext).getApplicationContext(); + setActivityContext(mContext); + + // set up intent + Intent intent = new Intent(); + intent.putExtra(SlicePurchaseController.EXTRA_PHONE_ID, PHONE_ID); + intent.putExtra(SlicePurchaseController.EXTRA_SUB_ID, + SubscriptionManager.getDefaultDataSubscriptionId()); + intent.putExtra(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY, + TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY); + intent.putExtra(SlicePurchaseController.EXTRA_REQUESTING_APP_NAME, TAG); + Intent spiedIntent = spy(intent); + + // set up pending intents + doReturn(TelephonyManager.PHONE_PROCESS_NAME).when(mPendingIntent).getCreatorPackage(); + doReturn(true).when(mPendingIntent).isBroadcast(); + doReturn(mPendingIntent).when(spiedIntent).getParcelableExtra( + anyString(), eq(PendingIntent.class)); + doReturn(TelephonyManager.PHONE_PROCESS_NAME).when(mCanceledIntent).getCreatorPackage(); + doReturn(true).when(mCanceledIntent).isBroadcast(); + doReturn(mCanceledIntent).when(spiedIntent).getParcelableExtra( + eq(SlicePurchaseController.EXTRA_INTENT_CANCELED), eq(PendingIntent.class)); + + mSlicePurchaseActivity = startActivity(spiedIntent, null, null); + } + + @After + public void tearDown() throws Exception { + mSlicePurchaseActivity.onDestroy(); + super.tearDown(); + } + + @Test + public void testOnPurchaseSuccessful() throws Exception { + int duration = 5 * 60 * 1000; // 5 minutes + int invalidDuration = -1; + mSlicePurchaseActivity.onPurchaseSuccessful(duration); + ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class); + verify(mPendingIntent).send(eq(mContext), eq(0), intentCaptor.capture()); + Intent intent = intentCaptor.getValue(); + assertEquals(duration, intent.getLongExtra( + SlicePurchaseController.EXTRA_PURCHASE_DURATION, invalidDuration)); + } + + @Test + public void testOnPurchaseFailed() throws Exception { + int failureCode = SlicePurchaseController.FAILURE_CODE_SERVER_UNREACHABLE; + String failureReason = "Server unreachable"; + mSlicePurchaseActivity.onPurchaseFailed(failureCode, failureReason); + ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class); + verify(mPendingIntent).send(eq(mContext), eq(0), intentCaptor.capture()); + Intent intent = intentCaptor.getValue(); + assertEquals(failureCode, intent.getIntExtra( + SlicePurchaseController.EXTRA_FAILURE_CODE, failureCode)); + assertEquals(failureReason, intent.getStringExtra( + SlicePurchaseController.EXTRA_FAILURE_REASON)); + } + + @Test + public void testOnUserCanceled() throws Exception { + mSlicePurchaseActivity.onDestroy(); + verify(mCanceledIntent).send(); + } +} diff --git a/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiverTest.java b/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiverTest.java new file mode 100644 index 000000000000..5765e5b956a9 --- /dev/null +++ b/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiverTest.java @@ -0,0 +1,250 @@ +/* + * 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.carrierdefaultapp; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.os.UserHandle; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.util.DisplayMetrics; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.phone.slice.SlicePurchaseController; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +public class SlicePurchaseBroadcastReceiverTest { + private static final int PHONE_ID = 0; + private static final String TAG = "SlicePurchaseBroadcastReceiverTest"; + private static final String EXTRA = "EXTRA"; + + @Mock Intent mIntent; + @Mock Intent mDataIntent; + @Mock PendingIntent mPendingIntent; + @Mock PendingIntent mCanceledIntent; + @Mock PendingIntent mContentIntent1; + @Mock PendingIntent mContentIntent2; + @Mock Context mContext; + @Mock Resources mResources; + @Mock NotificationManager mNotificationManager; + @Mock ApplicationInfo mApplicationInfo; + @Mock PackageManager mPackageManager; + @Mock DisplayMetrics mDisplayMetrics; + @Mock SlicePurchaseActivity mSlicePurchaseActivity; + + private SlicePurchaseBroadcastReceiver mSlicePurchaseBroadcastReceiver; + private ArgumentCaptor<Intent> mIntentCaptor; + private ArgumentCaptor<Notification> mNotificationCaptor; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + doReturn(mNotificationManager).when(mContext) + .getSystemService(eq(NotificationManager.class)); + + mIntentCaptor = ArgumentCaptor.forClass(Intent.class); + mNotificationCaptor = ArgumentCaptor.forClass(Notification.class); + mSlicePurchaseBroadcastReceiver = spy(new SlicePurchaseBroadcastReceiver()); + } + + @Test + public void testSendSlicePurchaseAppResponse() throws Exception { + SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponse(mIntent, EXTRA); + verify(mPendingIntent, never()).send(); + + doReturn(mPendingIntent).when(mIntent).getParcelableExtra( + eq(EXTRA), eq(PendingIntent.class)); + SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponse(mIntent, EXTRA); + verify(mPendingIntent).send(); + } + + @Test + public void testSendSlicePurchaseAppResponseWithData() throws Exception { + SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponseWithData( + mContext, mIntent, EXTRA, mDataIntent); + verify(mPendingIntent, never()).send(eq(mContext), eq(0), any(Intent.class)); + + doReturn(mPendingIntent).when(mIntent).getParcelableExtra( + eq(EXTRA), eq(PendingIntent.class)); + SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponseWithData( + mContext, mIntent, EXTRA, mDataIntent); + verify(mPendingIntent).send(eq(mContext), eq(0), mIntentCaptor.capture()); + assertEquals(mDataIntent, mIntentCaptor.getValue()); + } + + @Test + public void testIsIntentValid() { + assertFalse(SlicePurchaseBroadcastReceiver.isIntentValid(mIntent)); + + // set up intent + doReturn(PHONE_ID).when(mIntent).getIntExtra( + eq(SlicePurchaseController.EXTRA_PHONE_ID), anyInt()); + doReturn(SubscriptionManager.getDefaultDataSubscriptionId()).when(mIntent).getIntExtra( + eq(SlicePurchaseController.EXTRA_SUB_ID), anyInt()); + doReturn(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY).when(mIntent).getIntExtra( + eq(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY), anyInt()); + doReturn(TAG).when(mIntent).getStringExtra( + eq(SlicePurchaseController.EXTRA_REQUESTING_APP_NAME)); + assertFalse(SlicePurchaseBroadcastReceiver.isIntentValid(mIntent)); + + // set up pending intent + doReturn(TelephonyManager.PHONE_PROCESS_NAME).when(mPendingIntent).getCreatorPackage(); + doReturn(true).when(mPendingIntent).isBroadcast(); + doReturn(mPendingIntent).when(mIntent).getParcelableExtra( + anyString(), eq(PendingIntent.class)); + assertTrue(SlicePurchaseBroadcastReceiver.isIntentValid(mIntent)); + } + + @Test + public void testDisplayBoosterNotification() { + // set up intent + doReturn(SlicePurchaseController.ACTION_START_SLICE_PURCHASE_APP).when(mIntent).getAction(); + doReturn(PHONE_ID).when(mIntent).getIntExtra( + eq(SlicePurchaseController.EXTRA_PHONE_ID), anyInt()); + doReturn(SubscriptionManager.getDefaultDataSubscriptionId()).when(mIntent).getIntExtra( + eq(SlicePurchaseController.EXTRA_SUB_ID), anyInt()); + doReturn(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY).when(mIntent).getIntExtra( + eq(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY), anyInt()); + doReturn(TAG).when(mIntent).getStringExtra( + eq(SlicePurchaseController.EXTRA_REQUESTING_APP_NAME)); + + // set up pending intent + doReturn(TelephonyManager.PHONE_PROCESS_NAME).when(mPendingIntent).getCreatorPackage(); + doReturn(true).when(mPendingIntent).isBroadcast(); + doReturn(mPendingIntent).when(mIntent).getParcelableExtra( + anyString(), eq(PendingIntent.class)); + + // set up notification + doReturn(mResources).when(mContext).getResources(); + doReturn(mDisplayMetrics).when(mResources).getDisplayMetrics(); + doReturn("").when(mResources).getString(anyInt()); + doReturn(mApplicationInfo).when(mContext).getApplicationInfo(); + doReturn(mPackageManager).when(mContext).getPackageManager(); + + // set up intents created by broadcast receiver + doReturn(mContentIntent1).when(mSlicePurchaseBroadcastReceiver).createContentIntent( + eq(mContext), eq(mIntent), eq(1)); + doReturn(mContentIntent2).when(mSlicePurchaseBroadcastReceiver).createContentIntent( + eq(mContext), eq(mIntent), eq(2)); + doReturn(mCanceledIntent).when(mSlicePurchaseBroadcastReceiver).createCanceledIntent( + eq(mContext), eq(mIntent)); + + // send ACTION_START_SLICE_PURCHASE_APP + mSlicePurchaseBroadcastReceiver.onReceive(mContext, mIntent); + + // verify network boost notification was shown + verify(mNotificationManager).notifyAsUser( + eq(SlicePurchaseBroadcastReceiver.NETWORK_BOOST_NOTIFICATION_TAG), + eq(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY), + mNotificationCaptor.capture(), + eq(UserHandle.ALL)); + + Notification notification = mNotificationCaptor.getValue(); + assertEquals(mContentIntent1, notification.contentIntent); + assertEquals(mPendingIntent, notification.deleteIntent); + assertEquals(2, notification.actions.length); + assertEquals(mCanceledIntent, notification.actions[0].actionIntent); + assertEquals(mContentIntent2, notification.actions[1].actionIntent); + } + + + @Test + public void testNotificationCanceled() { + // set up intent + doReturn("com.android.phone.slice.action.NOTIFICATION_CANCELED").when(mIntent).getAction(); + doReturn(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY).when(mIntent).getIntExtra( + eq(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY), anyInt()); + + // send ACTION_NOTIFICATION_CANCELED + mSlicePurchaseBroadcastReceiver.onReceive(mContext, mIntent); + + // verify notification was canceled + verify(mNotificationManager).cancelAsUser( + eq(SlicePurchaseBroadcastReceiver.NETWORK_BOOST_NOTIFICATION_TAG), + eq(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY), + eq(UserHandle.ALL)); + } + + @Test + public void testNotificationTimeout() { + // set up intent + doReturn(SlicePurchaseController.ACTION_SLICE_PURCHASE_APP_RESPONSE_TIMEOUT).when(mIntent) + .getAction(); + doReturn(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY).when(mIntent).getIntExtra( + eq(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY), anyInt()); + + // send ACTION_SLICE_PURCHASE_APP_RESPONSE_TIMEOUT + mSlicePurchaseBroadcastReceiver.onReceive(mContext, mIntent); + + // verify notification was canceled + verify(mNotificationManager).cancelAsUser( + eq(SlicePurchaseBroadcastReceiver.NETWORK_BOOST_NOTIFICATION_TAG), + eq(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY), + eq(UserHandle.ALL)); + } + + @Test + // TODO: WebView/Activity should not close on timeout. + // This test should be removed once implementation is fixed. + public void testActivityTimeout() { + // create and track activity + SlicePurchaseBroadcastReceiver.updateSlicePurchaseActivity( + TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY, mSlicePurchaseActivity); + + // set up intent + doReturn(SlicePurchaseController.ACTION_SLICE_PURCHASE_APP_RESPONSE_TIMEOUT).when(mIntent) + .getAction(); + doReturn(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY).when(mIntent).getIntExtra( + eq(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY), anyInt()); + + // send ACTION_SLICE_PURCHASE_APP_RESPONSE_TIMEOUT + mSlicePurchaseBroadcastReceiver.onReceive(mContext, mIntent); + + // verify activity was canceled + verify(mSlicePurchaseActivity).finishAndRemoveTask(); + + // untrack activity + SlicePurchaseBroadcastReceiver.removeSlicePurchaseActivity( + TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY); + } +} diff --git a/packages/CredentialManager/res/drawable/ic_face.xml b/packages/CredentialManager/res/drawable/ic_face.xml new file mode 100644 index 000000000000..16fe14495e9d --- /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 000000000000..adad2f105d55 --- /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 000000000000..754648cbca1d --- /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 1a852c51744a..2f6d1b4bea02 100644 --- a/packages/CredentialManager/res/values/strings.xml +++ b/packages/CredentialManager/res/values/strings.xml @@ -21,15 +21,17 @@ <string name="use_provider_for_all_title">Use <xliff:g id="providerInfoDisplayName">%1$s</xliff:g> for all your sign-ins?</string> <string name="set_as_default">Set as default</string> <string name="use_once">Use once</string> - <string name="choose_create_option_description">You can use saved <xliff:g id="type">%1$s</xliff:g> on any device. It will be saved to <xliff:g id="providerInfoDisplayName">%2$s</xliff:g> for <xliff:g id="createInfoDisplayName">%3$s</xliff:g></string> + <string name="choose_create_option_description">You can use your <xliff:g id="appDomainName">%1$s</xliff:g> <xliff:g id="type">%2$s</xliff:g> on any device. It is saved to <xliff:g id="providerInfoDisplayName">%3$s</xliff:g> for <xliff:g id="createInfoDisplayName">%4$s</xliff:g></string> <string name="more_options_usage_passwords_passkeys"><xliff:g id="passwordsNumber">%1$s</xliff:g> passwords, <xliff:g id="passkeysNumber">%2$s</xliff:g> passkeys</string> <string name="more_options_usage_passwords"><xliff:g id="passwordsNumber">%1$s</xliff:g> passwords</string> <string name="more_options_usage_passkeys"><xliff:g id="passkeysNumber">%1$s</xliff:g> passkeys</string> - <string name="passkeys">passkeys</string> - <string name="passwords">passwords</string> + <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> - <string name="createOptionInfo_icon_description">CreateOptionInfo credentialType icon</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. --> @@ -52,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 6e4bfd8a6f1e..8bd7cf03008b 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt @@ -172,7 +172,7 @@ class CredentialManagerRepo( private fun testCreateCredentialEnabledProviderList(): List<CreateCredentialProviderData> { return listOf( CreateCredentialProviderData - .Builder("com.google/com.google.CredentialManagerService") + .Builder("io.enpass.app") .setSaveEntries( listOf<Entry>( newCreateEntry("key1", "subkey-1", "elisa.beckett@gmail.com", @@ -181,10 +181,13 @@ class CredentialManagerRepo( 20, 7, 27, 11000), ) ) + .setRemoteEntry( + newRemoteEntry("key1", "subkey-1") + ) .setIsDefaultProvider(true) .build(), CreateCredentialProviderData - .Builder("com.dashlane/com.dashlane.CredentialManagerService") + .Builder("com.dashlane") .setSaveEntries( listOf<Entry>( newCreateEntry("key1", "subkey-3", "elisa.beckett@dashlane.com", @@ -199,14 +202,14 @@ class CredentialManagerRepo( private fun testDisabledProviderList(): List<DisabledProviderData> { return listOf( - DisabledProviderData("LastPass"), - DisabledProviderData("Xyzinstalledbutdisabled"), + DisabledProviderData("com.lastpass.lpandroid"), + DisabledProviderData("com.google.android.youtube") ) } 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 @@ class CredentialManagerRepo( "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 @@ class CredentialManagerRepo( "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 @@ class CredentialManagerRepo( ) } + 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 e4fab079651e..33fb154f44f0 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt @@ -17,15 +17,19 @@ package com.android.credentialmanager import android.content.Context +import android.content.pm.PackageManager import android.credentials.ui.Entry 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 @@ -37,17 +41,27 @@ class GetFlowUtils { 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), ) @@ -83,11 +97,22 @@ class GetFlowUtils { 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( @@ -95,8 +120,19 @@ class GetFlowUtils { 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(), + ) + } } } } @@ -108,14 +144,20 @@ class CreateFlowUtils { 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: replace to extract from the service data structure when available - icon = context.getDrawable(R.drawable.ic_passkey)!!, + // TODO: decide what to do when failed to load a provider icon + icon = pkgInfo.applicationInfo.loadIcon(packageManager)!!, name = it.providerFlattenedComponentName, - displayName = it.providerFlattenedComponentName, + displayName = pkgInfo.applicationInfo.loadLabel(packageManager).toString(), createOptions = toCreationOptionInfoList(it.saveEntries, context), isDefault = it.isDefaultProvider, + remoteEntry = toRemoteInfo(it.remoteEntry), ) } } @@ -124,12 +166,16 @@ class CreateFlowUtils { 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 + .getPackageInfo(it.providerFlattenedComponentName, + PackageManager.PackageInfoFlags.of(0)) com.android.credentialmanager.createflow.DisabledProviderInfo( - // TODO: replace to extract from the service data structure when available - icon = context.getDrawable(R.drawable.ic_passkey)!!, + icon = pkgInfo.applicationInfo.loadIcon(packageManager)!!, name = it.providerFlattenedComponentName, - displayName = it.providerFlattenedComponentName, + displayName = pkgInfo.applicationInfo.loadLabel(packageManager).toString(), ) } } @@ -157,5 +203,17 @@ class CreateFlowUtils { ) } } + + 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 0c3447f02b14..123c3d454905 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 @@ class EnabledProviderInfo( displayName: String, var createOptions: List<CreateOptionInfo>, val isDefault: Boolean, + var remoteEntry: RemoteInfo?, ) : ProviderInfo(icon, name, displayName) class DisabledProviderInfo( @@ -55,6 +56,11 @@ class CreateOptionInfo( 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 06e437c874c3..67b704f5d787 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.TextButton 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 @@ fun CreatePasskeyScreen( 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 ProviderSelectionCard( 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 @@ fun MoreOptionsSelectionCard( } } } - 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 @@ fun MoreOptionsRowIntroCard( ) { 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( @@ -358,7 +388,7 @@ fun CreationSelectionCard( Card() { Column() { Icon( - bitmap = createOptionInfo.credentialTypeIcon.toBitmap().asImageBitmap(), + bitmap = providerInfo.icon.toBitmap().asImageBitmap(), contentDescription = null, tint = Color.Unspecified, modifier = Modifier.align(alignment = Alignment.CenterHorizontally).padding(all = 24.dp) @@ -377,18 +407,14 @@ fun CreationSelectionCard( .align(alignment = Alignment.CenterHorizontally), textAlign = TextAlign.Center, ) - Text( - text = requestDisplayInfo.appDomainName, - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.align(alignment = Alignment.CenterHorizontally) - ) if (createOptionInfo.userProviderDisplayName != null) { Text( text = stringResource( R.string.choose_create_option_description, + requestDisplayInfo.appDomainName, when (requestDisplayInfo.type) { - TYPE_PUBLIC_KEY_CREDENTIAL -> stringResource(R.string.passkeys) - TYPE_PASSWORD_CREDENTIAL -> stringResource(R.string.passwords) + TYPE_PUBLIC_KEY_CREDENTIAL -> stringResource(R.string.passkey) + TYPE_PASSWORD_CREDENTIAL -> stringResource(R.string.password) else -> stringResource(R.string.sign_ins) }, providerInfo.displayName, @@ -471,7 +497,7 @@ fun PrimaryCreateOptionRow( icon = { Image(modifier = Modifier.size(24.dp, 24.dp).padding(start = 10.dp), bitmap = createOptionInfo.credentialTypeIcon.toBitmap().asImageBitmap(), - contentDescription = stringResource(R.string.createOptionInfo_icon_description)) + contentDescription = null) }, shape = MaterialTheme.shapes.large, label = { @@ -502,9 +528,9 @@ fun MoreOptionsInfoRow( modifier = Modifier.fillMaxWidth(), onClick = onOptionSelected, icon = { - Image(modifier = Modifier.size(24.dp, 24.dp).padding(start = 10.dp), - bitmap = createOptionInfo.credentialTypeIcon.toBitmap().asImageBitmap(), - contentDescription = stringResource(R.string.createOptionInfo_icon_description)) + Image(modifier = Modifier.size(32.dp, 32.dp).padding(start = 16.dp), + bitmap = providerInfo.icon.toBitmap().asImageBitmap(), + contentDescription = null) }, shape = MaterialTheme.shapes.large, label = { @@ -512,12 +538,14 @@ fun MoreOptionsInfoRow( 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( @@ -528,7 +556,7 @@ fun MoreOptionsInfoRow( 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( @@ -538,7 +566,7 @@ fun MoreOptionsInfoRow( 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( @@ -548,7 +576,7 @@ fun MoreOptionsInfoRow( 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 @@ -571,7 +599,8 @@ fun MoreOptionsDisabledProvidersRow( icon = { Icon( Icons.Filled.Add, - contentDescription = null + contentDescription = null, + modifier = Modifier.padding(start = 16.dp) ) }, shape = MaterialTheme.shapes.large, @@ -580,12 +609,42 @@ fun MoreOptionsDisabledProvidersRow( 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 8b94201ad787..af74b8ea4de1 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 @@ import com.android.credentialmanager.common.ResultState 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 @@ class CreatePasskeyViewModel( // 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 23592c370c53..dcdd71a283a8 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.material.rememberModalBottomSheetSta 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 @@ fun GetCredentialScreen( 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 @@ fun GetCredentialScreen( } /** 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 @@ fun PrimarySelectionCard( items(sortedUserNameToCredentialEntryList) { CredentialEntryRow( credentialEntryInfo = it.sortedCredentialEntryList.first(), - onEntrySelected = onEntrySelected + onEntrySelected = onEntrySelected, + ) + } + items(authenticationEntryList) { + AuthenticationEntryRow( + authenticationEntryInfo = it, + onEntrySelected = onEntrySelected, ) } item { @@ -164,10 +170,13 @@ fun PrimarySelectionCard( @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 @@ fun AllSignInOptionCard( 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 @@ fun CredentialEntryRow( @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 f44927482fed..f78456aab332 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 @@ data class GetCredentialUiState( 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 @@ class GetCredentialViewModel( } } -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 @@ internal fun createSortedUserNameToCredentialEntryList( } } } + + // 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 84009b1eb441..c1d9ea9b9188 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 @@ data class ProviderInfo( // 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 @@ class AuthenticationEntryInfo( providerId: String, entryKey: String, entrySubkey: String, + val title: String, + val icon: Drawable, ) : EntryInfo(providerId, entryKey, entrySubkey) class ActionEntryInfo( @@ -66,6 +79,7 @@ class ActionEntryInfo( entryKey: String, entrySubkey: String, val title: String, + val icon: Drawable, val subTitle: String?, ) : EntryInfo(providerId, entryKey, entrySubkey) diff --git a/packages/SettingsLib/OWNERS b/packages/SettingsLib/OWNERS index 8eafbdfdf6bc..a53782a0bc6f 100644 --- a/packages/SettingsLib/OWNERS +++ b/packages/SettingsLib/OWNERS @@ -3,10 +3,11 @@ dsandler@android.com edgarwang@google.com emilychuang@google.com evanlaird@google.com +hanxu@google.com juliacr@google.com leifhendrik@google.com -tmfang@google.com virgild@google.com +ykhung@google.com # Exempt resource files (because they are in a flat directory and too hard to manage via OWNERS) per-file *.xml=* diff --git a/packages/SettingsLib/Spa/build.gradle b/packages/SettingsLib/Spa/build.gradle index 4fb77d7257ca..b8fd5791404f 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/AndroidManifest.xml b/packages/SettingsLib/Spa/gallery/AndroidManifest.xml index 50cab84e0d9d..1e52aaf72076 100644 --- a/packages/SettingsLib/Spa/gallery/AndroidManifest.xml +++ b/packages/SettingsLib/Spa/gallery/AndroidManifest.xml @@ -41,6 +41,20 @@ android:exported="false"> </provider> + <provider android:name="com.android.settingslib.spa.framework.SpaSliceProvider" + android:authorities="com.android.spa.gallery.slice.provider" + android:exported="true" > + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.app.slice.category.SLICE" /> + </intent-filter> + </provider> + + <receiver + android:name="com.android.settingslib.spa.framework.SpaSliceBroadcastReceiver" + android:exported="false"> + </receiver> + <activity android:name="com.android.settingslib.spa.framework.debug.BlankActivity" android:exported="true"> diff --git a/packages/SettingsLib/Spa/gallery/build.gradle b/packages/SettingsLib/Spa/gallery/build.gradle index e04a9be2acd6..7868aff4c189 100644 --- a/packages/SettingsLib/Spa/gallery/build.gradle +++ b/packages/SettingsLib/Spa/gallery/build.gradle @@ -21,12 +21,13 @@ plugins { 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/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt index 016b27f0c82c..941e7705441b 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt @@ -17,6 +17,7 @@ package com.android.settingslib.spa.gallery import android.content.Context +import com.android.settingslib.spa.framework.SpaSliceBroadcastReceiver import com.android.settingslib.spa.framework.common.LocalLogger import com.android.settingslib.spa.framework.common.SettingsPageProviderRepository import com.android.settingslib.spa.framework.common.SpaEnvironment @@ -79,8 +80,8 @@ class GallerySpaEnvironment(context: Context) : SpaEnvironment(context) { } override val browseActivityClass = GalleryMainActivity::class.java - + override val sliceBroadcastReceiverClass = SpaSliceBroadcastReceiver::class.java override val searchProviderAuthorities = "com.android.spa.gallery.search.provider" - + override val sliceProviderAuthorities = "com.android.spa.gallery.slice.provider" override val logger = LocalLogger() } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePage.kt index 26e59ff699a4..ff89f2b25e7d 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePage.kt @@ -27,6 +27,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.android.settingslib.spa.framework.common.EntrySearchData +import com.android.settingslib.spa.framework.common.EntrySliceData import com.android.settingslib.spa.framework.common.EntryStatusData import com.android.settingslib.spa.framework.common.SettingsEntry import com.android.settingslib.spa.framework.common.SettingsEntryBuilder @@ -37,6 +38,7 @@ import com.android.settingslib.spa.framework.compose.toState import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.gallery.R import com.android.settingslib.spa.gallery.SettingsPageProviderEnum +import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.ASYNC_PREFERENCE_SUMMARY import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.ASYNC_PREFERENCE_TITLE import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.AUTO_UPDATE_PREFERENCE_TITLE import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.DISABLE_PREFERENCE_SUMMARY @@ -46,10 +48,15 @@ import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Compan import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.SIMPLE_PREFERENCE_KEYWORDS import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.SIMPLE_PREFERENCE_SUMMARY import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.SIMPLE_PREFERENCE_TITLE +import com.android.settingslib.spa.slice.createBrowsePendingIntent +import com.android.settingslib.spa.slice.provider.createDemoActionSlice +import com.android.settingslib.spa.slice.provider.createDemoBrowseSlice +import com.android.settingslib.spa.slice.provider.createDemoSlice import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.PreferenceModel import com.android.settingslib.spa.widget.preference.SimplePreferenceMacro import com.android.settingslib.spa.widget.ui.SettingsIcon +import kotlinx.coroutines.delay private const val TAG = "PreferencePage" @@ -134,6 +141,26 @@ object PreferencePageProvider : SettingsPageProvider { override val enabled = model.asyncEnable } ) + } + .setSliceDataFn { sliceUri, _ -> + val createSliceImpl = { s: String -> + createDemoBrowseSlice( + sliceUri = sliceUri, + title = ASYNC_PREFERENCE_TITLE, + summary = s, + ) + } + return@setSliceDataFn object : EntrySliceData() { + init { + postValue(createSliceImpl("(loading)")) + } + + override suspend fun asyncRunner() { + spaLogger.message(TAG, "Async entry loading") + delay(2000L) + postValue(createSliceImpl(ASYNC_PREFERENCE_SUMMARY)) + } + } }.build() ) entryList.add( @@ -152,6 +179,27 @@ object PreferencePageProvider : SettingsPageProvider { } } ) + }.setSliceDataFn { sliceUri, args -> + val createSliceImpl = { v: Int -> + createDemoActionSlice( + sliceUri = sliceUri, + title = MANUAL_UPDATE_PREFERENCE_TITLE, + summary = "manual update value $v", + ) + } + + return@setSliceDataFn object : EntrySliceData() { + private var tick = args?.getString("init")?.toInt() ?: 0 + + init { + postValue(createSliceImpl(tick)) + } + + override suspend fun asyncAction() { + tick++ + postValue(createSliceImpl(tick)) + } + } }.build() ) entryList.add( @@ -170,7 +218,33 @@ object PreferencePageProvider : SettingsPageProvider { } ) } - .build() + .setSliceDataFn { sliceUri, args -> + val createSliceImpl = { v: Int -> + createDemoBrowseSlice( + sliceUri = sliceUri, + title = AUTO_UPDATE_PREFERENCE_TITLE, + summary = "auto update value $v", + ) + } + + return@setSliceDataFn object : EntrySliceData() { + private var tick = args?.getString("init")?.toInt() ?: 0 + + init { + postValue(createSliceImpl(tick)) + } + + override suspend fun asyncRunner() { + spaLogger.message(TAG, "autoUpdater.active") + while (true) { + delay(1000L) + tick++ + spaLogger.message(TAG, "autoUpdater.value $tick") + postValue(createSliceImpl(tick)) + } + } + } + }.build() ) return entryList @@ -201,6 +275,22 @@ object PreferencePageProvider : SettingsPageProvider { clickRoute = SettingsPageProviderEnum.PREFERENCE.name ) } + .setSliceDataFn { sliceUri, _ -> + val intent = owner.createBrowseIntent()?.createBrowsePendingIntent() + ?: return@setSliceDataFn null + return@setSliceDataFn object : EntrySliceData() { + init { + postValue( + createDemoSlice( + sliceUri = sliceUri, + title = PAGE_TITLE, + summary = "Injected Entry", + intent = intent, + ) + ) + } + } + } } override fun getTitle(arguments: Bundle?): String { diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageModel.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageModel.kt index d874417c1a56..fc6f10f79ceb 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageModel.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageModel.kt @@ -44,7 +44,7 @@ class PreferencePageModel : PageModel() { const val DISABLE_PREFERENCE_TITLE = "Disabled" const val DISABLE_PREFERENCE_SUMMARY = "Disabled summary" const val ASYNC_PREFERENCE_TITLE = "Async Preference" - private const val ASYNC_PREFERENCE_SUMMARY = "Async summary" + const val ASYNC_PREFERENCE_SUMMARY = "Async summary" const val MANUAL_UPDATE_PREFERENCE_TITLE = "Manual Updater" const val AUTO_UPDATE_PREFERENCE_TITLE = "Auto Updater" val SIMPLE_PREFERENCE_KEYWORDS = listOf("simple keyword1", "simple keyword2") diff --git a/packages/SettingsLib/Spa/spa/Android.bp b/packages/SettingsLib/Spa/spa/Android.bp index 037b45e6732a..2eaa73e8b21c 100644 --- a/packages/SettingsLib/Spa/spa/Android.bp +++ b/packages/SettingsLib/Spa/spa/Android.bp @@ -24,6 +24,9 @@ android_library { 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 7a20c747249b..4944784c190c 100644 --- a/packages/SettingsLib/Spa/spa/build.gradle +++ b/packages/SettingsLib/Spa/spa/build.gradle @@ -21,11 +21,12 @@ plugins { 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 @@ android { 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 35b9c0fbd16b..3689e4e66d11 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 @@ class SpaSearchProvider : ContentProvider() { 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 @@ class SpaSearchProvider : ContentProvider() { 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 000000000000..58131e69d329 --- /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 by 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 000000000000..faa04fda1904 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSliceProvider.kt @@ -0,0 +1,77 @@ +/* + * 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 + val sliceRepository by SpaEnvironmentFactory.instance.sliceDataRepository + return sliceRepository.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 000000000000..fc551a88f6c5 --- /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 224fe1d03390..9ee7f9ef7f86 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 @@ interface EntryData { 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 @@ data class SettingsEntry( // 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 @@ data class SettingsEntry( * 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 @@ data class SettingsEntry( * 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 @@ data class SettingsEntry( 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 @@ class SettingsEntryBuilder(private val name: String, private val owner: Settings // 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 @@ class SettingsEntryBuilder(private val name: String, private val owner: Settings // attributes isAllowSearch = isAllowSearch, isSearchDataDynamic = isSearchDataDynamic, - mutableStatus = mutableStatus, + hasMutableStatus = hasMutableStatus, + hasSliceSupport = hasSliceSupport, // functions statusDataImpl = statusDataFn, searchDataImpl = searchDataFn, + sliceDataImpl = sliceDataFn, uiLayoutImpl = uiLayoutFn, ) } @@ -207,7 +230,7 @@ class SettingsEntryBuilder(private val name: String, private val owner: Settings } fun setHasMutableStatus(hasMutableStatus: Boolean): SettingsEntryBuilder { - this.mutableStatus = hasMutableStatus + this.hasMutableStatus = hasMutableStatus return this } @@ -221,17 +244,23 @@ class SettingsEntryBuilder(private val name: String, private val owner: Settings 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 bb287d1b927a..a372bbd3721b 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 @@ data class SettingsPage( ) } + 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 151b50cdb5c4..60599d49968f 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 @@ interface SettingsPageProvider { 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 a9cb041cc198..945add47b965 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 @@ object SpaEnvironmentFactory { Log.d(TAG, "resetForPreview") } + fun isReady(): Boolean { + return spaEnvironment != null + } + val instance: SpaEnvironment get() { if (spaEnvironment == null) @@ -59,13 +65,15 @@ abstract class SpaEnvironment(context: Context) { val entryRepository = lazy { SettingsEntryRepository(pageProviderRepository.value) } + val sliceDataRepository = lazy { SettingsSliceDataRepository(entryRepository.value) } + // 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 26491d51e838..760064a7d691 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.localNavController 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 TAG = "DebugActivity" 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 @@ class DebugActivity : ComponentActivity() { 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 @@ class DebugActivity : ComponentActivity() { 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 @@ class DebugActivity : ComponentActivity() { 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 @@ class DebugActivity : ComponentActivity() { } @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 @@ class DebugActivity : ComponentActivity() { } } +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 000000000000..14855a8aed59 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SettingsSliceDataRepository.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.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.SettingsEntryRepository + +private const val TAG = "SliceDataRepository" + +class SettingsSliceDataRepository(private val entryRepository: SettingsEntryRepository) { + // 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") + + 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 000000000000..ff143ed864c8 --- /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 000000000000..cff1c0c619c4 --- /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 000000000000..b65b91f7990e --- /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 529a20156e46..2d501fccab3a 100644 --- a/packages/SettingsLib/Spa/tests/build.gradle +++ b/packages/SettingsLib/Spa/tests/build.gradle @@ -21,11 +21,12 @@ plugins { 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 @@ android { 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 31d2ae46fad7..2017d538ae81 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 @@ class SettingsEntryTest { 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 @@ class SettingsEntryTest { 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 539e56b1e134..7097a5da4268 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 @@ class SettingsPageTest { 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 71d7d8a89c54..cbfbb9ccbe90 100644 --- a/packages/SettingsLib/Spa/testutils/build.gradle +++ b/packages/SettingsLib/Spa/testutils/build.gradle @@ -20,11 +20,12 @@ plugins { } 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 000000000000..a397bb48103c --- /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 a7de4ce18b32..b2ea4a084e48 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.content.IntentFilter 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 @@ import androidx.lifecycle.LifecycleEventObserver */ @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/AppListModel.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListModel.kt index 373b57f73ef0..a7122d0eb03a 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListModel.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListModel.kt @@ -54,6 +54,14 @@ interface AppListModel<T : AppRecord> { ) /** + * Gets the group title of this item. + * + * Note: Items should be sorted by group in [getComparator] first, this [getGroupTitle] will not + * change the list order. + */ + fun getGroupTitle(option: Int, record: T): String? = null + + /** * Gets the summary for the given app record. * * @return null if no summary should be displayed. 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 ee8900352cf2..487dbcb29927 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.Intent 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 @@ internal data class AppListConfig( /** * The repository to load the App List data. */ -internal class AppListRepository(context: Context) { - private val packageManager = context.packageManager +internal interface AppListRepository { + /** Loads the list of [ApplicationInfo]. */ + suspend fun loadApps(config: AppListConfig): List<ApplicationInfo> - fun loadApps(configFlow: Flow<AppListConfig>): Flow<List<ApplicationInfo>> = configFlow - .map { loadApps(it) } - .flowOn(Dispatchers.Default) + /** Gets the flow of predicate that could used to filter system app. */ + fun showSystemPredicate( + userIdFlow: Flow<Int>, + showSystemFlow: Flow<Boolean>, + ): Flow<(app: ApplicationInfo) -> Boolean> +} - 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) - } +internal class AppListRepositoryImpl(context: Context) : AppListRepository { + private val packageManager = context.packageManager + + 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) + + 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 1e487daa36fb..650b27845bd2 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 com.android.settingslib.spa.framework.util.waitFirst 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 @@ internal data class AppListData<T : AppRecord>( 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 @@ internal class AppListViewModel<T : AppRecord>( 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 @@ internal class AppListViewModel<T : AppRecord>( 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 34f12af28dce..90710db6388b 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 android.graphics.drawable.Drawable 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 @@ interface AppRepository { 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 @@ internal class AppRepositoryImpl(private val context: Context) : AppRepository { 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/model/app/PackageManagers.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagers.kt index 215d22cd0c5e..cb0bfc648090 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagers.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagers.kt @@ -26,43 +26,76 @@ import com.android.settingslib.spa.framework.util.asyncFilter private const val TAG = "PackageManagers" -object PackageManagers { +interface IPackageManagers { + fun getPackageInfoAsUser(packageName: String, userId: Int): PackageInfo? + fun getApplicationInfoAsUser(packageName: String, userId: Int): ApplicationInfo? + + /** Checks whether a package is installed for a given user. */ + fun isPackageInstalledAsUser(packageName: String, userId: Int): Boolean + fun ApplicationInfo.hasRequestPermission(permission: String): Boolean + + /** Checks whether a permission is currently granted to the application. */ + fun ApplicationInfo.hasGrantPermission(permission: String): Boolean + + suspend fun getAppOpPermissionPackages(userId: Int, permission: String): Set<String> + fun getPackageInfoAsUser(packageName: String, flags: Int, userId: Int): PackageInfo? +} + +object PackageManagers : IPackageManagers by PackageManagersImpl(PackageManagerWrapperImpl) + +internal interface PackageManagerWrapper { + fun getPackageInfoAsUserCached( + packageName: String, + flags: Long, + userId: Int, + ): PackageInfo? +} + +internal object PackageManagerWrapperImpl : PackageManagerWrapper { + override fun getPackageInfoAsUserCached( + packageName: String, + flags: Long, + userId: Int, + ): PackageInfo? = PackageManager.getPackageInfoAsUserCached(packageName, flags, userId) +} + +internal class PackageManagersImpl( + private val packageManagerWrapper: PackageManagerWrapper, +) : IPackageManagers { private val iPackageManager by lazy { AppGlobals.getPackageManager() } - fun getPackageInfoAsUser(packageName: String, userId: Int): PackageInfo? = + override fun getPackageInfoAsUser(packageName: String, userId: Int): PackageInfo? = getPackageInfoAsUser(packageName, 0, userId) - fun getApplicationInfoAsUser(packageName: String, userId: Int): ApplicationInfo? = + override fun getApplicationInfoAsUser(packageName: String, userId: Int): ApplicationInfo? = PackageManager.getApplicationInfoAsUserCached(packageName, 0, userId) - /** Checks whether a package is installed for a given user. */ - fun isPackageInstalledAsUser(packageName: String, userId: Int): Boolean = + override fun isPackageInstalledAsUser(packageName: String, userId: Int): Boolean = getApplicationInfoAsUser(packageName, userId)?.hasFlag(ApplicationInfo.FLAG_INSTALLED) ?: false - fun ApplicationInfo.hasRequestPermission(permission: String): Boolean { + override fun ApplicationInfo.hasRequestPermission(permission: String): Boolean { val packageInfo = getPackageInfoAsUser(packageName, PackageManager.GET_PERMISSIONS, userId) return packageInfo?.requestedPermissions?.let { permission in it } ?: false } - fun ApplicationInfo.hasGrantPermission(permission: String): Boolean { + override fun ApplicationInfo.hasGrantPermission(permission: String): Boolean { val packageInfo = getPackageInfoAsUser(packageName, PackageManager.GET_PERMISSIONS, userId) - ?: return false - val index = packageInfo.requestedPermissions.indexOf(permission) + val index = packageInfo?.requestedPermissions?.indexOf(permission) ?: return false return index >= 0 && packageInfo.requestedPermissionsFlags[index].hasFlag(REQUESTED_PERMISSION_GRANTED) } - suspend fun getAppOpPermissionPackages(userId: Int, permission: String): Set<String> = + override suspend fun getAppOpPermissionPackages(userId: Int, permission: String): Set<String> = iPackageManager.getAppOpPermissionPackages(permission, userId).asIterable().asyncFilter { iPackageManager.isPackageAvailable(it, userId) }.toSet() - fun getPackageInfoAsUser(packageName: String, flags: Int, userId: Int): PackageInfo? = + override fun getPackageInfoAsUser(packageName: String, flags: Int, userId: Int): PackageInfo? = try { - PackageManager.getPackageInfoAsUserCached(packageName, flags.toLong(), userId) + packageManagerWrapper.getPackageInfoAsUserCached(packageName, flags.toLong(), userId) } catch (e: PackageManager.NameNotFoundException) { Log.w(TAG, "getPackageInfoAsUserCached() failed", e) null 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 9d6b311a33cb..681eb1c3508e 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 @@ -31,8 +34,11 @@ import com.android.settingslib.spa.framework.compose.LogCompositions import com.android.settingslib.spa.framework.compose.TimeMeasurer.Companion.rememberTimeMeasurer import com.android.settingslib.spa.framework.compose.rememberLazyListStateAndHideKeyboardWhenStartScroll import com.android.settingslib.spa.framework.compose.toState +import com.android.settingslib.spa.widget.ui.CategoryTitle 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.AppEntry import com.android.settingslib.spaprivileged.model.app.AppListConfig import com.android.settingslib.spaprivileged.model.app.AppListData import com.android.settingslib.spaprivileged.model.app.AppListModel @@ -96,17 +102,28 @@ private fun <T : AppRecord> AppListWidget( } items(count = list.size, key = { option to list[it].record.app.packageName }) { + remember(list) { listModel.getGroupTitleIfFirst(option, list, it) } + ?.let { group -> CategoryTitle(title = group) } + val appEntry = list[it] val summary = listModel.getSummary(option, appEntry.record) ?: "".toState() - val itemModel = remember(appEntry) { + appItem(remember(appEntry) { AppListItemModel(appEntry.record, appEntry.label, summary) - } - appItem(itemModel) + }) } } } } +/** Returns group title if this is the first item of the group. */ +private fun <T : AppRecord> AppListModel<T>.getGroupTitleIfFirst( + option: Int, + list: List<AppEntry<T>>, + index: Int, +): String? = getGroupTitle(option, list[index].record)?.takeIf { + index == 0 || it != getGroupTitle(option, list[index - 1].record) +} + @Composable private fun <T : AppRecord> loadAppListData( config: AppListConfig, @@ -120,5 +137,15 @@ private fun <T : AppRecord> loadAppListData( 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 5afe21e9a691..12955c887480 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/Android.bp +++ b/packages/SettingsLib/SpaPrivileged/tests/Android.bp @@ -31,6 +31,7 @@ android_test { ], 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 5d5a24eed0c3..bc6925baacc2 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 android.content.pm.PackageManager.ResolveInfoFlags 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.Mock 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 @@ class AppListRepositoryTest { 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 000000000000..b570815b4180 --- /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/model/app/PackageManagersTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/PackageManagersTest.kt new file mode 100644 index 000000000000..6c31f4b7e2f1 --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/PackageManagersTest.kt @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spaprivileged.model.app + +import android.content.pm.ApplicationInfo +import android.content.pm.PackageInfo +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class PackageManagersTest { + + private val fakePackageManagerWrapper = FakePackageManagerWrapper() + + private val packageManagersImpl = PackageManagersImpl(fakePackageManagerWrapper) + + @Test + fun hasGrantPermission_packageInfoIsNull_returnFalse() { + fakePackageManagerWrapper.fakePackageInfo = null + + val hasGrantPermission = with(packageManagersImpl) { + APP.hasGrantPermission(PERMISSION_A) + } + + assertThat(hasGrantPermission).isFalse() + } + + @Test + fun hasGrantPermission_requestedPermissionsIsNull_returnFalse() { + fakePackageManagerWrapper.fakePackageInfo = PackageInfo() + + val hasGrantPermission = with(packageManagersImpl) { + APP.hasGrantPermission(PERMISSION_A) + } + + assertThat(hasGrantPermission).isFalse() + } + + @Test + fun hasGrantPermission_notRequested_returnFalse() { + fakePackageManagerWrapper.fakePackageInfo = PackageInfo().apply { + requestedPermissions = arrayOf(PERMISSION_B) + requestedPermissionsFlags = intArrayOf(PackageInfo.REQUESTED_PERMISSION_GRANTED) + } + + val hasGrantPermission = with(packageManagersImpl) { + APP.hasGrantPermission(PERMISSION_A) + } + + assertThat(hasGrantPermission).isFalse() + } + + @Test + fun hasGrantPermission_notGranted_returnFalse() { + fakePackageManagerWrapper.fakePackageInfo = PackageInfo().apply { + requestedPermissions = arrayOf(PERMISSION_A, PERMISSION_B) + requestedPermissionsFlags = intArrayOf(0, PackageInfo.REQUESTED_PERMISSION_GRANTED) + } + + val hasGrantPermission = with(packageManagersImpl) { + APP.hasGrantPermission(PERMISSION_A) + } + + assertThat(hasGrantPermission).isFalse() + } + + @Test + fun hasGrantPermission_granted_returnTrue() { + fakePackageManagerWrapper.fakePackageInfo = PackageInfo().apply { + requestedPermissions = arrayOf(PERMISSION_A, PERMISSION_B) + requestedPermissionsFlags = intArrayOf(PackageInfo.REQUESTED_PERMISSION_GRANTED, 0) + } + + val hasGrantPermission = with(packageManagersImpl) { + APP.hasGrantPermission(PERMISSION_A) + } + + assertThat(hasGrantPermission).isTrue() + } + + private inner class FakePackageManagerWrapper : PackageManagerWrapper { + var fakePackageInfo: PackageInfo? = null + + override fun getPackageInfoAsUserCached( + packageName: String, + flags: Long, + userId: Int, + ): PackageInfo? = fakePackageInfo + } + + private companion object { + const val PACKAGE_NAME = "packageName" + const val PERMISSION_A = "permission.A" + const val PERMISSION_B = "permission.B" + const val UID = 123 + val APP = ApplicationInfo().apply { + packageName = PACKAGE_NAME + uid = UID + } + } +} diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListTest.kt index 80c4eac9b98f..9f20c78485a8 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListTest.kt @@ -58,26 +58,43 @@ class AppListTest { @Test fun couldShowAppItem() { - setContent(appEntries = listOf(APP_ENTRY)) + setContent(appEntries = listOf(APP_ENTRY_A)) - composeTestRule.onNodeWithText(APP_ENTRY.label).assertIsDisplayed() + composeTestRule.onNodeWithText(APP_ENTRY_A.label).assertIsDisplayed() } @Test fun couldShowHeader() { - setContent(header = { Text(HEADER) }, appEntries = listOf(APP_ENTRY)) + setContent(appEntries = listOf(APP_ENTRY_A), header = { Text(HEADER) }) composeTestRule.onNodeWithText(HEADER).assertIsDisplayed() } + @Test + fun whenNotGrouped_groupTitleDoesNotExist() { + setContent(appEntries = listOf(APP_ENTRY_A, APP_ENTRY_B), enableGrouping = false) + + composeTestRule.onNodeWithText(GROUP_A).assertDoesNotExist() + composeTestRule.onNodeWithText(GROUP_B).assertDoesNotExist() + } + + @Test + fun whenGrouped_groupTitleDisplayed() { + setContent(appEntries = listOf(APP_ENTRY_A, APP_ENTRY_B), enableGrouping = true) + + composeTestRule.onNodeWithText(GROUP_A).assertIsDisplayed() + composeTestRule.onNodeWithText(GROUP_B).assertIsDisplayed() + } + private fun setContent( - header: @Composable () -> Unit = {}, appEntries: List<AppEntry<TestAppRecord>>, + header: @Composable () -> Unit = {}, + enableGrouping: Boolean = false, ) { composeTestRule.setContent { AppList( config = AppListConfig(userId = USER_ID, showInstantApps = false), - listModel = TestAppListModel(), + listModel = TestAppListModel(enableGrouping), state = AppListState( showSystem = false.toState(), option = 0.toState(), @@ -96,17 +113,37 @@ class AppListTest { private companion object { const val USER_ID = 0 const val HEADER = "Header" - val APP_ENTRY = AppEntry( - record = TestAppRecord(ApplicationInfo()), - label = "AAA", + const val GROUP_A = "Group A" + const val GROUP_B = "Group B" + val APP_ENTRY_A = AppEntry( + record = TestAppRecord( + app = ApplicationInfo().apply { + packageName = "package.name.a" + }, + group = GROUP_A, + ), + label = "Label A", + labelCollationKey = CollationKey("", byteArrayOf()), + ) + val APP_ENTRY_B = AppEntry( + record = TestAppRecord( + app = ApplicationInfo().apply { + packageName = "package.name.b" + }, + group = GROUP_B, + ), + label = "Label B", labelCollationKey = CollationKey("", byteArrayOf()), ) } } -private data class TestAppRecord(override val app: ApplicationInfo) : AppRecord +private data class TestAppRecord( + override val app: ApplicationInfo, + val group: String? = null, +) : AppRecord -private class TestAppListModel : AppListModel<TestAppRecord> { +private class TestAppListModel(val enableGrouping: Boolean) : AppListModel<TestAppRecord> { override fun transform(userIdFlow: Flow<Int>, appListFlow: Flow<List<ApplicationInfo>>) = appListFlow.asyncMapItem { TestAppRecord(it) } @@ -118,4 +155,7 @@ private class TestAppListModel : AppListModel<TestAppRecord> { option: Int, recordListFlow: Flow<List<TestAppRecord>>, ) = recordListFlow + + override fun getGroupTitle(option: Int, record: TestAppRecord) = + if (enableGrouping) record.group else null } 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 cec6d7d5d760..b3638c21ccce 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 androidx.test.ext.junit.runners.AndroidJUnit4 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 @@ class AppStorageSizeTest { } } - assertThat(storageSize.value).isEqualTo("123 B") + composeTestRule.waitUntil { storageSize.value == "123 B" } } companion object { diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java index 7913c16b6b98..65c94cec6009 100644 --- a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java +++ b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java @@ -36,8 +36,10 @@ import android.content.pm.PackageStats; import android.content.pm.ParceledListSlice; import android.content.pm.ResolveInfo; import android.content.pm.UserInfo; +import android.content.pm.UserProperties; import android.graphics.drawable.Drawable; import android.net.Uri; +import android.os.Build; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; @@ -1577,8 +1579,8 @@ public class ApplicationsState { public long internalSize; public long externalSize; public String labelDescription; - public boolean mounted; + public boolean showInPersonalTab; /** * Setting this to {@code true} prevents the entry to be filtered by @@ -1635,6 +1637,33 @@ public class ApplicationsState { ThreadUtils.postOnBackgroundThread( () -> this.ensureLabelDescriptionLocked(context)); } + this.showInPersonalTab = shouldShowInPersonalTab(context, info.uid); + } + + /** + * Checks if the user that the app belongs to have the property + * {@link UserProperties#SHOW_IN_SETTINGS_WITH_PARENT} set. + */ + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + boolean shouldShowInPersonalTab(Context context, int uid) { + UserManager userManager = UserManager.get(context); + int userId = UserHandle.getUserId(uid); + + // Regardless of apk version, if the app belongs to the current user then return true. + if (userId == ActivityManager.getCurrentUser()) { + return true; + } + + // For sdk version < 34, if the app doesn't belong to the current user, + // then as per earlier behaviour the app shouldn't be displayed in personal tab. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + return false; + } + + UserProperties userProperties = userManager.getUserProperties( + UserHandle.of(userId)); + return userProperties.getShowInSettings() + == UserProperties.SHOW_IN_SETTINGS_WITH_PARENT; } public void ensureLabel(Context context) { @@ -1784,7 +1813,7 @@ public class ApplicationsState { @Override public boolean filterApp(AppEntry entry) { - return UserHandle.getUserId(entry.info.uid) == mCurrentUser; + return entry.showInPersonalTab; } }; @@ -1811,7 +1840,7 @@ public class ApplicationsState { @Override public boolean filterApp(AppEntry entry) { - return UserHandle.getUserId(entry.info.uid) != mCurrentUser; + return !entry.showInPersonalTab; } }; diff --git a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/EventLogWriter.java b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/EventLogWriter.java index 3e33da5a2ba7..ece898618f8c 100644 --- a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/EventLogWriter.java +++ b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/EventLogWriter.java @@ -50,6 +50,34 @@ public class EventLogWriter implements LogWriter { @Override public void clicked(int sourceCategory, String key) { + final LogMaker logMaker = new LogMaker(MetricsProto.MetricsEvent.ACTION_SETTINGS_TILE_CLICK) + .setType(MetricsProto.MetricsEvent.TYPE_ACTION); + if (sourceCategory != MetricsProto.MetricsEvent.VIEW_UNKNOWN) { + logMaker.addTaggedData(MetricsProto.MetricsEvent.FIELD_CONTEXT, sourceCategory); + } + if (!TextUtils.isEmpty(key)) { + logMaker.addTaggedData(MetricsProto.MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_NAME, + key); + } + MetricsLogger.action(logMaker); + } + + @Override + public void changed(int category, String key, int value) { + final LogMaker logMaker = new LogMaker( + MetricsProto.MetricsEvent.ACTION_SETTINGS_PREFERENCE_CHANGE) + .setType(MetricsProto.MetricsEvent.TYPE_ACTION); + if (category != MetricsProto.MetricsEvent.VIEW_UNKNOWN) { + logMaker.addTaggedData(MetricsProto.MetricsEvent.FIELD_CONTEXT, category); + } + if (!TextUtils.isEmpty(key)) { + logMaker.addTaggedData(MetricsProto.MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_NAME, + key); + logMaker.addTaggedData( + MetricsProto.MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_INT_VALUE, + value); + } + MetricsLogger.action(logMaker); } @Override diff --git a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/LogWriter.java b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/LogWriter.java index cceca13ec3e5..dcd6ccedf9bc 100644 --- a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/LogWriter.java +++ b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/LogWriter.java @@ -39,6 +39,11 @@ public interface LogWriter { void clicked(int category, String key); /** + * Logs a value changed event when user changed item value. + */ + void changed(int category, String key, int value); + + /** * Logs an user action. */ void action(Context context, int category, Pair<Integer, Object>... taggedData); diff --git a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/MetricsFeatureProvider.java b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/MetricsFeatureProvider.java index 915421a8f7a0..09abc394634a 100644 --- a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/MetricsFeatureProvider.java +++ b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/MetricsFeatureProvider.java @@ -108,6 +108,19 @@ public class MetricsFeatureProvider { } /** + * Logs a value changed event when user changed item value. + * + * @param category the target page id + * @param key the key id that user clicked + * @param value the value that user changed which converted to integer + */ + public void changed(int category, String key, int value) { + for (LogWriter writer : mLoggerWriters) { + writer.changed(category, key, value); + } + } + + /** * Logs a simple action without page id or attribution * * @param category the target page diff --git a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/SharedPreferencesLogger.java b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/SharedPreferencesLogger.java index 869de0debd37..067afa4ff726 100644 --- a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/SharedPreferencesLogger.java +++ b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/SharedPreferencesLogger.java @@ -35,15 +35,22 @@ public class SharedPreferencesLogger implements SharedPreferences { private static final String LOG_TAG = "SharedPreferencesLogger"; private final String mTag; + private final int mMetricCategory; private final Context mContext; private final MetricsFeatureProvider mMetricsFeature; private final Set<String> mPreferenceKeySet; public SharedPreferencesLogger(Context context, String tag, MetricsFeatureProvider metricsFeature) { + this(context, tag, metricsFeature, SettingsEnums.PAGE_UNKNOWN); + } + + public SharedPreferencesLogger(Context context, String tag, + MetricsFeatureProvider metricsFeature, int metricCategory) { mContext = context; mTag = tag; mMetricsFeature = metricsFeature; + mMetricCategory = metricCategory; mPreferenceKeySet = new ConcurrentSkipListSet<>(); } @@ -151,20 +158,15 @@ public class SharedPreferencesLogger implements SharedPreferences { return; } // Pref key exists in set, log its change in metrics. - mMetricsFeature.action(SettingsEnums.PAGE_UNKNOWN, - SettingsEnums.ACTION_SETTINGS_PREFERENCE_CHANGE, - SettingsEnums.PAGE_UNKNOWN, - prefKey, - intVal); + mMetricsFeature.changed(mMetricCategory, key, intVal); } @VisibleForTesting void logPackageName(String key, String value) { - final String prefKey = mTag + "/" + key; - mMetricsFeature.action(SettingsEnums.PAGE_UNKNOWN, + mMetricsFeature.action(mMetricCategory, SettingsEnums.ACTION_SETTINGS_PREFERENCE_CHANGE, SettingsEnums.PAGE_UNKNOWN, - prefKey + ":" + value, + key + ":" + value, 0); } diff --git a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java index 34da30555fb3..3e710e4c7e35 100644 --- a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java @@ -24,7 +24,6 @@ import android.os.PowerManager; import android.os.UserHandle; import android.provider.Settings.Global; import android.provider.Settings.Secure; -import android.text.TextUtils; import android.util.KeyValueListParser; import android.util.Log; import android.util.Slog; @@ -221,17 +220,14 @@ public class BatterySaverUtils { } /** - * Reverts battery saver schedule mode to none if we are in a bad state where routine mode - * is selected but no app is configured to actually provide the signal. + * Reverts battery saver schedule mode to none if routine mode is selected. * @param context a valid context */ public static void revertScheduleToNoneIfNeeded(Context context) { ContentResolver resolver = context.getContentResolver(); final int currentMode = Global.getInt(resolver, Global.AUTOMATIC_POWER_SAVE_MODE, PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE); - boolean providerConfigured = !TextUtils.isEmpty(context.getString( - com.android.internal.R.string.config_batterySaverScheduleProvider)); - if (currentMode == PowerManager.POWER_SAVE_MODE_TRIGGER_DYNAMIC && !providerConfigured) { + if (currentMode == PowerManager.POWER_SAVE_MODE_TRIGGER_DYNAMIC) { Global.putInt(resolver, Global.LOW_POWER_MODE_TRIGGER_LEVEL, 0); Global.putInt(resolver, Global.AUTOMATIC_POWER_SAVE_MODE, PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE); diff --git a/packages/SettingsLib/src/com/android/settingslib/graph/ThemedBatteryDrawable.kt b/packages/SettingsLib/src/com/android/settingslib/graph/ThemedBatteryDrawable.kt index 5fa04f93e993..faea5b2c3e71 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 @@ open class ThemedBatteryDrawable(private val context: Context, frameColor: Int) } 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/SettingsLib/src/com/android/settingslib/media/MediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java index c829bc316246..3ba51d2a2602 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java @@ -293,7 +293,7 @@ public abstract class MediaDevice implements Comparable<MediaDevice> { return false; } setConnectedRecord(); - mRouterManager.selectRoute(mPackageName, mRouteInfo); + mRouterManager.transfer(mPackageName, mRouteInfo); return true; } diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/applications/ApplicationsStateTest.java b/packages/SettingsLib/tests/integ/src/com/android/settingslib/applications/ApplicationsStateTest.java index f1e1e7d920cc..c5598bfa9438 100644 --- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/applications/ApplicationsStateTest.java +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/applications/ApplicationsStateTest.java @@ -293,4 +293,15 @@ public class ApplicationsStateTest { assertThat(ApplicationsState.FILTER_MOVIES.filterApp(mEntry)).isFalse(); } + + @Test + public void testPersonalAndWorkFiltersDisplaysCorrectApps() { + mEntry.showInPersonalTab = true; + assertThat(ApplicationsState.FILTER_PERSONAL.filterApp(mEntry)).isTrue(); + assertThat(ApplicationsState.FILTER_WORK.filterApp(mEntry)).isFalse(); + + mEntry.showInPersonalTab = false; + assertThat(ApplicationsState.FILTER_PERSONAL.filterApp(mEntry)).isFalse(); + assertThat(ApplicationsState.FILTER_WORK.filterApp(mEntry)).isTrue(); + } } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java index fc2bf0a9bd93..39875f7950e4 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java @@ -17,6 +17,7 @@ package com.android.settingslib.applications; import static android.os.UserHandle.MU_ENABLED; +import static android.os.UserHandle.USER_SYSTEM; import static com.google.common.truth.Truth.assertThat; @@ -48,9 +49,11 @@ import android.content.pm.ModuleInfo; import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; import android.content.pm.ResolveInfo; +import android.content.pm.UserProperties; import android.content.res.Resources; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; +import android.os.Build; import android.os.Handler; import android.os.RemoteException; import android.os.UserHandle; @@ -58,6 +61,8 @@ import android.os.UserManager; import android.text.TextUtils; import android.util.IconDrawableFactory; +import androidx.test.core.app.ApplicationProvider; + import com.android.settingslib.applications.ApplicationsState.AppEntry; import com.android.settingslib.applications.ApplicationsState.Callbacks; import com.android.settingslib.applications.ApplicationsState.Session; @@ -71,6 +76,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.mockito.Spy; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; @@ -79,6 +85,7 @@ import org.robolectric.annotation.Implements; import org.robolectric.shadow.api.Shadow; import org.robolectric.shadows.ShadowContextImpl; import org.robolectric.shadows.ShadowLooper; +import org.robolectric.util.ReflectionHelpers; import java.util.ArrayList; import java.util.Arrays; @@ -95,6 +102,7 @@ public class ApplicationsStateRoboTest { private final static String LAUNCHABLE_PACKAGE_NAME = "com.android.launchable"; private static final int PROFILE_USERID = 10; + private static final int PROFILE_USERID2 = 11; private static final String PKG_1 = "PKG1"; private static final int OWNER_UID_1 = 1001; @@ -106,6 +114,10 @@ public class ApplicationsStateRoboTest { private static final String PKG_3 = "PKG3"; private static final int OWNER_UID_3 = 1003; + private static final int PROFILE_UID_3 = UserHandle.getUid(PROFILE_USERID2, OWNER_UID_3); + + private static final String CLONE_USER = "clone_user"; + private static final String RANDOM_USER = "random_user"; /** Class under test */ private ApplicationsState mApplicationsState; @@ -113,6 +125,8 @@ public class ApplicationsStateRoboTest { private Application mApplication; + @Spy + Context mContext = ApplicationProvider.getApplicationContext(); @Mock private Callbacks mCallbacks; @Captor @@ -738,4 +752,51 @@ public class ApplicationsStateRoboTest { when(configChanges.applyNewConfig(any(Resources.class))).thenReturn(false); mApplicationsState.setInterestingConfigChanges(configChanges); } + + @Test + public void shouldShowInPersonalTab_forCurrentUser_returnsTrue() { + ApplicationInfo appInfo = createApplicationInfo(PKG_1); + AppEntry primaryUserApp = createAppEntry(appInfo, 1); + + assertThat(primaryUserApp.shouldShowInPersonalTab(mContext, appInfo.uid)).isTrue(); + } + + @Test + public void shouldShowInPersonalTab_userProfilePreU_returnsFalse() { + ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", + Build.VERSION_CODES.TIRAMISU); + // Create an app (and subsequent AppEntry) in a non-primary user profile. + ApplicationInfo appInfo1 = createApplicationInfo(PKG_1, PROFILE_UID_1); + AppEntry nonPrimaryUserApp = createAppEntry(appInfo1, 1); + + assertThat(nonPrimaryUserApp.shouldShowInPersonalTab(mContext, appInfo1.uid)).isFalse(); + } + + @Test + public void shouldShowInPersonalTab_currentUserIsParent_returnsAsPerUserPropertyOfProfile1() { + // Mark system user as parent for both profile users. + ShadowUserManager shadowUserManager = Shadow + .extract(RuntimeEnvironment.application.getSystemService(UserManager.class)); + shadowUserManager.addProfile(USER_SYSTEM, PROFILE_USERID, + CLONE_USER, 0); + shadowUserManager.addProfile(USER_SYSTEM, PROFILE_USERID2, + RANDOM_USER, 0); + shadowUserManager.setupUserProperty(PROFILE_USERID, + /*showInSettings*/ UserProperties.SHOW_IN_SETTINGS_WITH_PARENT); + shadowUserManager.setupUserProperty(PROFILE_USERID2, + /*showInSettings*/ UserProperties.SHOW_IN_SETTINGS_NO); + + ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", + Build.VERSION_CODES.UPSIDE_DOWN_CAKE); + + // Treat PROFILE_USERID as a clone user profile and create an app PKG_1 in it. + ApplicationInfo appInfo1 = createApplicationInfo(PKG_1, PROFILE_UID_1); + // Treat PROFILE_USERID2 as a random non-primary profile and create an app PKG_3 in it. + ApplicationInfo appInfo2 = createApplicationInfo(PKG_3, PROFILE_UID_3); + AppEntry nonPrimaryUserApp1 = createAppEntry(appInfo1, 1); + AppEntry nonPrimaryUserApp2 = createAppEntry(appInfo2, 2); + + assertThat(nonPrimaryUserApp1.shouldShowInPersonalTab(mContext, appInfo1.uid)).isTrue(); + assertThat(nonPrimaryUserApp2.shouldShowInPersonalTab(mContext, appInfo2.uid)).isFalse(); + } } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/instrumentation/SharedPreferenceLoggerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/instrumentation/SharedPreferenceLoggerTest.java index 89de81fde889..a2b208aced64 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/instrumentation/SharedPreferenceLoggerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/instrumentation/SharedPreferenceLoggerTest.java @@ -39,7 +39,6 @@ public class SharedPreferenceLoggerTest { private static final String TEST_TAG = "tag"; private static final String TEST_KEY = "key"; - private static final String TEST_TAGGED_KEY = TEST_TAG + "/" + TEST_KEY; @Mock(answer = Answers.RETURNS_DEEP_STUBS) private Context mContext; @@ -65,10 +64,8 @@ public class SharedPreferenceLoggerTest { editor.putInt(TEST_KEY, 2); editor.putInt(TEST_KEY, 2); - verify(mMetricsFeature, times(6)).action(eq(SettingsEnums.PAGE_UNKNOWN), - eq(SettingsEnums.ACTION_SETTINGS_PREFERENCE_CHANGE), - eq(SettingsEnums.PAGE_UNKNOWN), - eq(TEST_TAGGED_KEY), + verify(mMetricsFeature, times(6)).changed(eq(SettingsEnums.PAGE_UNKNOWN), + eq(TEST_KEY), anyInt()); } @@ -82,15 +79,11 @@ public class SharedPreferenceLoggerTest { editor.putBoolean(TEST_KEY, false); - verify(mMetricsFeature).action(SettingsEnums.PAGE_UNKNOWN, - SettingsEnums.ACTION_SETTINGS_PREFERENCE_CHANGE, - SettingsEnums.PAGE_UNKNOWN, - TEST_TAGGED_KEY, + verify(mMetricsFeature).changed(SettingsEnums.PAGE_UNKNOWN, + TEST_KEY, 1); - verify(mMetricsFeature, times(3)).action(SettingsEnums.PAGE_UNKNOWN, - SettingsEnums.ACTION_SETTINGS_PREFERENCE_CHANGE, - SettingsEnums.PAGE_UNKNOWN, - TEST_TAGGED_KEY, + verify(mMetricsFeature, times(3)).changed(SettingsEnums.PAGE_UNKNOWN, + TEST_KEY, 0); } @@ -103,10 +96,8 @@ public class SharedPreferenceLoggerTest { editor.putLong(TEST_KEY, 1); editor.putLong(TEST_KEY, 2); - verify(mMetricsFeature, times(4)).action(eq(SettingsEnums.PAGE_UNKNOWN), - eq(SettingsEnums.ACTION_SETTINGS_PREFERENCE_CHANGE), - eq(SettingsEnums.PAGE_UNKNOWN), - eq(TEST_TAGGED_KEY), + verify(mMetricsFeature, times(4)).changed(eq(SettingsEnums.PAGE_UNKNOWN), + eq(TEST_KEY), anyInt()); } @@ -117,10 +108,8 @@ public class SharedPreferenceLoggerTest { editor.putLong(TEST_KEY, 1); editor.putLong(TEST_KEY, veryBigNumber); - verify(mMetricsFeature).action(SettingsEnums.PAGE_UNKNOWN, - SettingsEnums.ACTION_SETTINGS_PREFERENCE_CHANGE, - SettingsEnums.PAGE_UNKNOWN, - TEST_TAGGED_KEY, + verify(mMetricsFeature).changed(SettingsEnums.PAGE_UNKNOWN, + TEST_KEY, Integer.MAX_VALUE); } @@ -131,10 +120,8 @@ public class SharedPreferenceLoggerTest { editor.putLong(TEST_KEY, 1); editor.putLong(TEST_KEY, veryNegativeNumber); - verify(mMetricsFeature).action(SettingsEnums.PAGE_UNKNOWN, - SettingsEnums.ACTION_SETTINGS_PREFERENCE_CHANGE, - SettingsEnums.PAGE_UNKNOWN, - TEST_TAGGED_KEY, Integer.MIN_VALUE); + verify(mMetricsFeature).changed(SettingsEnums.PAGE_UNKNOWN, + TEST_KEY, Integer.MIN_VALUE); } @Test @@ -146,10 +133,8 @@ public class SharedPreferenceLoggerTest { editor.putFloat(TEST_KEY, 1); editor.putFloat(TEST_KEY, 2); - verify(mMetricsFeature, times(4)).action(eq(SettingsEnums.PAGE_UNKNOWN), - eq(SettingsEnums.ACTION_SETTINGS_PREFERENCE_CHANGE), - eq(SettingsEnums.PAGE_UNKNOWN), - eq(TEST_TAGGED_KEY), + verify(mMetricsFeature, times(4)).changed(eq(SettingsEnums.PAGE_UNKNOWN), + eq(TEST_KEY), anyInt()); } @@ -159,7 +144,7 @@ public class SharedPreferenceLoggerTest { verify(mMetricsFeature).action(SettingsEnums.PAGE_UNKNOWN, ACTION_SETTINGS_PREFERENCE_CHANGE, SettingsEnums.PAGE_UNKNOWN, - "tag/key:com.android.settings", + "key:com.android.settings", 0); } @@ -170,10 +155,8 @@ public class SharedPreferenceLoggerTest { mSharedPrefLogger.logValue(TEST_KEY, "62"); mSharedPrefLogger.logValue(TEST_KEY, "0"); - verify(mMetricsFeature, times(3)).action(eq(SettingsEnums.PAGE_UNKNOWN), - eq(SettingsEnums.ACTION_SETTINGS_PREFERENCE_CHANGE), - eq(SettingsEnums.PAGE_UNKNOWN), - eq(TEST_TAGGED_KEY), + verify(mMetricsFeature, times(3)).changed(eq(SettingsEnums.PAGE_UNKNOWN), + eq(TEST_KEY), anyInt()); } @@ -185,10 +168,8 @@ public class SharedPreferenceLoggerTest { mSharedPrefLogger.logValue(TEST_KEY, "4.2"); mSharedPrefLogger.logValue(TEST_KEY, "3.0"); - verify(mMetricsFeature, times(0)).action(eq(SettingsEnums.PAGE_UNKNOWN), - eq(SettingsEnums.ACTION_SETTINGS_PREFERENCE_CHANGE), - eq(SettingsEnums.PAGE_UNKNOWN), - eq(TEST_TAGGED_KEY), + verify(mMetricsFeature, times(0)).changed(eq(SettingsEnums.PAGE_UNKNOWN), + eq(TEST_KEY), anyInt()); } } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java index 5399f8a0eff0..c058a61a3e9e 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java @@ -461,7 +461,7 @@ public class MediaDeviceTest { public void connect_shouldSelectRoute() { mInfoMediaDevice1.connect(); - verify(mMediaRouter2Manager).selectRoute(TEST_PACKAGE_NAME, mRouteInfo1); + verify(mMediaRouter2Manager).transfer(TEST_PACKAGE_NAME, mRouteInfo1); } @Test diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java index aea2f5235201..f5c9bcd763b4 100644 --- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java +++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java @@ -884,7 +884,7 @@ public class SettingsBackupTest { 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/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index 90fab08ed43e..2e4a245df6a6 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -720,6 +720,57 @@ <!-- Permission required for CTS test - CtsDeviceLockTestCases --> <uses-permission android:name="android.permission.MANAGE_DEVICE_LOCK_STATE" /> + <!-- Permission required for CTS test - CtsAppFgsTestCases --> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" /> + + <!-- Permission required for CTS test - CtsAppFgsTestCases --> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" /> + + <!-- Permission required for CTS test - CtsAppFgsTestCases --> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" /> + + <!-- Permission required for CTS test - CtsAppFgsTestCases --> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" /> + + <!-- Permission required for CTS test - CtsAppFgsTestCases --> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" /> + + <!-- Permission required for CTS test - CtsAppFgsTestCases --> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" /> + + <!-- Permission required for CTS test - CtsAppFgsTestCases --> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" /> + + <!-- Permission required for CTS test - CtsAppFgsTestCases --> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" /> + + <!-- Permission required for CTS test - CtsAppFgsTestCases --> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_HEALTH" /> + + <!-- Permission required for CTS test - CtsAppFgsTestCases --> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING" /> + + <!-- Permission required for CTS test - CtsAppFgsTestCases --> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" /> + + <!-- Permission required for CTS test - CtsAppFgsTestCases --> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" /> + + <!-- Permissions required for CTS test - CtsAppFgsTestCases --> + <uses-permission android:name="android.permission.CAPTURE_MEDIA_OUTPUT" /> + + <!-- Permissions required for CTS test - CtsAppFgsTestCases --> + <uses-permission android:name="android.permission.CAPTURE_TUNER_AUDIO_INPUT" /> + + <!-- Permissions required for CTS test - CtsAppFgsTestCases --> + <uses-permission android:name="android.permission.CAPTURE_VOICE_COMMUNICATION_OUTPUT" /> + + <!-- Permissions required for CTS test - CtsAppFgsTestCases --> + <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" /> + + <!-- Permissions required for CTS test - CtsAppFgsTestCases --> + <uses-permission android:name="android.permission.USE_EXACT_ALARM" /> + <application android:label="@string/app_label" android:theme="@android:style/Theme.DeviceDefault.DayNight" android:defaultToDeviceProtectedStorage="true" diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 47771aa3c774..11237dca7804 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 eada40e6a40d..278a89f7dba3 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/compose/features/src/com/android/systemui/user/ui/compose/UserSwitcherScreen.kt b/packages/SystemUI/compose/features/src/com/android/systemui/user/ui/compose/UserSwitcherScreen.kt deleted file mode 100644 index 4d94bab6c26c..000000000000 --- a/packages/SystemUI/compose/features/src/com/android/systemui/user/ui/compose/UserSwitcherScreen.kt +++ /dev/null @@ -1,392 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package com.android.systemui.user.ui.compose - -import android.graphics.drawable.Drawable -import androidx.appcompat.content.res.AppCompatResources -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.sizeIn -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.graphics.painter.ColorPainter -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.colorResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import androidx.core.graphics.drawable.toBitmap -import com.android.systemui.common.ui.compose.load -import com.android.systemui.compose.SysUiOutlinedButton -import com.android.systemui.compose.SysUiTextButton -import com.android.systemui.compose.features.R -import com.android.systemui.compose.theme.LocalAndroidColorScheme -import com.android.systemui.user.ui.viewmodel.UserActionViewModel -import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel -import com.android.systemui.user.ui.viewmodel.UserViewModel -import java.lang.Integer.min -import kotlin.math.ceil - -@Composable -fun UserSwitcherScreen( - viewModel: UserSwitcherViewModel, - onFinished: () -> Unit, - modifier: Modifier = Modifier, -) { - val isFinishRequested: Boolean by viewModel.isFinishRequested.collectAsState(false) - val users: List<UserViewModel> by viewModel.users.collectAsState(emptyList()) - val maxUserColumns: Int by viewModel.maximumUserColumns.collectAsState(1) - val menuActions: List<UserActionViewModel> by viewModel.menu.collectAsState(emptyList()) - val isOpenMenuButtonVisible: Boolean by viewModel.isOpenMenuButtonVisible.collectAsState(false) - val isMenuVisible: Boolean by viewModel.isMenuVisible.collectAsState(false) - - UserSwitcherScreenStateless( - isFinishRequested = isFinishRequested, - users = users, - maxUserColumns = maxUserColumns, - menuActions = menuActions, - isOpenMenuButtonVisible = isOpenMenuButtonVisible, - isMenuVisible = isMenuVisible, - onMenuClosed = viewModel::onMenuClosed, - onOpenMenuButtonClicked = viewModel::onOpenMenuButtonClicked, - onCancelButtonClicked = viewModel::onCancelButtonClicked, - onFinished = { - onFinished() - viewModel.onFinished() - }, - modifier = modifier, - ) -} - -@Composable -private fun UserSwitcherScreenStateless( - isFinishRequested: Boolean, - users: List<UserViewModel>, - maxUserColumns: Int, - menuActions: List<UserActionViewModel>, - isOpenMenuButtonVisible: Boolean, - isMenuVisible: Boolean, - onMenuClosed: () -> Unit, - onOpenMenuButtonClicked: () -> Unit, - onCancelButtonClicked: () -> Unit, - onFinished: () -> Unit, - modifier: Modifier = Modifier, -) { - LaunchedEffect(isFinishRequested) { - if (isFinishRequested) { - onFinished() - } - } - - Box( - modifier = - modifier - .fillMaxSize() - .padding( - horizontal = 60.dp, - vertical = 40.dp, - ), - ) { - UserGrid( - users = users, - maxUserColumns = maxUserColumns, - modifier = Modifier.align(Alignment.Center), - ) - - Buttons( - menuActions = menuActions, - isOpenMenuButtonVisible = isOpenMenuButtonVisible, - isMenuVisible = isMenuVisible, - onMenuClosed = onMenuClosed, - onOpenMenuButtonClicked = onOpenMenuButtonClicked, - onCancelButtonClicked = onCancelButtonClicked, - modifier = Modifier.align(Alignment.BottomEnd), - ) - } -} - -@Composable -private fun UserGrid( - users: List<UserViewModel>, - maxUserColumns: Int, - modifier: Modifier = Modifier, -) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(44.dp), - modifier = modifier, - ) { - val rowCount = ceil(users.size / maxUserColumns.toFloat()).toInt() - (0 until rowCount).forEach { rowIndex -> - Row( - horizontalArrangement = Arrangement.spacedBy(64.dp), - modifier = modifier, - ) { - val fromIndex = rowIndex * maxUserColumns - val toIndex = min(users.size, (rowIndex + 1) * maxUserColumns) - users.subList(fromIndex, toIndex).forEach { user -> - UserItem( - viewModel = user, - ) - } - } - } - } -} - -@Composable -private fun UserItem( - viewModel: UserViewModel, -) { - val onClicked = viewModel.onClicked - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = - if (onClicked != null) { - Modifier.clickable { onClicked() } - } else { - Modifier - } - .alpha(viewModel.alpha), - ) { - Box { - UserItemBackground(modifier = Modifier.align(Alignment.Center).size(222.dp)) - - UserItemIcon( - image = viewModel.image, - isSelectionMarkerVisible = viewModel.isSelectionMarkerVisible, - modifier = Modifier.align(Alignment.Center).size(222.dp) - ) - } - - // User name - val text = viewModel.name.load() - if (text != null) { - // We use the box to center-align the text vertically as that is not possible with Text - // alone. - Box( - modifier = Modifier.size(width = 222.dp, height = 48.dp), - ) { - Text( - text = text, - style = MaterialTheme.typography.titleLarge, - color = colorResource(com.android.internal.R.color.system_neutral1_50), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.align(Alignment.Center), - ) - } - } - } -} - -@Composable -private fun UserItemBackground( - modifier: Modifier = Modifier, -) { - Image( - painter = ColorPainter(LocalAndroidColorScheme.current.colorBackground), - contentDescription = null, - modifier = modifier.clip(CircleShape), - ) -} - -@Composable -private fun UserItemIcon( - image: Drawable, - isSelectionMarkerVisible: Boolean, - modifier: Modifier = Modifier, -) { - Image( - bitmap = image.toBitmap().asImageBitmap(), - contentDescription = null, - modifier = - if (isSelectionMarkerVisible) { - // Draws a ring - modifier.border( - width = 8.dp, - color = LocalAndroidColorScheme.current.colorAccentPrimary, - shape = CircleShape, - ) - } else { - modifier - } - .padding(16.dp) - .clip(CircleShape) - ) -} - -@Composable -private fun Buttons( - menuActions: List<UserActionViewModel>, - isOpenMenuButtonVisible: Boolean, - isMenuVisible: Boolean, - onMenuClosed: () -> Unit, - onOpenMenuButtonClicked: () -> Unit, - onCancelButtonClicked: () -> Unit, - modifier: Modifier = Modifier, -) { - Row( - modifier = modifier, - ) { - // Cancel button. - SysUiTextButton( - onClick = onCancelButtonClicked, - ) { - Text(stringResource(R.string.cancel)) - } - - // "Open menu" button. - if (isOpenMenuButtonVisible) { - Spacer(modifier = Modifier.width(8.dp)) - // To properly use a DropdownMenu in Compose, we need to wrap the button that opens it - // and the menu itself in a Box. - Box { - SysUiOutlinedButton( - onClick = onOpenMenuButtonClicked, - ) { - Text(stringResource(R.string.add)) - } - Menu( - viewModel = menuActions, - isMenuVisible = isMenuVisible, - onMenuClosed = onMenuClosed, - ) - } - } - } -} - -@Composable -private fun Menu( - viewModel: List<UserActionViewModel>, - isMenuVisible: Boolean, - onMenuClosed: () -> Unit, - modifier: Modifier = Modifier, -) { - val maxItemWidth = LocalConfiguration.current.screenWidthDp.dp / 4 - DropdownMenu( - expanded = isMenuVisible, - onDismissRequest = onMenuClosed, - modifier = - modifier.background( - color = MaterialTheme.colorScheme.inverseOnSurface, - ), - ) { - viewModel.forEachIndexed { index, action -> - MenuItem( - viewModel = action, - onClicked = { action.onClicked() }, - topPadding = - if (index == 0) { - 16.dp - } else { - 0.dp - }, - bottomPadding = - if (index == viewModel.size - 1) { - 16.dp - } else { - 0.dp - }, - modifier = Modifier.sizeIn(maxWidth = maxItemWidth), - ) - } - } -} - -@Composable -private fun MenuItem( - viewModel: UserActionViewModel, - onClicked: () -> Unit, - topPadding: Dp, - bottomPadding: Dp, - modifier: Modifier = Modifier, -) { - val context = LocalContext.current - val density = LocalDensity.current - - val icon = - remember(viewModel.iconResourceId) { - val drawable = - checkNotNull(AppCompatResources.getDrawable(context, viewModel.iconResourceId)) - val size = with(density) { 20.dp.toPx() }.toInt() - drawable - .toBitmap( - width = size, - height = size, - ) - .asImageBitmap() - } - - DropdownMenuItem( - text = { - Text( - text = stringResource(viewModel.textResourceId), - style = MaterialTheme.typography.bodyMedium, - ) - }, - onClick = onClicked, - leadingIcon = { - Spacer(modifier = Modifier.width(10.dp)) - Image( - bitmap = icon, - contentDescription = null, - ) - }, - modifier = - modifier - .heightIn( - min = 56.dp, - ) - .padding( - start = 18.dp, - end = 65.dp, - top = topPadding, - bottom = bottomPadding, - ), - ) -} diff --git a/packages/SystemUI/ktfmt_includes.txt b/packages/SystemUI/ktfmt_includes.txt index 06ea381d0c1d..7fc9a8392d12 100644 --- a/packages/SystemUI/ktfmt_includes.txt +++ b/packages/SystemUI/ktfmt_includes.txt @@ -16,7 +16,6 @@ -packages/SystemUI/checks/tests/com/android/systemui/lint/RegisterReceiverViaContextDetectorTest.kt -packages/SystemUI/checks/tests/com/android/systemui/lint/SoftwareBitmapDetectorTest.kt -packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt --packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt -packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSContainerController.kt -packages/SystemUI/shared/src/com/android/systemui/flags/Flag.kt -packages/SystemUI/shared/src/com/android/systemui/flags/FlagListenable.kt diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt index 89f5c2c80e29..66e44b9005de 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt @@ -70,10 +70,10 @@ interface ClockController { } /** Optional method for dumping debug information */ - fun dump(pw: PrintWriter) { } + fun dump(pw: PrintWriter) {} /** Optional method for debug logging */ - fun setLogBuffer(logBuffer: LogBuffer) { } + fun setLogBuffer(logBuffer: LogBuffer) {} } /** Interface for a specific clock face version rendered by the clock */ @@ -88,40 +88,37 @@ interface ClockFaceController { /** Events that should call when various rendering parameters change */ interface ClockEvents { /** Call every time tick */ - fun onTimeTick() { } + fun onTimeTick() {} /** Call whenever timezone changes */ - fun onTimeZoneChanged(timeZone: TimeZone) { } + fun onTimeZoneChanged(timeZone: TimeZone) {} /** Call whenever the text time format changes (12hr vs 24hr) */ - fun onTimeFormatChanged(is24Hr: Boolean) { } + fun onTimeFormatChanged(is24Hr: Boolean) {} /** Call whenever the locale changes */ - fun onLocaleChanged(locale: Locale) { } - - /** Call whenever font settings change */ - fun onFontSettingChanged() { } + fun onLocaleChanged(locale: Locale) {} /** Call whenever the color palette should update */ - fun onColorPaletteChanged(resources: Resources) { } + fun onColorPaletteChanged(resources: Resources) {} } /** Methods which trigger various clock animations */ interface ClockAnimations { /** Runs an enter animation (if any) */ - fun enter() { } + fun enter() {} /** Sets how far into AOD the device currently is. */ - fun doze(fraction: Float) { } + fun doze(fraction: Float) {} /** Sets how far into the folding animation the device is. */ - fun fold(fraction: Float) { } + fun fold(fraction: Float) {} /** Runs the battery animation (if any). */ - fun charge() { } + fun charge() {} /** Move the clock, for example, if the notification tray appears in split-shade mode. */ - fun onPositionUpdated(fromRect: Rect, toRect: Rect, fraction: Float) { } + fun onPositionUpdated(fromRect: Rect, toRect: Rect, fraction: Float) {} /** * Whether this clock has a custom position update animation. If true, the keyguard will call @@ -135,11 +132,26 @@ interface ClockAnimations { /** Events that have specific data about the related face */ interface ClockFaceEvents { /** Region Darkness specific to the clock face */ - fun onRegionDarknessChanged(isDark: Boolean) { } + fun onRegionDarknessChanged(isDark: Boolean) {} + + /** + * Call whenever font settings change. Pass in a target font size in pixels. The specific clock + * design is allowed to ignore this target size on a case-by-case basis. + */ + fun onFontSettingChanged(fontSizePx: Float) {} + + /** + * Target region information for the clock face. For small clock, this will match the bounds of + * the parent view mostly, but have a target height based on the height of the default clock. + * For large clocks, the parent view is the entire device size, but most clocks will want to + * render within the centered targetRect to avoid obstructing other elements. The specified + * targetRegion is relative to the parent view. + */ + fun onTargetRegionChanged(targetRegion: Rect?) {} } /** Some data about a clock design */ data class ClockMetadata( val clockId: ClockId, - val name: String + val name: String, ) diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml index c29714957318..b49afeef09f3 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml @@ -37,7 +37,6 @@ android:id="@+id/lockscreen_clock_view_large" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_marginTop="@dimen/keyguard_large_clock_top_margin" android:clipChildren="false" android:visibility="gone" /> diff --git a/packages/SystemUI/res/drawable/accessibility_window_magnification_button_bg.xml b/packages/SystemUI/res/drawable/accessibility_window_magnification_button_bg.xml new file mode 100644 index 000000000000..eefe36459f9c --- /dev/null +++ b/packages/SystemUI/res/drawable/accessibility_window_magnification_button_bg.xml @@ -0,0 +1,26 @@ +<!--
+ Copyright (C) 2022 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="oval">
+ <solid android:color="@color/accessibility_window_magnifier_button_bg" />
+ <size
+ android:width="40dp"
+ android:height="40dp"/>
+ <corners android:radius="2dp"/>
+ <stroke
+ android:color="@color/accessibility_window_magnifier_button_bg_stroke"
+ android:width="1dp" />
+ </shape>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_magnification_corner_bottom_left.xml b/packages/SystemUI/res/drawable/ic_magnification_corner_bottom_left.xml new file mode 100644 index 000000000000..d25cd6558116 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_magnification_corner_bottom_left.xml @@ -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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M0,24l0,-4l24,-0l0,4z" + android:strokeWidth="1" + android:fillColor="@color/accessibility_window_magnifier_corner_view_color" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + <path + android:pathData="M0,24l0,-24l4,-0l0,24z" + android:strokeWidth="1" + android:fillColor="@color/accessibility_window_magnifier_corner_view_color" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> +</vector> diff --git a/packages/SystemUI/res/drawable/ic_magnification_corner_bottom_right.xml b/packages/SystemUI/res/drawable/ic_magnification_corner_bottom_right.xml new file mode 100644 index 000000000000..bb6df3ac1b32 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_magnification_corner_bottom_right.xml @@ -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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M24,24l-4,-0l-0,-24l4,-0z" + android:strokeWidth="1" + android:fillColor="@color/accessibility_window_magnifier_corner_view_color" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + <path + android:pathData="M24,24l-24,-0l-0,-4l24,-0z" + android:strokeWidth="1" + android:fillColor="@color/accessibility_window_magnifier_corner_view_color" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> +</vector> diff --git a/packages/SystemUI/res/drawable/ic_magnification_corner_top_left.xml b/packages/SystemUI/res/drawable/ic_magnification_corner_top_left.xml new file mode 100644 index 000000000000..8f259309b747 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_magnification_corner_top_left.xml @@ -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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M0,0h4v24h-4z" + android:strokeWidth="1" + android:fillColor="@color/accessibility_window_magnifier_corner_view_color" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + <path + android:pathData="M0,0h24v4h-24z" + android:strokeWidth="1" + android:fillColor="@color/accessibility_window_magnifier_corner_view_color" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> +</vector> diff --git a/packages/SystemUI/res/drawable/ic_magnification_corner_top_right.xml b/packages/SystemUI/res/drawable/ic_magnification_corner_top_right.xml new file mode 100644 index 000000000000..291db44ba8ba --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_magnification_corner_top_right.xml @@ -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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M24,0l-0,4l-24,0l-0,-4z" + android:strokeWidth="1" + android:fillColor="@color/accessibility_window_magnifier_corner_view_color" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + <path + android:pathData="M24,0l-0,24l-4,0l-0,-24z" + android:strokeWidth="1" + android:fillColor="@color/accessibility_window_magnifier_corner_view_color" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> +</vector> diff --git a/packages/SystemUI/res/drawable/internet_dialog_selected_effect.xml b/packages/SystemUI/res/drawable/internet_dialog_selected_effect.xml new file mode 100644 index 000000000000..8f6b4c246ba4 --- /dev/null +++ b/packages/SystemUI/res/drawable/internet_dialog_selected_effect.xml @@ -0,0 +1,26 @@ +<?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. + --> + +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + android:color="?android:attr/colorControlHighlight"> + <item android:id="@android:id/mask"> + <shape android:shape="rectangle"> + <solid android:color="@android:color/white"/> + <corners android:radius="?android:attr/buttonCornerRadius"/> + </shape> + </item> +</ripple> diff --git a/packages/SystemUI/res/layout/internet_connectivity_dialog.xml b/packages/SystemUI/res/layout/internet_connectivity_dialog.xml index ae2537fe29f6..f14be410bf75 100644 --- a/packages/SystemUI/res/layout/internet_connectivity_dialog.xml +++ b/packages/SystemUI/res/layout/internet_connectivity_dialog.xml @@ -312,22 +312,15 @@ <LinearLayout android:id="@+id/see_all_layout" - android:layout_width="match_parent" + style="@style/InternetDialog.Network" android:layout_height="64dp" - android:clickable="true" - android:focusable="true" - android:background="?android:attr/selectableItemBackground" - android:gravity="center_vertical|center_horizontal" - android:orientation="horizontal" - android:paddingStart="22dp" - android:paddingEnd="22dp"> + android:paddingStart="20dp"> <FrameLayout android:layout_width="24dp" android:layout_height="24dp" android:clickable="false" - android:layout_gravity="center_vertical|start" - android:layout_marginStart="@dimen/internet_dialog_network_layout_margin"> + android:layout_gravity="center_vertical|start"> <ImageView android:id="@+id/arrow_forward" android:src="@drawable/ic_arrow_forward" diff --git a/packages/SystemUI/res/layout/media_session_view.xml b/packages/SystemUI/res/layout/media_session_view.xml index 9b8b611558fe..530db0d0304a 100644 --- a/packages/SystemUI/res/layout/media_session_view.xml +++ b/packages/SystemUI/res/layout/media_session_view.xml @@ -44,7 +44,7 @@ android:background="@drawable/qs_media_outline_album_bg" /> - <com.android.systemui.ripple.MultiRippleView + <com.android.systemui.surfaceeffects.ripple.MultiRippleView android:id="@+id/touch_ripple_view" android:layout_width="match_parent" android:layout_height="@dimen/qs_media_session_height_expanded" @@ -53,6 +53,15 @@ app:layout_constraintTop_toTopOf="@id/album_art" app:layout_constraintBottom_toBottomOf="@id/album_art" /> + <com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseView + android:id="@+id/turbulence_noise_view" + android:layout_width="match_parent" + android:layout_height="@dimen/qs_media_session_height_expanded" + app:layout_constraintStart_toStartOf="@id/album_art" + app:layout_constraintEnd_toEndOf="@id/album_art" + app:layout_constraintTop_toTopOf="@id/album_art" + app:layout_constraintBottom_toBottomOf="@id/album_art" /> + <!-- Guideline for output switcher --> <androidx.constraintlayout.widget.Guideline android:id="@+id/center_vertical_guideline" diff --git a/packages/SystemUI/res/layout/window_magnifier_view.xml b/packages/SystemUI/res/layout/window_magnifier_view.xml index 0bff47ccf722..0be732855dee 100644 --- a/packages/SystemUI/res/layout/window_magnifier_view.xml +++ b/packages/SystemUI/res/layout/window_magnifier_view.xml @@ -40,19 +40,22 @@ android:id="@+id/top_handle" android:layout_width="match_parent" android:layout_height="@dimen/magnification_border_drag_size" - android:layout_alignParentTop="true"/> + android:layout_alignParentTop="true" + android:accessibilityTraversalAfter="@id/left_handle"/> <View android:id="@+id/right_handle" android:layout_width="@dimen/magnification_border_drag_size" android:layout_height="match_parent" - android:layout_alignParentEnd="true"/> + android:layout_alignParentEnd="true" + android:accessibilityTraversalAfter="@id/top_handle"/> <View android:id="@+id/bottom_handle" android:layout_width="match_parent" android:layout_height="@dimen/magnification_border_drag_size" - android:layout_alignParentBottom="true"/> + android:layout_alignParentBottom="true" + android:accessibilityTraversalAfter="@id/right_handle"/> <SurfaceView android:id="@+id/surface_view" @@ -62,6 +65,58 @@ </RelativeLayout> <ImageView + android:id="@+id/top_right_corner" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="@dimen/magnification_outer_border_margin" + android:layout_gravity="right|top" + android:paddingTop="@dimen/magnifier_drag_handle_padding" + android:paddingEnd="@dimen/magnifier_drag_handle_padding" + android:scaleType="center" + android:contentDescription="@string/magnification_drag_corner_to_resize" + android:src="@drawable/ic_magnification_corner_top_right" + android:accessibilityTraversalAfter="@id/top_left_corner"/> + + <ImageView + android:id="@+id/top_left_corner" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="@dimen/magnification_outer_border_margin" + android:layout_gravity="left|top" + android:paddingTop="@dimen/magnifier_drag_handle_padding" + android:paddingStart="@dimen/magnifier_drag_handle_padding" + android:scaleType="center" + android:contentDescription="@string/magnification_drag_corner_to_resize" + android:src="@drawable/ic_magnification_corner_top_left" + android:accessibilityTraversalAfter="@id/bottom_handle"/> + + <ImageView + android:id="@+id/bottom_right_corner" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="@dimen/magnification_outer_border_margin" + android:layout_gravity="right|bottom" + android:paddingEnd="@dimen/magnifier_drag_handle_padding" + android:paddingBottom="@dimen/magnifier_drag_handle_padding" + android:scaleType="center" + android:contentDescription="@string/magnification_drag_corner_to_resize" + android:src="@drawable/ic_magnification_corner_bottom_right" + android:accessibilityTraversalAfter="@id/top_right_corner"/> + + <ImageView + android:id="@+id/bottom_left_corner" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="@dimen/magnification_outer_border_margin" + android:layout_gravity="left|bottom" + android:paddingStart="@dimen/magnifier_drag_handle_padding" + android:paddingBottom="@dimen/magnifier_drag_handle_padding" + android:scaleType="center" + android:contentDescription="@string/magnification_drag_corner_to_resize" + android:src="@drawable/ic_magnification_corner_bottom_left" + android:accessibilityTraversalAfter="@id/bottom_right_corner"/> + + <ImageView android:id="@+id/drag_handle" android:layout_width="@dimen/magnification_drag_view_size" android:layout_height="@dimen/magnification_drag_view_size" diff --git a/packages/SystemUI/res/layout/wireless_charging_layout.xml b/packages/SystemUI/res/layout/wireless_charging_layout.xml index 887e3e715369..f1bc88370071 100644 --- a/packages/SystemUI/res/layout/wireless_charging_layout.xml +++ b/packages/SystemUI/res/layout/wireless_charging_layout.xml @@ -22,7 +22,7 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - <com.android.systemui.ripple.RippleView + <com.android.systemui.surfaceeffects.ripple.RippleView android:id="@+id/wireless_charging_ripple" android:layout_width="match_parent" android:layout_height="match_parent"/> diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index 55b59b63c2f9..6b4bea190977 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -172,6 +172,10 @@ <color name="accessibility_magnifier_bg">#FCFCFC</color> <color name="accessibility_magnifier_bg_stroke">#E0E0E0</color> <color name="accessibility_magnifier_icon_color">#252525</color> + <color name="accessibility_window_magnifier_button_bg">#0680FD</color> + <color name="accessibility_window_magnifier_icon_color">#FAFAFA</color> + <color name="accessibility_window_magnifier_button_bg_stroke">#252525</color> + <color name="accessibility_window_magnifier_corner_view_color">#0680FD</color> <!-- Volume dialog colors --> <color name="volume_dialog_background_color">@android:color/transparent</color> diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index ce9829b318cd..55d637916d0c 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 ad45471419a8..6577b07f7139 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 6824d7f17e13..eb291fd5f12a 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] --> @@ -405,8 +405,8 @@ <string name="keyguard_face_failed">Can\u2019t recognize face</string> <!-- Message shown to suggest using fingerprint sensor to authenticate after another biometric failed. [CHAR LIMIT=25] --> <string name="keyguard_suggest_fingerprint">Use fingerprint instead</string> - <!-- Message shown to inform the user that face unlock is not available. [CHAR LIMIT=65] --> - <string name="keyguard_face_unlock_unavailable">Face unlock unavailable.</string> + <!-- Message shown to inform the user that face unlock is not available. [CHAR LIMIT=59] --> + <string name="keyguard_face_unlock_unavailable">Face Unlock unavailable</string> <!-- Content description of the bluetooth icon when connected for accessibility (not shown on the screen). [CHAR LIMIT=NONE] --> <string name="accessibility_bluetooth_connected">Bluetooth connected.</string> @@ -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> @@ -2231,6 +2237,8 @@ <string name="magnification_mode_switch_state_window">Magnify part of screen</string> <!-- Click action label for magnification switch. [CHAR LIMIT=NONE] --> <string name="magnification_mode_switch_click_label">Switch</string> + <!-- Label of the corner of a rectangle that you can tap and drag to resize the magnification area. [CHAR LIMIT=NONE] --> + <string name="magnification_drag_corner_to_resize">Drag corner to resize</string> <!-- Title of the magnification option button allow diagonal scrolling [CHAR LIMIT=NONE]--> <string name="accessibility_allow_diagonal_scrolling">Allow diagonal scrolling</string> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 4e4bfe2ee8f3..ae80070dfa97 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -1091,7 +1091,7 @@ <item name="android:orientation">horizontal</item> <item name="android:focusable">true</item> <item name="android:clickable">true</item> - <item name="android:background">?android:attr/selectableItemBackground</item> + <item name="android:background">@drawable/internet_dialog_selected_effect</item> </style> <style name="InternetDialog.NetworkTitle"> diff --git a/packages/SystemUI/res/xml/media_session_collapsed.xml b/packages/SystemUI/res/xml/media_session_collapsed.xml index 148e5ec1606f..1eb621e0368b 100644 --- a/packages/SystemUI/res/xml/media_session_collapsed.xml +++ b/packages/SystemUI/res/xml/media_session_collapsed.xml @@ -44,6 +44,16 @@ app:layout_constraintTop_toTopOf="@+id/album_art" app:layout_constraintBottom_toBottomOf="@+id/album_art" /> + <!-- Turbulence noise must have the same constraint as the album art. --> + <Constraint + android:id="@+id/turbulence_noise_view" + android:layout_width="match_parent" + android:layout_height="@dimen/qs_media_session_height_collapsed" + app:layout_constraintStart_toStartOf="@+id/album_art" + app:layout_constraintEnd_toEndOf="@+id/album_art" + app:layout_constraintTop_toTopOf="@+id/album_art" + app:layout_constraintBottom_toBottomOf="@+id/album_art" /> + <Constraint android:id="@+id/header_title" android:layout_width="wrap_content" diff --git a/packages/SystemUI/res/xml/media_session_expanded.xml b/packages/SystemUI/res/xml/media_session_expanded.xml index ac484d7dde8e..64c2ef1fc915 100644 --- a/packages/SystemUI/res/xml/media_session_expanded.xml +++ b/packages/SystemUI/res/xml/media_session_expanded.xml @@ -37,6 +37,16 @@ app:layout_constraintTop_toTopOf="@+id/album_art" app:layout_constraintBottom_toBottomOf="@+id/album_art" /> + <!-- Turbulence noise must have the same constraint as the album art. --> + <Constraint + android:id="@+id/turbulence_noise_view" + android:layout_width="match_parent" + android:layout_height="@dimen/qs_media_session_height_expanded" + app:layout_constraintStart_toStartOf="@+id/album_art" + app:layout_constraintEnd_toEndOf="@+id/album_art" + app:layout_constraintTop_toTopOf="@+id/album_art" + app:layout_constraintBottom_toBottomOf="@+id/album_art" /> + <Constraint android:id="@+id/header_title" android:layout_width="wrap_content" diff --git a/packages/SystemUI/res/xml/qqs_header.xml b/packages/SystemUI/res/xml/qqs_header.xml index af4be1ade656..5d3650ccc8e6 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,14 +57,16 @@ <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_constraintHeight_min="@dimen/new_qs_header_non_clickable_element_height" app:layout_constraintStart_toEndOf="@id/date" app:layout_constraintEnd_toStartOf="@id/batteryRemainingIcon" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintHorizontal_bias="1" + app:layout_constraintHorizontal_chainStyle="packed" /> </Constraint> @@ -80,12 +82,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 d8a4e7752960..982c422f1fda 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/Android.bp b/packages/SystemUI/shared/Android.bp index 485a0d320bb9..8a0fca03fe3a 100644 --- a/packages/SystemUI/shared/Android.bp +++ b/packages/SystemUI/shared/Android.bp @@ -63,9 +63,6 @@ android_library { resource_dirs: [ "res", ], - optimize: { - proguard_flags_files: ["proguard.flags"], - }, min_sdk_version: "current", plugins: ["dagger2-compiler"], kotlincflags: ["-Xjvm-default=enable"], diff --git a/packages/SystemUI/shared/proguard.flags b/packages/SystemUI/shared/proguard.flags deleted file mode 100644 index 5eda04500190..000000000000 --- a/packages/SystemUI/shared/proguard.flags +++ /dev/null @@ -1,4 +0,0 @@ -# Retain signatures of TypeToken and its subclasses for gson usage in ClockRegistry --keepattributes Signature --keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken --keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken
\ No newline at end of file diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt index ca780c8dd3c9..599cd23f6616 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt @@ -20,6 +20,7 @@ import android.graphics.Rect import android.icu.text.NumberFormat import android.util.TypedValue import android.view.LayoutInflater +import android.view.View import android.widget.FrameLayout import androidx.annotation.VisibleForTesting import com.android.systemui.plugins.ClockAnimations @@ -80,7 +81,7 @@ class DefaultClockController( } override fun initialize(resources: Resources, dozeFraction: Float, foldFraction: Float) { - largeClock.recomputePadding() + largeClock.recomputePadding(null) animations = DefaultClockAnimations(dozeFraction, foldFraction) events.onColorPaletteChanged(resources) events.onTimeZoneChanged(TimeZone.getDefault()) @@ -101,6 +102,7 @@ class DefaultClockController( // MAGENTA is a placeholder, and will be assigned correctly in initialize private var currentColor = Color.MAGENTA private var isRegionDark = false + protected var targetRegion: Rect? = null init { view.setColors(currentColor, currentColor) @@ -112,8 +114,20 @@ class DefaultClockController( this@DefaultClockFaceController.isRegionDark = isRegionDark updateColor() } + + override fun onTargetRegionChanged(targetRegion: Rect?) { + this@DefaultClockFaceController.targetRegion = targetRegion + recomputePadding(targetRegion) + } + + override fun onFontSettingChanged(fontSizePx: Float) { + view.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSizePx) + recomputePadding(targetRegion) + } } + open fun recomputePadding(targetRegion: Rect?) {} + fun updateColor() { val color = if (isRegionDark) { @@ -135,9 +149,16 @@ class DefaultClockController( inner class LargeClockFaceController( view: AnimatableClockView, ) : DefaultClockFaceController(view) { - fun recomputePadding() { + override fun recomputePadding(targetRegion: Rect?) { + // We center the view within the targetRegion instead of within the parent + // view by computing the difference and adding that to the padding. + val parent = view.parent + val yDiff = + if (targetRegion != null && parent is View && parent.isLaidOut()) + targetRegion.centerY() - parent.height / 2f + else 0f val lp = view.getLayoutParams() as FrameLayout.LayoutParams - lp.topMargin = (-0.5f * view.bottom).toInt() + lp.topMargin = (-0.5f * view.bottom + yDiff).toInt() view.setLayoutParams(lp) } @@ -155,18 +176,6 @@ class DefaultClockController( override fun onTimeZoneChanged(timeZone: TimeZone) = clocks.forEach { it.onTimeZoneChanged(timeZone) } - override fun onFontSettingChanged() { - smallClock.view.setTextSize( - TypedValue.COMPLEX_UNIT_PX, - resources.getDimensionPixelSize(R.dimen.small_clock_text_size).toFloat() - ) - largeClock.view.setTextSize( - TypedValue.COMPLEX_UNIT_PX, - resources.getDimensionPixelSize(R.dimen.large_clock_text_size).toFloat() - ) - largeClock.recomputePadding() - } - override fun onColorPaletteChanged(resources: Resources) { largeClock.updateColor() smallClock.updateColor() 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 000000000000..c2658a9e61b1 --- /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/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index c9b8712bdde9..87e9d5630b74 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -26,6 +26,7 @@ import android.view.View import androidx.annotation.VisibleForTesting import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.R import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main @@ -43,6 +44,11 @@ import com.android.systemui.shared.regionsampling.RegionSampler import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback import com.android.systemui.statusbar.policy.ConfigurationController +import java.io.PrintWriter +import java.util.Locale +import java.util.TimeZone +import java.util.concurrent.Executor +import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.Job @@ -50,11 +56,6 @@ import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch -import java.io.PrintWriter -import java.util.Locale -import java.util.TimeZone -import java.util.concurrent.Executor -import javax.inject.Inject /** * Controller for a Clock provided by the registry and used on the keyguard. Instantiated by @@ -84,6 +85,7 @@ open class ClockEventController @Inject constructor( value.initialize(resources, dozeAmount, 0f) updateRegionSamplers(value) + updateFontSizes() } } @@ -150,7 +152,7 @@ open class ClockEventController @Inject constructor( mainExecutor, bgExecutor, regionSamplingEnabled, - updateFun = { updateColors() } ) + updateColors) } var smallRegionSampler: RegionSampler? = null @@ -166,7 +168,7 @@ open class ClockEventController @Inject constructor( } override fun onDensityOrFontScaleChanged() { - clock?.events?.onFontSettingChanged() + updateFontSizes() } } @@ -251,6 +253,13 @@ open class ClockEventController @Inject constructor( largeRegionSampler?.stopRegionSampler() } + private fun updateFontSizes() { + clock?.smallClock?.events?.onFontSettingChanged( + resources.getDimensionPixelSize(R.dimen.small_clock_text_size).toFloat()) + clock?.largeClock?.events?.onFontSettingChanged( + resources.getDimensionPixelSize(R.dimen.large_clock_text_size).toFloat()) + } + /** * Dump information for debugging */ diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java index 8ebad6c0fdbf..40423cd9ac2c 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java @@ -5,6 +5,7 @@ import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.content.Context; +import android.graphics.Rect; import android.util.AttributeSet; import android.util.Log; import android.view.View; @@ -22,6 +23,7 @@ import com.android.systemui.plugins.ClockController; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; + /** * Switch to show plugin clock when plugin is connected, otherwise it will show default clock. */ @@ -46,6 +48,7 @@ public class KeyguardClockSwitch extends RelativeLayout { */ private FrameLayout mSmallClockFrame; private FrameLayout mLargeClockFrame; + private ClockController mClock; private View mStatusArea; private int mSmartspaceTopOffset; @@ -95,6 +98,8 @@ public class KeyguardClockSwitch extends RelativeLayout { } void setClock(ClockController clock, int statusBarState) { + mClock = clock; + // Disconnect from existing plugin. mSmallClockFrame.removeAllViews(); mLargeClockFrame.removeAllViews(); @@ -108,6 +113,35 @@ public class KeyguardClockSwitch extends RelativeLayout { Log.i(TAG, "Attached new clock views to switch"); mSmallClockFrame.addView(clock.getSmallClock().getView()); mLargeClockFrame.addView(clock.getLargeClock().getView()); + updateClockTargetRegions(); + } + + void updateClockTargetRegions() { + if (mClock != null) { + if (mSmallClockFrame.isLaidOut()) { + int targetHeight = getResources() + .getDimensionPixelSize(R.dimen.small_clock_text_size); + mClock.getSmallClock().getEvents().onTargetRegionChanged(new Rect( + mSmallClockFrame.getLeft(), + mSmallClockFrame.getTop(), + mSmallClockFrame.getRight(), + mSmallClockFrame.getTop() + targetHeight)); + } + + if (mLargeClockFrame.isLaidOut()) { + int largeClockTopMargin = getResources() + .getDimensionPixelSize(R.dimen.keyguard_large_clock_top_margin); + int targetHeight = getResources() + .getDimensionPixelSize(R.dimen.large_clock_text_size) * 2; + int top = mLargeClockFrame.getHeight() / 2 - targetHeight / 2 + + largeClockTopMargin / 2; + mClock.getLargeClock().getEvents().onTargetRegionChanged(new Rect( + mLargeClockFrame.getLeft(), + top, + mLargeClockFrame.getRight(), + top + targetHeight)); + } + } } private void updateClockViews(boolean useLargeClock, boolean animate) { @@ -214,6 +248,10 @@ public class KeyguardClockSwitch extends RelativeLayout { protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); + if (changed) { + post(() -> updateClockTargetRegions()); + } + if (mDisplayedClockSize != null && !mChildrenAreLaidOut) { post(() -> updateClockViews(mDisplayedClockSize == LARGE, mAnimateOnLayout)); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java index ace942de1221..e6aae9bc461a 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java @@ -77,7 +77,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS @KeyguardClockSwitch.ClockSize private int mCurrentClockSize = SMALL; - private int mKeyguardClockTopMargin = 0; + private int mKeyguardSmallClockTopMargin = 0; private final ClockRegistry.ClockChangeListener mClockChangedListener; private ViewGroup mStatusArea; @@ -162,7 +162,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS mClockRegistry.registerClockChangeListener(mClockChangedListener); setClock(mClockRegistry.createCurrentClock()); mClockEventController.registerListeners(mView); - mKeyguardClockTopMargin = + mKeyguardSmallClockTopMargin = mView.getResources().getDimensionPixelSize(R.dimen.keyguard_clock_top_margin); if (mOnlyClock) { @@ -244,10 +244,12 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS */ public void onDensityOrFontScaleChanged() { mView.onDensityOrFontScaleChanged(); - mKeyguardClockTopMargin = + mKeyguardSmallClockTopMargin = mView.getResources().getDimensionPixelSize(R.dimen.keyguard_clock_top_margin); + mView.updateClockTargetRegions(); } + /** * Set which clock should be displayed on the keyguard. The other one will be automatically * hidden. @@ -327,7 +329,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS return frameHeight / 2 + clockHeight / 2; } else { int clockHeight = clock.getSmallClock().getView().getHeight(); - return clockHeight + statusBarHeaderHeight + mKeyguardClockTopMargin; + return clockHeight + statusBarHeaderHeight + mKeyguardSmallClockTopMargin; } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java index db64f05ccbea..8fa7b11e2664 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java @@ -68,7 +68,7 @@ public class KeyguardHostViewController extends ViewController<KeyguardHostView> 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 ffcf42f74a55..5c4126eeb93a 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java @@ -727,6 +727,11 @@ public class KeyguardSecurityContainer extends ConstraintLayout { 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 @@ public class KeyguardSecurityContainer extends ConstraintLayout { /** 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 @@ public class KeyguardSecurityContainer extends ConstraintLayout { 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 @@ public class KeyguardSecurityContainer extends ConstraintLayout { } @Override + public void onDensityOrFontScaleChanged() { + mView.removeView(mUserSwitcherViewGroup); + inflateUserSwitcher(); + } + + @Override public void onDestroy() { mUserSwitcherController.removeUserSwitchCallback(mUserSwitchCallback); } @@ -1145,6 +1154,15 @@ public class KeyguardSecurityContainer extends ConstraintLayout { } } + 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 7a49926f8ef1..01be33e1e156 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java @@ -251,6 +251,11 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard public void onUiModeChanged() { reloadColors(); } + + @Override + public void onDensityOrFontScaleChanged() { + KeyguardSecurityContainerController.this.onDensityOrFontScaleChanged(); + } }; private boolean mBouncerVisible = false; private final KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback = @@ -727,6 +732,14 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard 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 bddf4b09ebb3..25afe11ac536 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipperController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipperController.java @@ -83,6 +83,13 @@ public class KeyguardSecurityViewFlipperController } } + /** 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 83e23bd52f19..8b9823be65fd 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 @@ public class KeyguardStatusView extends GridLayout { 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 055f376989d3..d694dc0d7bf6 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -144,6 +144,7 @@ import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.log.SessionTracker; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.settings.UserTracker; import com.android.systemui.shared.system.TaskStackChangeListener; import com.android.systemui.shared.system.TaskStackChangeListeners; import com.android.systemui.statusbar.StatusBarState; @@ -263,6 +264,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab "com.android.settings", "com.android.settings.FallbackHome"); private final Context mContext; + private final UserTracker mUserTracker; private final KeyguardUpdateMonitorLogger mLogger; private final boolean mIsPrimaryUser; private final AuthController mAuthController; @@ -470,19 +472,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab 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 +483,17 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } } } - - 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); } } } @@ -856,13 +850,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mHandler.removeCallbacks(mFpCancelNotReceived); } try { - final int userId; - try { - userId = ActivityManager.getService().getCurrentUser().id; - } catch (RemoteException e) { - mLogger.logException(e, "Failed to get current user id"); - return; - } + final int userId = mUserTracker.getUserId(); if (userId != authUserId) { mLogger.logFingerprintAuthForWrongUser(authUserId); return; @@ -1080,13 +1068,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mLogger.d("Aborted successful auth because device is going to sleep."); return; } - final int userId; - try { - userId = ActivityManager.getService().getCurrentUser().id; - } catch (RemoteException e) { - mLogger.logException(e, "Failed to get current user id"); - return; - } + final int userId = mUserTracker.getUserId(); if (userId != authUserId) { mLogger.logFaceAuthForWrongUser(authUserId); return; @@ -1936,6 +1918,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab @Inject protected KeyguardUpdateMonitor( Context context, + UserTracker userTracker, @Main Looper mainLooper, BroadcastDispatcher broadcastDispatcher, SecureSettings secureSettings, @@ -1968,6 +1951,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab FaceWakeUpTriggersConfig faceWakeUpTriggersConfig) { mContext = context; mSubscriptionManager = subscriptionManager; + mUserTracker = userTracker; mTelephonyListenerManager = telephonyListenerManager; mDeviceProvisioned = isDeviceProvisionedInSettingsDb(); mStrongAuthTracker = new StrongAuthTracker(context, this::notifyStrongAuthStateChanged, @@ -2200,7 +2184,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener); mIsPrimaryUser = mUserManager.isPrimaryUser(); - int user = ActivityManager.getCurrentUser(); + int user = mUserTracker.getUserId(); mUserIsUnlocked.put(user, mUserManager.isUserUnlocked(user)); mLogoutEnabled = mDevicePolicyManager.isLogoutEnabled(); updateSecondaryLockscreenRequirement(user); @@ -3826,7 +3810,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab pw.println(" " + subId + "=" + mServiceStates.get(subId)); } if (mFpm != null && mFpm.isHardwareDetected()) { - final int userId = ActivityManager.getCurrentUser(); + final int userId = mUserTracker.getUserId(); final int strongAuthFlags = mStrongAuthTracker.getStrongAuthForUser(userId); BiometricAuthenticated fingerprint = mUserFingerprintAuthenticated.get(userId); pw.println(" Fingerprint state (user=" + userId + ")"); @@ -3866,7 +3850,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } } if (mFaceManager != null && mFaceManager.isHardwareDetected()) { - final int userId = ActivityManager.getCurrentUser(); + final int userId = mUserTracker.getUserId(); final int strongAuthFlags = mStrongAuthTracker.getStrongAuthForUser(userId); BiometricAuthenticated face = mUserFaceAuthenticated.get(userId); pw.println(" Face authentication state (user=" + userId + ")"); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java index c06e1dcf08c2..c5142f309a46 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java @@ -174,14 +174,12 @@ public class KeyguardUpdateMonitorCallback { 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/keyguard/clock/ClockManager.java b/packages/SystemUI/src/com/android/keyguard/clock/ClockManager.java index 9a0bfc19f848..ad9609f9ec42 100644 --- a/packages/SystemUI/src/com/android/keyguard/clock/ClockManager.java +++ b/packages/SystemUI/src/com/android/keyguard/clock/ClockManager.java @@ -29,17 +29,17 @@ import android.util.ArrayMap; import android.util.DisplayMetrics; import android.view.LayoutInflater; +import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; -import androidx.lifecycle.Observer; -import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dock.DockManager; import com.android.systemui.dock.DockManager.DockEventListener; import com.android.systemui.plugins.ClockPlugin; import com.android.systemui.plugins.PluginListener; -import com.android.systemui.settings.CurrentUserObservable; +import com.android.systemui.settings.UserTracker; import com.android.systemui.shared.plugins.PluginManager; import java.util.ArrayList; @@ -47,6 +47,7 @@ import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.concurrent.Executor; import java.util.function.Supplier; import javax.inject.Inject; @@ -69,7 +70,8 @@ public final class ClockManager { private final ContentResolver mContentResolver; private final SettingsWrapper mSettingsWrapper; private final Handler mMainHandler = new Handler(Looper.getMainLooper()); - private final CurrentUserObservable mCurrentUserObservable; + private final UserTracker mUserTracker; + private final Executor mMainExecutor; /** * Observe settings changes to know when to switch the clock face. @@ -80,7 +82,7 @@ public final class ClockManager { public void onChange(boolean selfChange, Collection<Uri> uris, int flags, int userId) { if (Objects.equals(userId, - mCurrentUserObservable.getCurrentUser().getValue())) { + mUserTracker.getUserId())) { reload(); } } @@ -89,7 +91,13 @@ public final class ClockManager { /** * Observe user changes and react by potentially loading the custom clock for the new user. */ - private final Observer<Integer> mCurrentUserObserver = (newUserId) -> reload(); + private final UserTracker.Callback mUserChangedCallback = + new UserTracker.Callback() { + @Override + public void onUserChanged(int newUser, @NonNull Context userContext) { + reload(); + } + }; private final PluginManager mPluginManager; @Nullable private final DockManager mDockManager; @@ -129,22 +137,24 @@ public final class ClockManager { @Inject public ClockManager(Context context, LayoutInflater layoutInflater, PluginManager pluginManager, SysuiColorExtractor colorExtractor, - @Nullable DockManager dockManager, BroadcastDispatcher broadcastDispatcher) { + @Nullable DockManager dockManager, UserTracker userTracker, + @Main Executor mainExecutor) { this(context, layoutInflater, pluginManager, colorExtractor, - context.getContentResolver(), new CurrentUserObservable(broadcastDispatcher), + context.getContentResolver(), userTracker, mainExecutor, new SettingsWrapper(context.getContentResolver()), dockManager); } @VisibleForTesting ClockManager(Context context, LayoutInflater layoutInflater, PluginManager pluginManager, SysuiColorExtractor colorExtractor, - ContentResolver contentResolver, CurrentUserObservable currentUserObservable, + ContentResolver contentResolver, UserTracker userTracker, Executor mainExecutor, SettingsWrapper settingsWrapper, DockManager dockManager) { mContext = context; mPluginManager = pluginManager; mContentResolver = contentResolver; mSettingsWrapper = settingsWrapper; - mCurrentUserObservable = currentUserObservable; + mUserTracker = userTracker; + mMainExecutor = mainExecutor; mDockManager = dockManager; mPreviewClocks = new AvailableClocks(); @@ -226,7 +236,7 @@ public final class ClockManager { mContentResolver.registerContentObserver( Settings.Secure.getUriFor(Settings.Secure.DOCKED_CLOCK_FACE), false, mContentObserver, UserHandle.USER_ALL); - mCurrentUserObservable.getCurrentUser().observeForever(mCurrentUserObserver); + mUserTracker.addCallback(mUserChangedCallback, mMainExecutor); if (mDockManager != null) { mDockManager.addListener(mDockEventListener); } @@ -235,7 +245,7 @@ public final class ClockManager { private void unregister() { mPluginManager.removePluginListener(mPreviewClocks); mContentResolver.unregisterContentObserver(mContentObserver); - mCurrentUserObservable.getCurrentUser().removeObserver(mCurrentUserObserver); + mUserTracker.removeCallback(mUserChangedCallback); if (mDockManager != null) { mDockManager.removeListener(mDockEventListener); } @@ -363,7 +373,7 @@ public final class ClockManager { ClockPlugin plugin = null; if (ClockManager.this.isDocked()) { final String name = mSettingsWrapper.getDockedClockFace( - mCurrentUserObservable.getCurrentUser().getValue()); + mUserTracker.getUserId()); if (name != null) { plugin = mClocks.get(name); if (plugin != null) { @@ -372,7 +382,7 @@ public final class ClockManager { } } final String name = mSettingsWrapper.getLockScreenCustomClockFace( - mCurrentUserObservable.getCurrentUser().getValue()); + mUserTracker.getUserId()); if (name != null) { plugin = mClocks.get(name); } diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt index 32ce537ea25a..9e58500c7206 100644 --- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt +++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt @@ -18,14 +18,11 @@ package com.android.keyguard.logging import com.android.systemui.log.dagger.KeyguardLog import com.android.systemui.plugins.log.LogBuffer -import com.android.systemui.plugins.log.LogLevel import com.android.systemui.plugins.log.LogLevel.DEBUG import com.android.systemui.plugins.log.LogLevel.ERROR import com.android.systemui.plugins.log.LogLevel.INFO import com.android.systemui.plugins.log.LogLevel.VERBOSE import com.android.systemui.plugins.log.LogLevel.WARNING -import com.android.systemui.plugins.log.MessageInitializer -import com.android.systemui.plugins.log.MessagePrinter import com.google.errorprone.annotations.CompileTimeConstant import javax.inject.Inject @@ -37,18 +34,16 @@ private const val TAG = "KeyguardLog" * an overkill. */ class KeyguardLogger @Inject constructor(@KeyguardLog private val buffer: LogBuffer) { - fun d(@CompileTimeConstant msg: String) = log(msg, DEBUG) + fun d(@CompileTimeConstant msg: String) = buffer.log(TAG, DEBUG, msg) - fun e(@CompileTimeConstant msg: String) = log(msg, ERROR) + fun e(@CompileTimeConstant msg: String) = buffer.log(TAG, ERROR, msg) - fun v(@CompileTimeConstant msg: String) = log(msg, VERBOSE) + fun v(@CompileTimeConstant msg: String) = buffer.log(TAG, VERBOSE, msg) - fun w(@CompileTimeConstant msg: String) = log(msg, WARNING) + fun w(@CompileTimeConstant msg: String) = buffer.log(TAG, WARNING, msg) - fun log(msg: String, level: LogLevel) = buffer.log(TAG, level, msg) - - private fun debugLog(messageInitializer: MessageInitializer, messagePrinter: MessagePrinter) { - buffer.log(TAG, DEBUG, messageInitializer, messagePrinter) + fun logException(ex: Exception, @CompileTimeConstant logMsg: String) { + buffer.log(TAG, ERROR, {}, { logMsg }, exception = ex) } fun v(msg: String, arg: Any) { @@ -61,17 +56,24 @@ class KeyguardLogger @Inject constructor(@KeyguardLog private val buffer: LogBuf // TODO: remove after b/237743330 is fixed fun logStatusBarCalculatedAlpha(alpha: Float) { - debugLog({ double1 = alpha.toDouble() }, { "Calculated new alpha: $double1" }) + buffer.log(TAG, DEBUG, { double1 = alpha.toDouble() }, { "Calculated new alpha: $double1" }) } // TODO: remove after b/237743330 is fixed fun logStatusBarExplicitAlpha(alpha: Float) { - debugLog({ double1 = alpha.toDouble() }, { "new mExplicitAlpha value: $double1" }) + buffer.log( + TAG, + DEBUG, + { double1 = alpha.toDouble() }, + { "new mExplicitAlpha value: $double1" } + ) } // TODO: remove after b/237743330 is fixed fun logStatusBarAlphaVisibility(visibility: Int, alpha: Float, state: String) { - debugLog( + buffer.log( + TAG, + DEBUG, { int1 = visibility double1 = alpha.toDouble() @@ -80,4 +82,22 @@ class KeyguardLogger @Inject constructor(@KeyguardLog private val buffer: LogBuf { "changing visibility to $int1 with alpha $double1 in state: $str1" } ) } + + @JvmOverloads + fun logBiometricMessage( + @CompileTimeConstant context: String, + msgId: Int? = null, + msg: String? = null + ) { + buffer.log( + TAG, + DEBUG, + { + str1 = context + str2 = "$msgId" + str3 = msg + }, + { "$str1 msgId: $str2 msg: $str3" } + ) + } } diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java index a5fdc68226e8..51bcd6b18936 100644 --- a/packages/SystemUI/src/com/android/systemui/Dependency.java +++ b/packages/SystemUI/src/com/android/systemui/Dependency.java @@ -70,6 +70,7 @@ import com.android.systemui.qs.ReduceBrightColorsController; import com.android.systemui.qs.tiles.dialog.InternetDialogFactory; import com.android.systemui.recents.OverviewProxyService; import com.android.systemui.screenrecord.RecordingController; +import com.android.systemui.settings.UserTracker; import com.android.systemui.shade.ShadeController; import com.android.systemui.shared.plugins.PluginManager; import com.android.systemui.shared.system.ActivityManagerWrapper; @@ -357,6 +358,7 @@ public class Dependency { @Inject Lazy<GroupExpansionManager> mGroupExpansionManagerLazy; @Inject Lazy<SystemUIDialogManager> mSystemUIDialogManagerLazy; @Inject Lazy<DialogLaunchAnimator> mDialogLaunchAnimatorLazy; + @Inject Lazy<UserTracker> mUserTrackerLazy; @Inject public Dependency() { @@ -564,6 +566,7 @@ public class Dependency { mProviders.put(GroupExpansionManager.class, mGroupExpansionManagerLazy::get); mProviders.put(SystemUIDialogManager.class, mSystemUIDialogManagerLazy::get); mProviders.put(DialogLaunchAnimator.class, mDialogLaunchAnimatorLazy::get); + mProviders.put(UserTracker.class, mUserTrackerLazy::get); Dependency.setInstance(this); } diff --git a/packages/SystemUI/src/com/android/systemui/DisplayCutoutBaseView.kt b/packages/SystemUI/src/com/android/systemui/DisplayCutoutBaseView.kt index 5d52056d8b17..90ecb466b5d0 100644 --- a/packages/SystemUI/src/com/android/systemui/DisplayCutoutBaseView.kt +++ b/packages/SystemUI/src/com/android/systemui/DisplayCutoutBaseView.kt @@ -169,7 +169,7 @@ open class DisplayCutoutBaseView : View, RegionInterceptableView { return } cutoutPath.reset() - display.getDisplayInfo(displayInfo) + context.display?.getDisplayInfo(displayInfo) displayInfo.displayCutout?.cutoutPath?.let { path -> cutoutPath.set(path) } invalidate() } diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java index 11d579d481c1..7e3b1389792c 100644 --- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java +++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java @@ -26,7 +26,6 @@ import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_M import android.annotation.IdRes; import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.ActivityManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -170,6 +169,7 @@ public class ScreenDecorations implements CoreStartable, Tunable , Dumpable { private Display.Mode mDisplayMode; @VisibleForTesting protected DisplayInfo mDisplayInfo = new DisplayInfo(); + private DisplayCutout mDisplayCutout; @VisibleForTesting protected void showCameraProtection(@NonNull Path protectionPath, @NonNull Rect bounds) { @@ -384,6 +384,7 @@ public class ScreenDecorations implements CoreStartable, Tunable , Dumpable { mRotation = mDisplayInfo.rotation; mDisplayMode = mDisplayInfo.getMode(); mDisplayUniqueId = mDisplayInfo.uniqueId; + mDisplayCutout = mDisplayInfo.displayCutout; mRoundedCornerResDelegate = new RoundedCornerResDelegate(mContext.getResources(), mDisplayUniqueId); mRoundedCornerResDelegate.setPhysicalPixelDisplaySizeRatio( @@ -899,7 +900,7 @@ public class ScreenDecorations implements CoreStartable, Tunable , Dumpable { private final BroadcastReceiver mUserSwitchIntentReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - int newUserId = ActivityManager.getCurrentUser(); + int newUserId = mUserTracker.getUserId(); if (DEBUG) { Log.d(TAG, "UserSwitched newUserId=" + newUserId); } @@ -1022,7 +1023,8 @@ public class ScreenDecorations implements CoreStartable, Tunable , Dumpable { mRoundedCornerResDelegate.dump(pw, args); } - private void updateConfiguration() { + @VisibleForTesting + void updateConfiguration() { Preconditions.checkState(mHandler.getLooper().getThread() == Thread.currentThread(), "must call on " + mHandler.getLooper().getThread() + ", but was " + Thread.currentThread()); @@ -1033,11 +1035,14 @@ public class ScreenDecorations implements CoreStartable, Tunable , Dumpable { mDotViewController.setNewRotation(newRotation); } final Display.Mode newMod = mDisplayInfo.getMode(); + final DisplayCutout newCutout = mDisplayInfo.displayCutout; if (!mPendingConfigChange - && (newRotation != mRotation || displayModeChanged(mDisplayMode, newMod))) { + && (newRotation != mRotation || displayModeChanged(mDisplayMode, newMod) + || !Objects.equals(newCutout, mDisplayCutout))) { mRotation = newRotation; mDisplayMode = newMod; + mDisplayCutout = newCutout; mRoundedCornerResDelegate.setPhysicalPixelDisplaySizeRatio( getPhysicalPixelDisplaySizeRatio()); if (mScreenDecorHwcLayer != null) { diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java index a6e767ca27ee..ec15d1a0c7b6 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java @@ -158,6 +158,10 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold private View mTopDrag; private View mRightDrag; private View mBottomDrag; + private ImageView mTopLeftCornerView; + private ImageView mTopRightCornerView; + private ImageView mBottomLeftCornerView; + private ImageView mBottomRightCornerView; private final Configuration mConfiguration; @NonNull @@ -357,13 +361,15 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold return false; } - private void changeMagnificationSize(@MagnificationSize int index) { + @VisibleForTesting + void changeMagnificationSize(@MagnificationSize int index) { final int initSize = Math.min(mWindowBounds.width(), mWindowBounds.height()) / 3; int size = (int) (initSize * MAGNIFICATION_SCALE_OPTIONS[index]); setWindowSize(size, size); } - private void setEditMagnifierSizeMode(boolean enable) { + @VisibleForTesting + void setEditMagnifierSizeMode(boolean enable) { mEditSizeEnable = enable; applyResourcesValues(); @@ -639,10 +645,37 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold Region regionInsideDragBorder = new Region(mBorderDragSize, mBorderDragSize, mMirrorView.getWidth() - mBorderDragSize, mMirrorView.getHeight() - mBorderDragSize); + + Region tapExcludeRegion = new Region(); + Rect dragArea = new Rect(); mDragView.getHitRect(dragArea); - regionInsideDragBorder.op(dragArea, Region.Op.DIFFERENCE); + Rect topLeftArea = new Rect(); + mTopLeftCornerView.getHitRect(topLeftArea); + + Rect topRightArea = new Rect(); + mTopRightCornerView.getHitRect(topRightArea); + + Rect bottomLeftArea = new Rect(); + mBottomLeftCornerView.getHitRect(bottomLeftArea); + + Rect bottomRightArea = new Rect(); + mBottomRightCornerView.getHitRect(bottomRightArea); + + Rect closeArea = new Rect(); + mCloseView.getHitRect(closeArea); + + // add tapExcludeRegion for Drag or close + tapExcludeRegion.op(dragArea, Region.Op.UNION); + tapExcludeRegion.op(topLeftArea, Region.Op.UNION); + tapExcludeRegion.op(topRightArea, Region.Op.UNION); + tapExcludeRegion.op(bottomLeftArea, Region.Op.UNION); + tapExcludeRegion.op(bottomRightArea, Region.Op.UNION); + tapExcludeRegion.op(closeArea, Region.Op.UNION); + + regionInsideDragBorder.op(tapExcludeRegion, Region.Op.DIFFERENCE); + return regionInsideDragBorder; } @@ -756,6 +789,10 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold mRightDrag = mMirrorView.findViewById(R.id.right_handle); mBottomDrag = mMirrorView.findViewById(R.id.bottom_handle); mCloseView = mMirrorView.findViewById(R.id.close_button); + mTopRightCornerView = mMirrorView.findViewById(R.id.top_right_corner); + mTopLeftCornerView = mMirrorView.findViewById(R.id.top_left_corner); + mBottomRightCornerView = mMirrorView.findViewById(R.id.bottom_right_corner); + mBottomLeftCornerView = mMirrorView.findViewById(R.id.bottom_left_corner); mDragView.setOnTouchListener(this); mLeftDrag.setOnTouchListener(this); @@ -763,6 +800,10 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold mRightDrag.setOnTouchListener(this); mBottomDrag.setOnTouchListener(this); mCloseView.setOnTouchListener(this); + mTopLeftCornerView.setOnTouchListener(this); + mTopRightCornerView.setOnTouchListener(this); + mBottomLeftCornerView.setOnTouchListener(this); + mBottomRightCornerView.setOnTouchListener(this); } /** @@ -831,8 +872,16 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold @Override public boolean onTouch(View v, MotionEvent event) { - if (v == mDragView || v == mLeftDrag || v == mTopDrag || v == mRightDrag - || v == mBottomDrag || v == mCloseView) { + if (v == mDragView + || v == mLeftDrag + || v == mTopDrag + || v == mRightDrag + || v == mBottomDrag + || v == mTopLeftCornerView + || v == mTopRightCornerView + || v == mBottomLeftCornerView + || v == mBottomRightCornerView + || v == mCloseView) { return mGestureDetector.onTouch(v, event); } return false; @@ -1195,7 +1244,7 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold @Override public boolean onDrag(View view, float offsetX, float offsetY) { if (mEditSizeEnable) { - changeWindowSize(view, offsetX, offsetY); + return changeWindowSize(view, offsetX, offsetY); } else { move((int) offsetX, (int) offsetY); } @@ -1220,13 +1269,47 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold if (mEditSizeEnable) { mDragView.setVisibility(View.GONE); mCloseView.setVisibility(View.VISIBLE); + mTopRightCornerView.setVisibility(View.VISIBLE); + mTopLeftCornerView.setVisibility(View.VISIBLE); + mBottomRightCornerView.setVisibility(View.VISIBLE); + mBottomLeftCornerView.setVisibility(View.VISIBLE); } else { mDragView.setVisibility(View.VISIBLE); mCloseView.setVisibility(View.GONE); + mTopRightCornerView.setVisibility(View.GONE); + mTopLeftCornerView.setVisibility(View.GONE); + mBottomRightCornerView.setVisibility(View.GONE); + mBottomLeftCornerView.setVisibility(View.GONE); } } - public boolean changeWindowSize(View view, float offsetX, float offsetY) { + private boolean changeWindowSize(View view, float offsetX, float offsetY) { + if (view == mLeftDrag) { + changeMagnificationFrameSize(offsetX, 0, 0, 0); + } else if (view == mRightDrag) { + changeMagnificationFrameSize(0, 0, offsetX, 0); + } else if (view == mTopDrag) { + changeMagnificationFrameSize(0, offsetY, 0, 0); + } else if (view == mBottomDrag) { + changeMagnificationFrameSize(0, 0, 0, offsetY); + } else if (view == mTopLeftCornerView) { + changeMagnificationFrameSize(offsetX, offsetY, 0, 0); + } else if (view == mTopRightCornerView) { + changeMagnificationFrameSize(0, offsetY, offsetX, 0); + } else if (view == mBottomLeftCornerView) { + changeMagnificationFrameSize(offsetX, 0, 0, offsetY); + } else if (view == mBottomRightCornerView) { + changeMagnificationFrameSize(0, 0, offsetX, offsetY); + } else { + return false; + } + + return true; + } + + private void changeMagnificationFrameSize( + float leftOffset, float topOffset, float rightOffset, + float bottomOffset) { boolean bRTL = isRTL(mContext); final int initSize = Math.min(mWindowBounds.width(), mWindowBounds.height()) / 3; @@ -1236,54 +1319,26 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold Rect tempRect = new Rect(); tempRect.set(mMagnificationFrame); - if (view == mLeftDrag) { - if (bRTL) { - tempRect.right += offsetX; - if (tempRect.right > mWindowBounds.width()) { - return false; - } - } else { - tempRect.left += offsetX; - if (tempRect.left < 0) { - return false; - } - } - } else if (view == mRightDrag) { - if (bRTL) { - tempRect.left += offsetX; - if (tempRect.left < 0) { - return false; - } - } else { - tempRect.right += offsetX; - if (tempRect.right > mWindowBounds.width()) { - return false; - } - } - } else if (view == mTopDrag) { - tempRect.top += offsetY; - if (tempRect.top < 0) { - return false; - } - } else if (view == mBottomDrag) { - tempRect.bottom += offsetY; - if (tempRect.bottom > mWindowBounds.height()) { - return false; - } + if (bRTL) { + tempRect.left += (int) (rightOffset); + tempRect.right += (int) (leftOffset); + } else { + tempRect.right += (int) (rightOffset); + tempRect.left += (int) (leftOffset); } + tempRect.top += (int) (topOffset); + tempRect.bottom += (int) (bottomOffset); if (tempRect.width() < initSize || tempRect.height() < initSize || tempRect.width() > maxWidthSize || tempRect.height() > maxHeightSize) { - return false; + return; } - mMagnificationFrame.set(tempRect); computeBounceAnimationScale(); calculateMagnificationFrameBoundary(); modifyWindowMagnification(true); - return true; } private static boolean isRTL(Context context) { diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java index 9cffd5d609a7..069c0f656d7b 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java @@ -103,7 +103,7 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest MagnificationSize.LARGE, }) /** Denotes the Magnification size type. */ - @interface MagnificationSize { + public @interface MagnificationSize { int NONE = 0; int SMALL = 1; int MEDIUM = 2; diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java index 33e155df80e3..b8f14aef648a 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java @@ -16,14 +16,19 @@ package com.android.systemui.accessibility.floatingmenu; +import static com.android.internal.accessibility.common.ShortcutConstants.AccessibilityFragmentType.INVISIBLE_TOGGLE; +import static com.android.internal.accessibility.util.AccessibilityUtils.getAccessibilityServiceFragmentType; +import static com.android.internal.accessibility.util.AccessibilityUtils.setAccessibilityServiceState; import static com.android.systemui.accessibility.floatingmenu.MenuMessageView.Index; +import android.accessibilityservice.AccessibilityServiceInfo; import android.annotation.IntDef; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.Configuration; import android.os.Handler; import android.os.Looper; +import android.os.UserHandle; import android.provider.Settings; import android.util.PluralsMessageFormatter; import android.view.MotionEvent; @@ -82,8 +87,22 @@ class MenuViewLayer extends FrameLayout { final Runnable mDismissMenuAction = new Runnable() { @Override public void run() { - Settings.Secure.putString(getContext().getContentResolver(), - Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, /* value= */ ""); + Settings.Secure.putStringForUser(getContext().getContentResolver(), + Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, /* value= */ "", + UserHandle.USER_CURRENT); + + // Should disable the corresponding service when the fragment type is + // INVISIBLE_TOGGLE, which will enable service when the shortcut is on. + final List<AccessibilityServiceInfo> serviceInfoList = + mAccessibilityManager.getEnabledAccessibilityServiceList( + AccessibilityServiceInfo.FEEDBACK_ALL_MASK); + serviceInfoList.forEach(info -> { + if (getAccessibilityServiceFragmentType(info) == INVISIBLE_TOGGLE) { + setAccessibilityServiceState(mContext, info.getComponentName(), /* enabled= */ + false); + } + }); + mFloatingMenu.hide(); } }; 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 000000000000..b52ddc1dbc42 --- /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 6a10d4ab1e8b..03d999f697d0 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 android.widget.TextView; 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 class BatteryMeterView extends LinearLayout implements DarkReceiver { 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 @@ public class BatteryMeterView extends LinearLayout implements DarkReceiver { 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 @@ public class BatteryMeterView extends LinearLayout implements DarkReceiver { 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 @@ public class BatteryMeterView extends LinearLayout implements DarkReceiver { 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 @@ public class BatteryMeterView extends LinearLayout implements DarkReceiver { 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 @@ public class BatteryMeterView extends LinearLayout implements DarkReceiver { 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 @@ public class BatteryMeterView extends LinearLayout implements DarkReceiver { 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 @@ public class BatteryMeterView extends LinearLayout implements DarkReceiver { 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 @@ public class BatteryMeterView extends LinearLayout implements DarkReceiver { } mBatteryStateUnknown = isUnknown; + updateContentDescription(); if (mBatteryStateUnknown) { mBatteryIconView.setImageDrawable(getUnknownStateDrawable()); @@ -349,15 +389,43 @@ public class BatteryMeterView extends LinearLayout implements DarkReceiver { 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 ae9a32309d45..f4ec33ad24b5 100644 --- a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java +++ b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java @@ -17,19 +17,23 @@ package com.android.systemui.battery; import static android.provider.Settings.System.SHOW_BATTERY_PERCENT; -import android.app.ActivityManager; import android.content.ContentResolver; +import android.content.Context; import android.database.ContentObserver; import android.net.Uri; import android.os.Handler; +import android.os.HandlerExecutor; import android.provider.Settings; import android.text.TextUtils; import android.util.ArraySet; import android.view.View; -import com.android.systemui.broadcast.BroadcastDispatcher; +import androidx.annotation.NonNull; + import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.settings.CurrentUserTracker; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; +import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.phone.StatusBarIconController; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.ConfigurationController; @@ -42,12 +46,13 @@ import javax.inject.Inject; public class BatteryMeterViewController extends ViewController<BatteryMeterView> { private final ConfigurationController mConfigurationController; private final TunerService mTunerService; + private final Handler mMainHandler; private final ContentResolver mContentResolver; private final BatteryController mBatteryController; private final String mSlotBattery; private final SettingObserver mSettingObserver; - private final CurrentUserTracker mCurrentUserTracker; + private final UserTracker mUserTracker; private final ConfigurationController.ConfigurationListener mConfigurationListener = new ConfigurationController.ConfigurationListener() { @@ -84,6 +89,21 @@ public class BatteryMeterViewController extends ViewController<BatteryMeterView> public void onBatteryUnknownStateChanged(boolean isUnknown) { mView.onBatteryUnknownStateChanged(isUnknown); } + + @Override + public void onIsOverheatedChanged(boolean isOverheated) { + mView.onIsOverheatedChanged(isOverheated); + } + }; + + private final UserTracker.Callback mUserChangedCallback = + new UserTracker.Callback() { + @Override + public void onUserChanged(int newUser, @NonNull Context userContext) { + mContentResolver.unregisterContentObserver(mSettingObserver); + registerShowBatteryPercentObserver(newUser); + mView.updateShowPercent(); + } }; // Some places may need to show the battery conditionally, and not obey the tuner @@ -93,30 +113,26 @@ public class BatteryMeterViewController extends ViewController<BatteryMeterView> @Inject public BatteryMeterViewController( BatteryMeterView view, + UserTracker userTracker, ConfigurationController configurationController, TunerService tunerService, - BroadcastDispatcher broadcastDispatcher, @Main Handler mainHandler, ContentResolver contentResolver, + FeatureFlags featureFlags, BatteryController batteryController) { super(view); + mUserTracker = userTracker; mConfigurationController = configurationController; mTunerService = tunerService; + mMainHandler = mainHandler; mContentResolver = contentResolver; 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); - mCurrentUserTracker = new CurrentUserTracker(broadcastDispatcher) { - @Override - public void onUserSwitched(int newUserId) { - contentResolver.unregisterContentObserver(mSettingObserver); - registerShowBatteryPercentObserver(newUserId); - mView.updateShowPercent(); - } - }; + mSettingObserver = new SettingObserver(mMainHandler); } @Override @@ -125,9 +141,9 @@ public class BatteryMeterViewController extends ViewController<BatteryMeterView> subscribeForTunerUpdates(); mBatteryController.addCallback(mBatteryStateChangeCallback); - registerShowBatteryPercentObserver(ActivityManager.getCurrentUser()); + registerShowBatteryPercentObserver(mUserTracker.getUserId()); registerGlobalBatteryUpdateObserver(); - mCurrentUserTracker.startTracking(); + mUserTracker.addCallback(mUserChangedCallback, new HandlerExecutor(mMainHandler)); mView.updateShowPercent(); } @@ -138,7 +154,7 @@ public class BatteryMeterViewController extends ViewController<BatteryMeterView> unsubscribeFromTunerUpdates(); mBatteryController.removeCallback(mBatteryStateChangeCallback); - mCurrentUserTracker.stopTracking(); + mUserTracker.removeCallback(mUserChangedCallback); mContentResolver.unregisterContentObserver(mSettingObserver); } 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 000000000000..6455a9656fde --- /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/biometrics/AuthRippleView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt index c93fe6ac9f34..4b57d455a137 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt @@ -29,7 +29,7 @@ import android.view.View import android.view.animation.PathInterpolator import com.android.internal.graphics.ColorUtils import com.android.systemui.animation.Interpolators -import com.android.systemui.ripple.RippleShader +import com.android.systemui.surfaceeffects.ripple.RippleShader private const val RIPPLE_SPARKLE_STRENGTH: Float = 0.4f diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPasswordViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPasswordViewBinder.kt index c619648a314c..5110a9cfb33b 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPasswordViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPasswordViewBinder.kt @@ -6,6 +6,8 @@ import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputMethodManager import android.widget.ImeAwareEditText import android.widget.TextView +import android.window.OnBackInvokedCallback +import android.window.OnBackInvokedDispatcher import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.R @@ -13,7 +15,7 @@ import com.android.systemui.biometrics.ui.CredentialPasswordView import com.android.systemui.biometrics.ui.CredentialView import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel import com.android.systemui.lifecycle.repeatWhenAttached -import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.launch /** Sub-binder for the [CredentialPasswordView]. */ @@ -29,6 +31,8 @@ object CredentialPasswordViewBinder { val passwordField: ImeAwareEditText = view.requireViewById(R.id.lockPassword) + val onBackInvokedCallback = OnBackInvokedCallback { host.onCredentialAborted() } + view.repeatWhenAttached { passwordField.requestFocus() passwordField.scheduleShowSoftInput() @@ -43,9 +47,7 @@ object CredentialPasswordViewBinder { launch { viewModel.checkCredential(text, header) } } ) - passwordField.setOnKeyListener( - OnBackButtonListener { host.onCredentialAborted() } - ) + passwordField.setOnKeyListener(OnBackButtonListener(onBackInvokedCallback)) } } @@ -66,18 +68,35 @@ object CredentialPasswordViewBinder { } } } + + val onBackInvokedDispatcher = view.findOnBackInvokedDispatcher() + if (onBackInvokedDispatcher != null) { + launch { + onBackInvokedDispatcher.registerOnBackInvokedCallback( + OnBackInvokedDispatcher.PRIORITY_DEFAULT, + onBackInvokedCallback + ) + awaitCancellation() + } + .invokeOnCompletion { + onBackInvokedDispatcher.unregisterOnBackInvokedCallback( + onBackInvokedCallback + ) + } + } } } } } -private class OnBackButtonListener(private val onBack: () -> Unit) : View.OnKeyListener { +private class OnBackButtonListener(private val onBackInvokedCallback: OnBackInvokedCallback) : + View.OnKeyListener { override fun onKey(v: View, keyCode: Int, event: KeyEvent): Boolean { if (keyCode != KeyEvent.KEYCODE_BACK) { return false } if (event.action == KeyEvent.ACTION_UP) { - onBack() + onBackInvokedCallback.onBackInvoked() } return true } diff --git a/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt b/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt index 616e49c0b709..1454210ad798 100644 --- a/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt +++ b/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt @@ -31,7 +31,7 @@ import com.android.systemui.R import com.android.systemui.dagger.SysUISingleton import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags -import com.android.systemui.ripple.RippleView +import com.android.systemui.surfaceeffects.ripple.RippleView import com.android.systemui.statusbar.commandline.Command import com.android.systemui.statusbar.commandline.CommandRegistry import com.android.systemui.statusbar.policy.BatteryController diff --git a/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingAnimation.java b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingAnimation.java index e82d0ea85490..3808ab742419 100644 --- a/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingAnimation.java +++ b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingAnimation.java @@ -30,7 +30,7 @@ import android.view.WindowManager; import com.android.internal.logging.UiEvent; import com.android.internal.logging.UiEventLogger; -import com.android.systemui.ripple.RippleShader.RippleShape; +import com.android.systemui.surfaceeffects.ripple.RippleShader.RippleShape; /** * A WirelessChargingAnimation is a view containing view + animation for wireless charging. diff --git a/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java index 145569919e8e..36103f8db8d8 100644 --- a/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java +++ b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java @@ -33,9 +33,9 @@ import android.widget.TextView; import com.android.settingslib.Utils; import com.android.systemui.R; import com.android.systemui.animation.Interpolators; -import com.android.systemui.ripple.RippleAnimationConfig; -import com.android.systemui.ripple.RippleShader.RippleShape; -import com.android.systemui.ripple.RippleView; +import com.android.systemui.surfaceeffects.ripple.RippleAnimationConfig; +import com.android.systemui.surfaceeffects.ripple.RippleShader.RippleShape; +import com.android.systemui.surfaceeffects.ripple.RippleView; import java.text.NumberFormat; diff --git a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java index beaccbaf9a70..e8e1f2e95f5d 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java @@ -231,7 +231,8 @@ public class BrightLineFalsingManager implements FalsingManager { // check for false tap if it is a seekbar interaction if (interactionType == MEDIA_SEEKBAR) { - localResult[0] &= isFalseTap(LOW_PENALTY); + localResult[0] &= isFalseTap(mFeatureFlags.isEnabled(Flags.MEDIA_FALSING_PENALTY) + ? FalsingManager.MODERATE_PENALTY : FalsingManager.LOW_PENALTY); } logDebug("False Gesture (type: " + interactionType + "): " + localResult[0]); diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt index b11103a4d27b..7df08651d5ab 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt @@ -18,6 +18,7 @@ package com.android.systemui.controls.management import android.app.ActivityOptions import android.content.ComponentName +import android.content.Context import android.content.Intent import android.os.Bundle import android.util.Log @@ -33,21 +34,23 @@ import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView import com.android.systemui.R -import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.controls.CustomIconCache import com.android.systemui.controls.controller.ControlsControllerImpl import com.android.systemui.controls.controller.StructureInfo import com.android.systemui.controls.ui.ControlsActivity import com.android.systemui.controls.ui.ControlsUiController -import com.android.systemui.settings.CurrentUserTracker +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.settings.UserTracker +import java.util.concurrent.Executor import javax.inject.Inject /** * Activity for rearranging and removing controls for a given structure */ open class ControlsEditingActivity @Inject constructor( + @Main private val mainExecutor: Executor, private val controller: ControlsControllerImpl, - private val broadcastDispatcher: BroadcastDispatcher, + private val userTracker: UserTracker, private val customIconCache: CustomIconCache, private val uiController: ControlsUiController ) : ComponentActivity() { @@ -66,12 +69,12 @@ open class ControlsEditingActivity @Inject constructor( private lateinit var subtitle: TextView private lateinit var saveButton: View - private val currentUserTracker = object : CurrentUserTracker(broadcastDispatcher) { + private val userTrackerCallback: UserTracker.Callback = object : UserTracker.Callback { private val startingUser = controller.currentUserId - override fun onUserSwitched(newUserId: Int) { - if (newUserId != startingUser) { - stopTracking() + override fun onUserChanged(newUser: Int, userContext: Context) { + if (newUser != startingUser) { + userTracker.removeCallback(this) finish() } } @@ -104,7 +107,7 @@ open class ControlsEditingActivity @Inject constructor( super.onStart() setUpList() - currentUserTracker.startTracking() + userTracker.addCallback(userTrackerCallback, mainExecutor) if (DEBUG) { Log.d(TAG, "Registered onBackInvokedCallback") @@ -115,7 +118,7 @@ open class ControlsEditingActivity @Inject constructor( override fun onStop() { super.onStop() - currentUserTracker.stopTracking() + userTracker.removeCallback(userTrackerCallback) if (DEBUG) { Log.d(TAG, "Unregistered onBackInvokedCallback") @@ -248,7 +251,7 @@ open class ControlsEditingActivity @Inject constructor( } override fun onDestroy() { - currentUserTracker.stopTracking() + userTracker.removeCallback(userTrackerCallback) super.onDestroy() } } diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt index 9b2a72823d86..3e97d3132bc7 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt @@ -20,6 +20,7 @@ import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.app.ActivityOptions import android.content.ComponentName +import android.content.Context import android.content.Intent import android.content.res.Configuration import android.os.Bundle @@ -39,7 +40,6 @@ import androidx.activity.ComponentActivity import androidx.viewpager2.widget.ViewPager2 import com.android.systemui.Prefs import com.android.systemui.R -import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.TooltipManager import com.android.systemui.controls.controller.ControlsControllerImpl @@ -47,7 +47,7 @@ import com.android.systemui.controls.controller.StructureInfo import com.android.systemui.controls.ui.ControlsActivity import com.android.systemui.controls.ui.ControlsUiController import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.settings.CurrentUserTracker +import com.android.systemui.settings.UserTracker import java.text.Collator import java.util.concurrent.Executor import java.util.function.Consumer @@ -57,7 +57,7 @@ open class ControlsFavoritingActivity @Inject constructor( @Main private val executor: Executor, private val controller: ControlsControllerImpl, private val listingController: ControlsListingController, - private val broadcastDispatcher: BroadcastDispatcher, + private val userTracker: UserTracker, private val uiController: ControlsUiController ) : ComponentActivity() { @@ -95,12 +95,12 @@ open class ControlsFavoritingActivity @Inject constructor( private var cancelLoadRunnable: Runnable? = null private var isPagerLoaded = false - private val currentUserTracker = object : CurrentUserTracker(broadcastDispatcher) { + private val userTrackerCallback: UserTracker.Callback = object : UserTracker.Callback { private val startingUser = controller.currentUserId - override fun onUserSwitched(newUserId: Int) { - if (newUserId != startingUser) { - stopTracking() + override fun onUserChanged(newUser: Int, userContext: Context) { + if (newUser != startingUser) { + userTracker.removeCallback(this) finish() } } @@ -363,7 +363,7 @@ open class ControlsFavoritingActivity @Inject constructor( super.onStart() listingController.addCallback(listingCallback) - currentUserTracker.startTracking() + userTracker.addCallback(userTrackerCallback, executor) if (DEBUG) { Log.d(TAG, "Registered onBackInvokedCallback") @@ -388,7 +388,7 @@ open class ControlsFavoritingActivity @Inject constructor( super.onStop() listingController.removeCallback(listingCallback) - currentUserTracker.stopTracking() + userTracker.removeCallback(userTrackerCallback) if (DEBUG) { Log.d(TAG, "Unregistered onBackInvokedCallback") diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt index 47690a7fa487..90bc5d0f8daa 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt @@ -18,6 +18,7 @@ package com.android.systemui.controls.management import android.app.ActivityOptions import android.content.ComponentName +import android.content.Context import android.content.Intent import android.os.Bundle import android.util.Log @@ -33,13 +34,12 @@ import androidx.activity.ComponentActivity import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.android.systemui.R -import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.ui.ControlsActivity import com.android.systemui.controls.ui.ControlsUiController import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.settings.CurrentUserTracker +import com.android.systemui.settings.UserTracker import java.util.concurrent.Executor import javax.inject.Inject @@ -51,7 +51,7 @@ open class ControlsProviderSelectorActivity @Inject constructor( @Background private val backExecutor: Executor, private val listingController: ControlsListingController, private val controlsController: ControlsController, - private val broadcastDispatcher: BroadcastDispatcher, + private val userTracker: UserTracker, private val uiController: ControlsUiController ) : ComponentActivity() { @@ -62,12 +62,12 @@ open class ControlsProviderSelectorActivity @Inject constructor( } private var backShouldExit = false private lateinit var recyclerView: RecyclerView - private val currentUserTracker = object : CurrentUserTracker(broadcastDispatcher) { + private val userTrackerCallback: UserTracker.Callback = object : UserTracker.Callback { private val startingUser = listingController.currentUserId - override fun onUserSwitched(newUserId: Int) { - if (newUserId != startingUser) { - stopTracking() + override fun onUserChanged(newUser: Int, userContext: Context) { + if (newUser != startingUser) { + userTracker.removeCallback(this) finish() } } @@ -129,7 +129,7 @@ open class ControlsProviderSelectorActivity @Inject constructor( override fun onStart() { super.onStart() - currentUserTracker.startTracking() + userTracker.addCallback(userTrackerCallback, executor) recyclerView.alpha = 0.0f recyclerView.adapter = AppAdapter( @@ -161,7 +161,7 @@ open class ControlsProviderSelectorActivity @Inject constructor( override fun onStop() { super.onStop() - currentUserTracker.stopTracking() + userTracker.removeCallback(userTrackerCallback) if (DEBUG) { Log.d(TAG, "Unregistered onBackInvokedCallback") @@ -190,7 +190,7 @@ open class ControlsProviderSelectorActivity @Inject constructor( } override fun onDestroy() { - currentUserTracker.stopTracking() + userTracker.removeCallback(userTrackerCallback) super.onDestroy() } diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsRequestDialog.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsRequestDialog.kt index b376455ee815..86bde5c3cf4d 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsRequestDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsRequestDialog.kt @@ -19,6 +19,7 @@ package com.android.systemui.controls.management import android.app.AlertDialog import android.app.Dialog import android.content.ComponentName +import android.content.Context import android.content.DialogInterface import android.content.Intent import android.os.Bundle @@ -32,18 +33,20 @@ import android.widget.ImageView import android.widget.TextView import androidx.activity.ComponentActivity import com.android.systemui.R -import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.controller.ControlInfo import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.ui.RenderInfo -import com.android.systemui.settings.CurrentUserTracker +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.phone.SystemUIDialog +import java.util.concurrent.Executor import javax.inject.Inject open class ControlsRequestDialog @Inject constructor( + @Main private val mainExecutor: Executor, private val controller: ControlsController, - private val broadcastDispatcher: BroadcastDispatcher, + private val userTracker: UserTracker, private val controlsListingController: ControlsListingController ) : ComponentActivity(), DialogInterface.OnClickListener, DialogInterface.OnCancelListener { @@ -58,12 +61,12 @@ open class ControlsRequestDialog @Inject constructor( override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) {} } - private val currentUserTracker = object : CurrentUserTracker(broadcastDispatcher) { + private val userTrackerCallback: UserTracker.Callback = object : UserTracker.Callback { private val startingUser = controller.currentUserId - override fun onUserSwitched(newUserId: Int) { - if (newUserId != startingUser) { - stopTracking() + override fun onUserChanged(newUser: Int, userContext: Context) { + if (newUser != startingUser) { + userTracker.removeCallback(this) finish() } } @@ -72,7 +75,7 @@ open class ControlsRequestDialog @Inject constructor( override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - currentUserTracker.startTracking() + userTracker.addCallback(userTrackerCallback, mainExecutor) controlsListingController.addCallback(callback) val requestUser = intent.getIntExtra(Intent.EXTRA_USER_ID, UserHandle.USER_NULL) @@ -118,7 +121,7 @@ open class ControlsRequestDialog @Inject constructor( override fun onDestroy() { dialog?.dismiss() - currentUserTracker.stopTracking() + userTracker.removeCallback(userTrackerCallback) controlsListingController.removeCallback(callback) super.onDestroy() } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java index 25418c3f2105..0664e9f6ac39 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java @@ -17,6 +17,7 @@ package com.android.systemui.dagger; import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.app.ActivityManager; import android.app.ActivityTaskManager; import android.app.AlarmManager; @@ -69,6 +70,7 @@ import android.os.PowerManager; import android.os.ServiceManager; import android.os.UserManager; import android.os.Vibrator; +import android.os.storage.StorageManager; import android.permission.PermissionManager; import android.safetycenter.SafetyCenterManager; import android.service.dreams.DreamService; @@ -110,6 +112,7 @@ import dagger.Provides; /** * Provides Non-SystemUI, Framework-Owned instances to the dependency graph. */ +@SuppressLint("NonInjectedService") @Module public class FrameworkServicesModule { @Provides @@ -469,7 +472,13 @@ public class FrameworkServicesModule { @Provides @Singleton - static SubscriptionManager provideSubcriptionManager(Context context) { + static StorageManager provideStorageManager(Context context) { + return context.getSystemService(StorageManager.class); + } + + @Provides + @Singleton + static SubscriptionManager provideSubscriptionManager(Context context) { return context.getSystemService(SubscriptionManager.class); } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/GlobalRootComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/GlobalRootComponent.java index fe89c9a1e3b9..9e8c0ec7423e 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 android.content.Context; 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 7ab36e84178e..d3555eec0243 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 @@ public interface ReferenceSysUIComponent extends SysUIComponent { 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/doze/DozeSensors.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java index d0258d37cc96..f64d91869fcb 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java @@ -24,7 +24,6 @@ import static com.android.systemui.plugins.SensorManagerPlugin.Sensor.TYPE_WAKE_ import static com.android.systemui.plugins.SensorManagerPlugin.Sensor.TYPE_WAKE_LOCK_SCREEN; import android.annotation.AnyThread; -import android.app.ActivityManager; import android.database.ContentObserver; import android.hardware.Sensor; import android.hardware.SensorManager; @@ -50,6 +49,7 @@ import com.android.internal.logging.UiEventLoggerImpl; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.biometrics.AuthController; import com.android.systemui.plugins.SensorManagerPlugin; +import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.statusbar.policy.DevicePostureController; import com.android.systemui.util.sensors.AsyncSensorManager; @@ -99,6 +99,7 @@ public class DozeSensors { private final SecureSettings mSecureSettings; private final DevicePostureController mDevicePostureController; private final AuthController mAuthController; + private final UserTracker mUserTracker; private final boolean mScreenOffUdfpsEnabled; // Sensors @@ -152,7 +153,8 @@ public class DozeSensors { ProximitySensor proximitySensor, SecureSettings secureSettings, AuthController authController, - DevicePostureController devicePostureController + DevicePostureController devicePostureController, + UserTracker userTracker ) { mSensorManager = sensorManager; mConfig = config; @@ -170,6 +172,7 @@ public class DozeSensors { mDevicePostureController = devicePostureController; mDevicePosture = mDevicePostureController.getDevicePosture(); mAuthController = authController; + mUserTracker = userTracker; mUdfpsEnrolled = mAuthController.isUdfpsEnrolled(KeyguardUpdateMonitor.getCurrentUser()); @@ -441,7 +444,7 @@ public class DozeSensors { private final ContentObserver mSettingsObserver = new ContentObserver(mHandler) { @Override public void onChange(boolean selfChange, Collection<Uri> uris, int flags, int userId) { - if (userId != ActivityManager.getCurrentUser()) { + if (userId != mUserTracker.getUserId()) { return; } for (TriggerSensor s : mTriggerSensors) { diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java index 32cb1c01b776..0b69b80689e0 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java @@ -45,6 +45,7 @@ import com.android.systemui.dock.DockManager; import com.android.systemui.doze.DozeMachine.State; import com.android.systemui.doze.dagger.DozeScope; import com.android.systemui.log.SessionTracker; +import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.statusbar.policy.DevicePostureController; import com.android.systemui.statusbar.policy.KeyguardStateController; @@ -188,7 +189,8 @@ public class DozeTriggers implements DozeMachine.Part { UiEventLogger uiEventLogger, SessionTracker sessionTracker, KeyguardStateController keyguardStateController, - DevicePostureController devicePostureController) { + DevicePostureController devicePostureController, + UserTracker userTracker) { mContext = context; mDozeHost = dozeHost; mConfig = config; @@ -200,7 +202,7 @@ public class DozeTriggers implements DozeMachine.Part { mDozeSensors = new DozeSensors(mSensorManager, dozeParameters, config, wakeLock, this::onSensor, this::onProximityFar, dozeLog, proximitySensor, - secureSettings, authController, devicePostureController); + secureSettings, authController, devicePostureController, userTracker); mDockManager = dockManager; mProxCheck = proxCheck; mDozeLog = dozeLog; 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 5694f6da0ea5..440dcbc18a12 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 @@ public class ComplicationLayoutEngine implements Complication.VisibilityControll 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 a21eb19bd548..2b32d349dd67 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 @@ public class ComplicationLayoutParams extends ViewGroup.LayoutParams { } /** + * 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 cedd850ac2ef..c01cf43eae74 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.UiEvent; 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.dreams.complication.dagger.DreamHomeControlsComplica 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 @@ public class DreamHomeControlsComplication implements Complication { 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(); + + private final DreamOverlayStateController.Callback mOverlayStateCallback = + new DreamOverlayStateController.Callback() { + @Override + public void onStateChanged() { + if (mOverlayActive == mDreamOverlayStateController.isOverlayActive()) { + return; + } - if (available != mControlServicesAvailable) { - mControlServicesAvailable = available; - updateComplicationAvailability(); + mOverlayActive = !mOverlayActive; + + if (mOverlayActive) { + updateHomeControlsComplication(); + } } }; @@ -102,18 +114,29 @@ public class DreamHomeControlsComplication implements Complication { public void start() { mControlsComponent.getControlsListingController().ifPresent( c -> c.addCallback(mControlsCallback)); + mDreamOverlayStateController.addCallback(mOverlayStateCallback); + } + + private void updateHomeControlsComplication() { + mControlsComponent.getControlsListingController().ifPresent(c -> { + if (isHomeControlsAvailable(c.getCurrentServices())) { + mDreamOverlayStateController.addComplication(mComplication); + } else { + mDreamOverlayStateController.removeComplication(mComplication); + } + }); } - private void updateComplicationAvailability() { + 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 7d2ce51ffbf6..69b85b5a5e51 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 @@ public interface RegisteredComplicationsModule { 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 @@ public interface RegisteredComplicationsModule { 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 @@ public interface RegisteredComplicationsModule { 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 @@ public interface RegisteredComplicationsModule { */ @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 f9dca08ae14f..101f4a450071 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 @@ import dagger.Provides; 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 @@ public interface DreamModule { /** */ @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 002ffdb68682..784e92d37e28 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -79,10 +79,18 @@ object Flags { val NOTIFICATION_GROUP_CORNER = unreleasedFlag(116, "notification_group_corner", teamfood = true) + // TODO(b/259217907) + @JvmField + val NOTIFICATION_GROUP_DISMISSAL_ANIMATION = + unreleasedFlag(259217907, "notification_group_dismissal_animation", teamfood = true) + // TODO(b/257506350): Tracking Bug val FSI_CHROME = unreleasedFlag(117, "fsi_chrome") - // next id: 118 + // TODO(b/257315550): Tracking Bug + val NO_HUN_FOR_OLD_WHEN = unreleasedFlag(118, "no_hun_for_old_when") + + // next id: 119 // 200 - keyguard/lockscreen // ** Flag retired ** @@ -112,28 +120,6 @@ object Flags { @JvmField val MODERN_BOUNCER = releasedFlag(208, "modern_bouncer") /** - * Whether the user interactor and repository should use `UserSwitcherController`. - * - * If this is `false`, the interactor and repo skip the controller and directly access the - * framework APIs. - */ - // TODO(b/254513286): Tracking Bug - val USER_INTERACTOR_AND_REPO_USE_CONTROLLER = - unreleasedFlag(210, "user_interactor_and_repo_use_controller") - - /** - * Whether `UserSwitcherController` should use the user interactor. - * - * When this is `true`, the controller does not directly access framework APIs. Instead, it goes - * through the interactor. - * - * Note: do not set this to true if [.USER_INTERACTOR_AND_REPO_USE_CONTROLLER] is `true` as it - * would created a cycle between controller -> interactor -> controller. - */ - // TODO(b/254513102): Tracking Bug - val USER_CONTROLLER_USES_INTERACTOR = releasedFlag(211, "user_controller_uses_interactor") - - /** * Whether the clock on a wide lock screen should use the new "stepping" animation for moving * the digits when the clock moves. */ @@ -207,9 +193,7 @@ object Flags { @JvmField val NEW_FOOTER_ACTIONS = releasedFlag(507, "new_footer_actions") // TODO(b/244064524): Tracking Bug - @JvmField - val QS_SECONDARY_DATA_SUB_INFO = - unreleasedFlag(508, "qs_secondary_data_sub_info", teamfood = true) + @JvmField val QS_SECONDARY_DATA_SUB_INFO = releasedFlag(508, "qs_secondary_data_sub_info") // 600- status bar // TODO(b/254513246): Tracking Bug @@ -239,6 +223,9 @@ object Flags { // 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") @@ -291,6 +278,8 @@ object Flags { // TODO(b/254513168): Tracking Bug @JvmField val UMO_SURFACE_RIPPLE = unreleasedFlag(907, "umo_surface_ripple") + @JvmField val MEDIA_FALSING_PENALTY = unreleasedFlag(908, "media_falsing_media") + // 1000 - dock val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging") @@ -380,7 +369,8 @@ object Flags { // 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/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java index 3ef5499237f1..db2cd91374e5 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java @@ -125,6 +125,7 @@ import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.GlobalActions.GlobalActionsManager; import com.android.systemui.plugins.GlobalActionsPanelPlugin; import com.android.systemui.scrim.ScrimDrawable; +import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.phone.CentralSurfaces; @@ -201,6 +202,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene protected final SecureSettings mSecureSettings; protected final Resources mResources; private final ConfigurationController mConfigurationController; + private final UserTracker mUserTracker; private final UserManager mUserManager; private final TrustManager mTrustManager; private final IActivityManager mIActivityManager; @@ -339,6 +341,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene @NonNull VibratorHelper vibrator, @Main Resources resources, ConfigurationController configurationController, + UserTracker userTracker, KeyguardStateController keyguardStateController, UserManager userManager, TrustManager trustManager, @@ -370,6 +373,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene mSecureSettings = secureSettings; mResources = resources; mConfigurationController = configurationController; + mUserTracker = userTracker; mUserManager = userManager; mTrustManager = trustManager; mIActivityManager = iActivityManager; @@ -1198,11 +1202,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene } protected UserInfo getCurrentUser() { - try { - return mIActivityManager.getCurrentUser(); - } catch (RemoteException re) { - return null; - } + return mUserTracker.getUserInfo(); } /** 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 000000000000..0f4581ce3e61 --- /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/KeyguardSliceProvider.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java index 5d564f74772b..bafd2e70a676 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java @@ -17,7 +17,6 @@ package com.android.systemui.keyguard; import android.annotation.AnyThread; -import android.app.ActivityManager; import android.app.AlarmManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; @@ -52,6 +51,7 @@ import com.android.keyguard.KeyguardUpdateMonitorCallback; import com.android.systemui.R; import com.android.systemui.SystemUIAppComponentFactory; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.NotificationMediaManager; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.phone.DozeParameters; @@ -140,6 +140,8 @@ public class KeyguardSliceProvider extends SliceProvider implements public KeyguardBypassController mKeyguardBypassController; @Inject public KeyguardUpdateMonitor mKeyguardUpdateMonitor; + @Inject + UserTracker mUserTracker; private CharSequence mMediaTitle; private CharSequence mMediaArtist; protected boolean mDozing; @@ -355,7 +357,7 @@ public class KeyguardSliceProvider extends SliceProvider implements synchronized (this) { if (withinNHoursLocked(mNextAlarmInfo, ALARM_VISIBILITY_HOURS)) { String pattern = android.text.format.DateFormat.is24HourFormat(getContext(), - ActivityManager.getCurrentUser()) ? "HH:mm" : "h:mm"; + mUserTracker.getUserId()) ? "HH:mm" : "h:mm"; mNextAlarm = android.text.format.DateFormat.format(pattern, mNextAlarmInfo.getTriggerTime()).toString(); } else { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index d52efab746a7..663582ede385 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -36,7 +36,6 @@ import static com.android.systemui.DejankUtils.whitelistIpcs; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; -import android.app.ActivityManager; import android.app.ActivityTaskManager; import android.app.AlarmManager; import android.app.PendingIntent; @@ -124,6 +123,7 @@ import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.dagger.KeyguardModule; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.settings.UserTracker; import com.android.systemui.shade.NotificationPanelViewController; import com.android.systemui.shade.ShadeController; import com.android.systemui.shade.ShadeExpansionStateManager; @@ -263,6 +263,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, private AlarmManager mAlarmManager; private AudioManager mAudioManager; private StatusBarManager mStatusBarManager; + private final UserTracker mUserTracker; private final SysuiStatusBarStateController mStatusBarStateController; private final Executor mUiBgExecutor; private final ScreenOffAnimationController mScreenOffAnimationController; @@ -715,7 +716,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, @Override public void keyguardDone(boolean strongAuth, int targetUserId) { - if (targetUserId != ActivityManager.getCurrentUser()) { + if (targetUserId != mUserTracker.getUserId()) { return; } if (DEBUG) Log.d(TAG, "keyguardDone"); @@ -738,7 +739,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, public void keyguardDonePending(boolean strongAuth, int targetUserId) { Trace.beginSection("KeyguardViewMediator.mViewMediatorCallback#keyguardDonePending"); if (DEBUG) Log.d(TAG, "keyguardDonePending"); - if (targetUserId != ActivityManager.getCurrentUser()) { + if (targetUserId != mUserTracker.getUserId()) { Trace.endSection(); return; } @@ -1131,6 +1132,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, */ public KeyguardViewMediator( Context context, + UserTracker userTracker, FalsingCollector falsingCollector, LockPatternUtils lockPatternUtils, BroadcastDispatcher broadcastDispatcher, @@ -1156,6 +1158,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, Lazy<NotificationShadeWindowController> notificationShadeWindowControllerLazy, Lazy<ActivityLaunchAnimator> activityLaunchAnimator) { mContext = context; + mUserTracker = userTracker; mFalsingCollector = falsingCollector; mLockPatternUtils = lockPatternUtils; mBroadcastDispatcher = broadcastDispatcher; @@ -1234,7 +1237,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); - KeyguardUpdateMonitor.setCurrentUser(ActivityManager.getCurrentUser()); + KeyguardUpdateMonitor.setCurrentUser(mUserTracker.getUserId()); // Assume keyguard is showing (unless it's disabled) until we know for sure, unless Keyguard // is disabled. diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java index 78a7c9e35eb7..ef3c44340e57 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java @@ -47,6 +47,7 @@ import com.android.systemui.keyguard.data.repository.KeyguardRepositoryModule; import com.android.systemui.keyguard.domain.interactor.StartKeyguardTransitionModule; import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceModule; import com.android.systemui.navigationbar.NavigationModeController; +import com.android.systemui.settings.UserTracker; import com.android.systemui.shade.ShadeController; import com.android.systemui.statusbar.NotificationShadeDepthController; import com.android.systemui.statusbar.NotificationShadeWindowController; @@ -86,6 +87,7 @@ public class KeyguardModule { @SysUISingleton public static KeyguardViewMediator newKeyguardViewMediator( Context context, + UserTracker userTracker, FalsingCollector falsingCollector, LockPatternUtils lockPatternUtils, BroadcastDispatcher broadcastDispatcher, @@ -114,6 +116,7 @@ public class KeyguardModule { Lazy<ActivityLaunchAnimator> activityLaunchAnimator) { return new KeyguardViewMediator( context, + userTracker, falsingCollector, lockPatternUtils, broadcastDispatcher, 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 783f752cbd20..9a90fe7e60bd 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.BouncerShowMessageModel 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 @@ constructor( 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 @@ constructor( } 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 fcd653b33a6f..910cdf2df5a4 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 @@ constructor( @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/controls/models/player/MediaViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt index a7f1b95555ba..a8f39fa9a456 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt @@ -26,7 +26,8 @@ import android.widget.TextView import androidx.constraintlayout.widget.Barrier import com.android.systemui.R import com.android.systemui.media.controls.models.GutsViewHolder -import com.android.systemui.ripple.MultiRippleView +import com.android.systemui.surfaceeffects.ripple.MultiRippleView +import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseView import com.android.systemui.util.animation.TransitionLayout private const val TAG = "MediaViewHolder" @@ -38,6 +39,8 @@ class MediaViewHolder constructor(itemView: View) { // Player information val albumView = itemView.requireViewById<ImageView>(R.id.album_art) val multiRippleView = itemView.requireViewById<MultiRippleView>(R.id.touch_ripple_view) + val turbulenceNoiseView = + itemView.requireViewById<TurbulenceNoiseView>(R.id.turbulence_noise_view) val appIcon = itemView.requireViewById<ImageView>(R.id.icon) val titleText = itemView.requireViewById<TextView>(R.id.header_title) val artistText = itemView.requireViewById<TextView>(R.id.header_artist) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataFilter.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataFilter.kt index 45b319b274b2..cf71d675865b 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataFilter.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataFilter.kt @@ -20,13 +20,12 @@ import android.content.Context import android.os.SystemProperties import android.util.Log import com.android.internal.annotations.VisibleForTesting -import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.broadcast.BroadcastSender import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.media.controls.models.player.MediaData import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData import com.android.systemui.media.controls.util.MediaUiEventLogger -import com.android.systemui.settings.CurrentUserTracker +import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.util.time.SystemClock import java.util.SortedMap @@ -62,14 +61,13 @@ class MediaDataFilter @Inject constructor( private val context: Context, - private val broadcastDispatcher: BroadcastDispatcher, + private val userTracker: UserTracker, private val broadcastSender: BroadcastSender, private val lockscreenUserManager: NotificationLockscreenUserManager, @Main private val executor: Executor, private val systemClock: SystemClock, private val logger: MediaUiEventLogger ) : MediaDataManager.Listener { - private val userTracker: CurrentUserTracker private val _listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf() internal val listeners: Set<MediaDataManager.Listener> get() = _listeners.toSet() @@ -81,15 +79,15 @@ constructor( private var smartspaceMediaData: SmartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA private var reactivatedKey: String? = null - init { - userTracker = - object : CurrentUserTracker(broadcastDispatcher) { - override fun onUserSwitched(newUserId: Int) { - // Post this so we can be sure lockscreenUserManager already got the broadcast - executor.execute { handleUserSwitched(newUserId) } - } + private val userTrackerCallback = + object : UserTracker.Callback { + override fun onUserChanged(newUser: Int, userContext: Context) { + handleUserSwitched(newUser) } - userTracker.startTracking() + } + + init { + userTracker.addCallback(userTrackerCallback, executor) } override fun onMediaDataLoaded( diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/ColorSchemeTransition.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/ColorSchemeTransition.kt index 918417fcd9a9..93be6a78ccd5 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/ColorSchemeTransition.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/ColorSchemeTransition.kt @@ -29,7 +29,8 @@ import com.android.internal.annotations.VisibleForTesting import com.android.settingslib.Utils import com.android.systemui.media.controls.models.player.MediaViewHolder import com.android.systemui.monet.ColorScheme -import com.android.systemui.ripple.MultiRippleController +import com.android.systemui.surfaceeffects.ripple.MultiRippleController +import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseController /** * A [ColorTransition] is an object that updates the colors of views each time [updateColorScheme] @@ -102,13 +103,21 @@ internal constructor( private val context: Context, private val mediaViewHolder: MediaViewHolder, private val multiRippleController: MultiRippleController, + private val turbulenceNoiseController: TurbulenceNoiseController, animatingColorTransitionFactory: AnimatingColorTransitionFactory ) { constructor( context: Context, mediaViewHolder: MediaViewHolder, multiRippleController: MultiRippleController, - ) : this(context, mediaViewHolder, multiRippleController, ::AnimatingColorTransition) + turbulenceNoiseController: TurbulenceNoiseController + ) : this( + context, + mediaViewHolder, + multiRippleController, + turbulenceNoiseController, + ::AnimatingColorTransition + ) val bgColor = context.getColor(com.android.systemui.R.color.material_dynamic_secondary95) val surfaceColor = @@ -129,6 +138,7 @@ internal constructor( mediaViewHolder.actionPlayPause.backgroundTintList = accentColorList mediaViewHolder.gutsViewHolder.setAccentPrimaryColor(accentPrimary) multiRippleController.updateColor(accentPrimary) + turbulenceNoiseController.updateNoiseColor(accentPrimary) } val accentSecondary = diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java index 215fa03c8c59..21e64e28ff19 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java @@ -31,6 +31,7 @@ import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.ColorStateList; +import android.graphics.BlendMode; import android.graphics.Color; import android.graphics.ColorMatrix; import android.graphics.ColorMatrixColorFilter; @@ -64,6 +65,7 @@ import androidx.annotation.UiThread; import androidx.constraintlayout.widget.ConstraintSet; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.graphics.ColorUtils; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.InstanceId; import com.android.settingslib.widget.AdaptiveIcon; @@ -97,13 +99,16 @@ import com.android.systemui.monet.ColorScheme; import com.android.systemui.monet.Style; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.FalsingManager; -import com.android.systemui.ripple.MultiRippleController; -import com.android.systemui.ripple.RippleAnimation; -import com.android.systemui.ripple.RippleAnimationConfig; -import com.android.systemui.ripple.RippleShader; import com.android.systemui.shared.system.SysUiStatsLog; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.policy.KeyguardStateController; +import com.android.systemui.surfaceeffects.ripple.MultiRippleController; +import com.android.systemui.surfaceeffects.ripple.MultiRippleView; +import com.android.systemui.surfaceeffects.ripple.RippleAnimation; +import com.android.systemui.surfaceeffects.ripple.RippleAnimationConfig; +import com.android.systemui.surfaceeffects.ripple.RippleShader; +import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseAnimationConfig; +import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseController; import com.android.systemui.util.ColorUtilKt; import com.android.systemui.util.animation.TransitionLayout; import com.android.systemui.util.time.SystemClock; @@ -216,7 +221,9 @@ public class MediaControlPanel { private boolean mShowBroadcastDialogButton = false; private String mSwitchBroadcastApp; private MultiRippleController mMultiRippleController; + private TurbulenceNoiseController mTurbulenceNoiseController; private FeatureFlags mFeatureFlags; + private TurbulenceNoiseAnimationConfig mTurbulenceNoiseAnimationConfig = null; /** * Initialize a new control panel @@ -394,9 +401,20 @@ public class MediaControlPanel { AnimatorSet exit = loadAnimator(R.anim.media_metadata_exit, Interpolators.EMPHASIZED_ACCELERATE, titleText, artistText); - mMultiRippleController = new MultiRippleController(vh.getMultiRippleView()); + MultiRippleView multiRippleView = vh.getMultiRippleView(); + mMultiRippleController = new MultiRippleController(multiRippleView); + mTurbulenceNoiseController = new TurbulenceNoiseController(vh.getTurbulenceNoiseView()); + multiRippleView.addRipplesFinishedListener( + () -> { + if (mTurbulenceNoiseAnimationConfig == null) { + mTurbulenceNoiseAnimationConfig = createLingeringNoiseAnimation(); + } + // Color will be correctly updated in ColorSchemeTransition. + mTurbulenceNoiseController.play(mTurbulenceNoiseAnimationConfig); + } + ); mColorSchemeTransition = new ColorSchemeTransition( - mContext, mMediaViewHolder, mMultiRippleController); + mContext, mMediaViewHolder, mMultiRippleController, mTurbulenceNoiseController); mMetadataAnimationHandler = new MetadataAnimationHandler(exit, enter); } @@ -571,7 +589,10 @@ public class MediaControlPanel { seamlessView.setContentDescription(deviceString); seamlessView.setOnClickListener( v -> { - if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { + if (mFalsingManager.isFalseTap( + mFeatureFlags.isEnabled(Flags.MEDIA_FALSING_PENALTY) + ? FalsingManager.MODERATE_PENALTY : + FalsingManager.LOW_PENALTY)) { return; } @@ -994,7 +1015,10 @@ public class MediaControlPanel { } else { button.setEnabled(true); button.setOnClickListener(v -> { - if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { + if (!mFalsingManager.isFalseTap( + mFeatureFlags.isEnabled(Flags.MEDIA_FALSING_PENALTY) + ? FalsingManager.MODERATE_PENALTY : + FalsingManager.LOW_PENALTY)) { mLogger.logTapAction(button.getId(), mUid, mPackageName, mInstanceId); logSmartspaceCardReported(SMARTSPACE_CARD_CLICK_EVENT); action.run(); @@ -1027,7 +1051,7 @@ public class MediaControlPanel { /* maxWidth= */ maxSize, /* maxHeight= */ maxSize, /* pixelDensity= */ getContext().getResources().getDisplayMetrics().density, - mColorSchemeTransition.getAccentPrimary().getTargetColor(), + mColorSchemeTransition.getAccentPrimary().getCurrentColor(), /* opacity= */ 100, /* shouldFillRipple= */ false, /* sparkleStrength= */ 0f, @@ -1036,6 +1060,26 @@ public class MediaControlPanel { ); } + private TurbulenceNoiseAnimationConfig createLingeringNoiseAnimation() { + return new TurbulenceNoiseAnimationConfig( + TurbulenceNoiseAnimationConfig.DEFAULT_NOISE_GRID_COUNT, + TurbulenceNoiseAnimationConfig.DEFAULT_LUMINOSITY_MULTIPLIER, + /* noiseMoveSpeedX= */ 0f, + /* noiseMoveSpeedY= */ 0f, + TurbulenceNoiseAnimationConfig.DEFAULT_NOISE_SPEED_Z, + /* color= */ mColorSchemeTransition.getAccentPrimary().getCurrentColor(), + // We want to add (BlendMode.PLUS) the turbulence noise on top of the album art. + // Thus, set the background color with alpha 0. + /* backgroundColor= */ ColorUtils.setAlphaComponent(Color.BLACK, 0), + TurbulenceNoiseAnimationConfig.DEFAULT_OPACITY, + /* width= */ mMediaViewHolder.getMultiRippleView().getWidth(), + /* height= */ mMediaViewHolder.getMultiRippleView().getHeight(), + TurbulenceNoiseAnimationConfig.DEFAULT_NOISE_DURATION_IN_MILLIS, + this.getContext().getResources().getDisplayMetrics().density, + BlendMode.PLUS, + /* onAnimationEnd= */ null + ); + } private void clearButton(final ImageButton button) { button.setImageDrawable(null); button.setContentDescription(null); 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 a4a968067462..647beb95a3bc 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 @@ class MediaTttCommandLineHelper @Inject constructor( @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 @@ class MediaTttCommandLineHelper @Inject constructor( 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 @@ class MediaTttCommandLineHelper @Inject constructor( @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 @@ class MediaTttCommandLineHelper @Inject constructor( 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 8bddffc842f5..691953aaba36 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 @@ class MediaTttChipControllerReceiver @Inject constructor( 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 @@ data class ChipReceiverInfo( 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/receiver/ReceiverChipRippleView.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt index e354a03f1725..1ea202582f83 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt @@ -18,8 +18,8 @@ package com.android.systemui.media.taptotransfer.receiver import android.content.Context import android.util.AttributeSet -import com.android.systemui.ripple.RippleShader -import com.android.systemui.ripple.RippleView +import com.android.systemui.surfaceeffects.ripple.RippleShader +import com.android.systemui.surfaceeffects.ripple.RippleView /** * An expanding ripple effect for the media tap-to-transfer receiver chip. 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 d1ea2d0c83bd..bb7bc6fff99f 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 @@ constructor( } displayedState = null - chipbarCoordinator.removeView(removalReason) + chipbarCoordinator.removeView(routeInfo.id, removalReason) } else { displayedState = chipState chipbarCoordinator.displayView( @@ -162,6 +162,7 @@ constructor( 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/mediaprojection/appselector/data/RecentTaskListProvider.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTaskListProvider.kt index e8b49cd8ec1c..7a77c476aa11 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTaskListProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTaskListProvider.kt @@ -16,19 +16,19 @@ package com.android.systemui.mediaprojection.appselector.data -import android.app.ActivityManager import android.app.ActivityManager.RECENT_IGNORE_UNAVAILABLE import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.settings.UserTracker import com.android.systemui.util.kotlin.getOrNull import com.android.wm.shell.recents.RecentTasks import com.android.wm.shell.util.GroupedRecentTaskInfo import java.util.Optional +import java.util.concurrent.Executor import javax.inject.Inject import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext -import java.util.concurrent.Executor interface RecentTaskListProvider { /** Loads recent tasks, the returned task list is from the most-recent to least-recent order */ @@ -40,7 +40,8 @@ class ShellRecentTaskListProvider constructor( @Background private val coroutineDispatcher: CoroutineDispatcher, @Background private val backgroundExecutor: Executor, - private val recentTasks: Optional<RecentTasks> + private val recentTasks: Optional<RecentTasks>, + private val userTracker: UserTracker ) : RecentTaskListProvider { private val recents by lazy { recentTasks.getOrNull() } @@ -67,10 +68,8 @@ constructor( getRecentTasks( Integer.MAX_VALUE, RECENT_IGNORE_UNAVAILABLE, - ActivityManager.getCurrentUser(), + userTracker.userId, backgroundExecutor - ) { tasks -> - continuation.resume(tasks) - } + ) { tasks -> continuation.resume(tasks) } } } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarInflaterView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarInflaterView.java index 59bb2278edfe..2a7704f13c3b 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarInflaterView.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarInflaterView.java @@ -45,11 +45,10 @@ import com.android.systemui.recents.OverviewProxyService; import com.android.systemui.shared.system.QuickStepContract; import java.io.PrintWriter; +import java.lang.ref.WeakReference; import java.util.Objects; -public class NavigationBarInflaterView extends FrameLayout - implements NavigationModeController.ModeChangedListener { - +public class NavigationBarInflaterView extends FrameLayout { private static final String TAG = "NavBarInflater"; public static final String NAV_BAR_VIEWS = "sysui_nav_bar"; @@ -83,6 +82,24 @@ public class NavigationBarInflaterView extends FrameLayout private static final String ABSOLUTE_SUFFIX = "A"; private static final String ABSOLUTE_VERTICAL_CENTERED_SUFFIX = "C"; + private static class Listener implements NavigationModeController.ModeChangedListener { + private final WeakReference<NavigationBarInflaterView> mSelf; + + Listener(NavigationBarInflaterView self) { + mSelf = new WeakReference<>(self); + } + + @Override + public void onNavigationModeChanged(int mode) { + NavigationBarInflaterView self = mSelf.get(); + if (self != null) { + self.onNavigationModeChanged(mode); + } + } + } + + private final Listener mListener; + protected LayoutInflater mLayoutInflater; protected LayoutInflater mLandscapeInflater; @@ -106,7 +123,8 @@ public class NavigationBarInflaterView extends FrameLayout super(context, attrs); createInflaters(); mOverviewProxyService = Dependency.get(OverviewProxyService.class); - mNavBarMode = Dependency.get(NavigationModeController.class).addListener(this); + mListener = new Listener(this); + mNavBarMode = Dependency.get(NavigationModeController.class).addListener(mListener); } @VisibleForTesting @@ -146,14 +164,13 @@ public class NavigationBarInflaterView extends FrameLayout return getContext().getString(defaultResource); } - @Override - public void onNavigationModeChanged(int mode) { + private void onNavigationModeChanged(int mode) { mNavBarMode = mode; } @Override protected void onDetachedFromWindow() { - Dependency.get(NavigationModeController.class).removeListener(this); + Dependency.get(NavigationModeController.class).removeListener(mListener); super.onDetachedFromWindow(); } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java index 7964d164dc3a..4e3831cce374 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java @@ -59,7 +59,6 @@ import android.window.BackEvent; import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.internal.policy.GestureNavigationSettingsObserver; import com.android.systemui.R; -import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.flags.FeatureFlags; @@ -71,7 +70,7 @@ import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.NavigationEdgeBackPlugin; import com.android.systemui.plugins.PluginListener; import com.android.systemui.recents.OverviewProxyService; -import com.android.systemui.settings.CurrentUserTracker; +import com.android.systemui.settings.UserTracker; import com.android.systemui.shared.plugins.PluginManager; import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.InputChannelCompat; @@ -102,8 +101,8 @@ import javax.inject.Provider; /** * Utility class to handle edge swipes for back gesture */ -public class EdgeBackGestureHandler extends CurrentUserTracker - implements PluginListener<NavigationEdgeBackPlugin>, ProtoTraceable<SystemUiTraceProto> { +public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBackPlugin>, + ProtoTraceable<SystemUiTraceProto> { private static final String TAG = "EdgeBackGestureHandler"; private static final int MAX_LONG_PRESS_TIMEOUT = SystemProperties.getInt( @@ -172,6 +171,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker private final Context mContext; + private final UserTracker mUserTracker; private final OverviewProxyService mOverviewProxyService; private final SysUiState mSysUiState; private Runnable mStateChangeCallback; @@ -321,6 +321,15 @@ public class EdgeBackGestureHandler extends CurrentUserTracker private final Consumer<Boolean> mOnIsInPipStateChangedListener = (isInPip) -> mIsInPip = isInPip; + private final UserTracker.Callback mUserChangedCallback = + new UserTracker.Callback() { + @Override + public void onUserChanged(int newUser, @NonNull Context userContext) { + updateIsEnabled(); + updateCurrentUserResources(); + } + }; + EdgeBackGestureHandler( Context context, OverviewProxyService overviewProxyService, @@ -328,7 +337,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker PluginManager pluginManager, @Main Executor executor, @Background Executor backgroundExecutor, - BroadcastDispatcher broadcastDispatcher, + UserTracker userTracker, ProtoTracer protoTracer, NavigationModeController navigationModeController, BackPanelController.Factory backPanelControllerFactory, @@ -340,11 +349,11 @@ public class EdgeBackGestureHandler extends CurrentUserTracker Provider<NavigationBarEdgePanel> navigationBarEdgePanelProvider, Provider<BackGestureTfClassifierProvider> backGestureTfClassifierProviderProvider, FeatureFlags featureFlags) { - super(broadcastDispatcher); mContext = context; mDisplayId = context.getDisplayId(); mMainExecutor = executor; mBackgroundExecutor = backgroundExecutor; + mUserTracker = userTracker; mOverviewProxyService = overviewProxyService; mSysUiState = sysUiState; mPluginManager = pluginManager; @@ -463,12 +472,6 @@ public class EdgeBackGestureHandler extends CurrentUserTracker } } - @Override - public void onUserSwitched(int newUserId) { - updateIsEnabled(); - updateCurrentUserResources(); - } - /** * @see NavigationBarView#onAttachedToWindow() */ @@ -478,7 +481,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker mOverviewProxyService.addCallback(mQuickSwitchListener); mSysUiState.addCallback(mSysUiStateCallback); updateIsEnabled(); - startTracking(); + mUserTracker.addCallback(mUserChangedCallback, mMainExecutor); } /** @@ -490,7 +493,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker mOverviewProxyService.removeCallback(mQuickSwitchListener); mSysUiState.removeCallback(mSysUiStateCallback); updateIsEnabled(); - stopTracking(); + mUserTracker.removeCallback(mUserChangedCallback); } /** @@ -1093,7 +1096,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker private final PluginManager mPluginManager; private final Executor mExecutor; private final Executor mBackgroundExecutor; - private final BroadcastDispatcher mBroadcastDispatcher; + private final UserTracker mUserTracker; private final ProtoTracer mProtoTracer; private final NavigationModeController mNavigationModeController; private final BackPanelController.Factory mBackPanelControllerFactory; @@ -1113,7 +1116,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker PluginManager pluginManager, @Main Executor executor, @Background Executor backgroundExecutor, - BroadcastDispatcher broadcastDispatcher, + UserTracker userTracker, ProtoTracer protoTracer, NavigationModeController navigationModeController, BackPanelController.Factory backPanelControllerFactory, @@ -1131,7 +1134,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker mPluginManager = pluginManager; mExecutor = executor; mBackgroundExecutor = backgroundExecutor; - mBroadcastDispatcher = broadcastDispatcher; + mUserTracker = userTracker; mProtoTracer = protoTracer; mNavigationModeController = navigationModeController; mBackPanelControllerFactory = backPanelControllerFactory; @@ -1154,7 +1157,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker mPluginManager, mExecutor, mBackgroundExecutor, - mBroadcastDispatcher, + mUserTracker, mProtoTracer, mNavigationModeController, mBackPanelControllerFactory, diff --git a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java index 0697133a02f9..f92bbf75d027 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java +++ b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java @@ -364,13 +364,18 @@ public class PagedTileLayout extends ViewPager implements QSTileLayout { private void distributeTiles() { emptyAndInflateOrRemovePages(); - final int tileCount = mPages.get(0).maxTiles(); - if (DEBUG) Log.d(TAG, "Distributing tiles"); + final int tilesPerPageCount = mPages.get(0).maxTiles(); int index = 0; - final int NT = mTiles.size(); - for (int i = 0; i < NT; i++) { + final int totalTilesCount = mTiles.size(); + if (DEBUG) { + Log.d(TAG, "Distributing tiles: " + + "[tilesPerPageCount=" + tilesPerPageCount + "]" + + "[totalTilesCount=" + totalTilesCount + "]" + ); + } + for (int i = 0; i < totalTilesCount; i++) { TileRecord tile = mTiles.get(i); - if (mPages.get(index).mRecords.size() == tileCount) index++; + if (mPages.get(index).mRecords.size() == tilesPerPageCount) index++; if (DEBUG) { Log.d(TAG, "Adding " + tile.tile.getClass().getSimpleName() + " to " + index); @@ -577,8 +582,8 @@ public class PagedTileLayout extends ViewPager implements QSTileLayout { }); setOffscreenPageLimit(lastPageNumber); // Ensure the page to reveal has been inflated. int dx = getWidth() * lastPageNumber; - mScroller.startScroll(getScrollX(), getScrollY(), isLayoutRtl() ? -dx : dx, 0, - REVEAL_SCROLL_DURATION_MILLIS); + mScroller.startScroll(getScrollX(), getScrollY(), isLayoutRtl() ? -dx : dx, 0, + REVEAL_SCROLL_DURATION_MILLIS); postInvalidateOnAnimation(); } @@ -738,6 +743,7 @@ public class PagedTileLayout extends ViewPager implements QSTileLayout { public interface PageListener { int INVALID_PAGE = -1; + void onPageChanged(boolean isFirst, int pageNumber); } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java index 920a108382fb..d9be2810d165 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 class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca 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 2a80de0e24de..dd88c83949fb 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.ComponentName; 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.plugins.qs.QSTileView; 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 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr 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/SettingObserver.java b/packages/SystemUI/src/com/android/systemui/qs/SettingObserver.java index 6b0abd41dfff..7794fa071f45 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/SettingObserver.java +++ b/packages/SystemUI/src/com/android/systemui/qs/SettingObserver.java @@ -16,7 +16,6 @@ package com.android.systemui.qs; -import android.app.ActivityManager; import android.database.ContentObserver; import android.os.Handler; @@ -47,10 +46,6 @@ public abstract class SettingObserver extends ContentObserver implements Listena this(settingsProxy, handler, settingName, userId, 0); } - public SettingObserver(SettingsProxy settingsProxy, Handler handler, String settingName) { - this(settingsProxy, handler, settingName, ActivityManager.getCurrentUser()); - } - public SettingObserver(SettingsProxy settingsProxy, Handler handler, String settingName, int userId, int defaultValue) { super(handler); diff --git a/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java index 3d00dd49cb1c..7ee404756633 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java +++ b/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java @@ -123,7 +123,6 @@ public class TileLayout extends ViewGroup implements QSTileLayout { public boolean updateResources() { final Resources res = mContext.getResources(); mResourceColumns = Math.max(1, res.getInteger(R.integer.quick_settings_num_columns)); - updateColumns(); mMaxCellHeight = mContext.getResources().getDimensionPixelSize(mCellHeightResId); mCellMarginHorizontal = res.getDimensionPixelSize(R.dimen.qs_tile_margin_horizontal); mSidePadding = useSidePadding() ? mCellMarginHorizontal / 2 : 0; diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java index cf10c7940871..79fcc7d81372 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java +++ b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java @@ -82,12 +82,12 @@ public class QSCustomizer extends LinearLayout { DefaultItemAnimator animator = new DefaultItemAnimator(); animator.setMoveDuration(TileAdapter.MOVE_DURATION); mRecyclerView.setItemAnimator(animator); + + updateTransparentViewHeight(); } void updateResources() { - LayoutParams lp = (LayoutParams) mTransparentView.getLayoutParams(); - lp.height = QSUtils.getQsHeaderSystemIconsAreaHeight(mContext); - mTransparentView.setLayoutParams(lp); + updateTransparentViewHeight(); mRecyclerView.getAdapter().notifyItemChanged(0); } @@ -236,4 +236,10 @@ public class QSCustomizer extends LinearLayout { public boolean isOpening() { return mOpening; } + + private void updateTransparentViewHeight() { + LayoutParams lp = (LayoutParams) mTransparentView.getLayoutParams(); + lp.height = QSUtils.getQsHeaderSystemIconsAreaHeight(mContext); + mTransparentView.setLayoutParams(lp); + } }
\ No newline at end of file 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 931dc8df151a..9f6317fd931b 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 @@ class QSLogger @Inject constructor( }) } - fun logInternetTileUpdate(lastType: Int, callback: String) { + fun logInternetTileUpdate(tileSpec: String, lastType: Int, callback: String) { log(VERBOSE, { + str1 = tileSpec int1 = lastType - str1 = callback + str2 = callback + }, { + "[$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 }, { - "mLastTileState=$int1, Callback=$str1." + "[$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 972b24343d10..b355d4bb67fe 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.QSIconView 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 @@ open class QSTileViewImpl @JvmOverloads constructor( 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 @@ open class QSTileViewImpl @JvmOverloads constructor( 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 @@ open class QSTileViewImpl @JvmOverloads constructor( // 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/AirplaneModeTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/AirplaneModeTile.java index 86d4fa3002fc..033dbe0f82ee 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/AirplaneModeTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/AirplaneModeTile.java @@ -48,6 +48,7 @@ import com.android.systemui.qs.QSHost; import com.android.systemui.qs.SettingObserver; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.tileimpl.QSTileImpl; +import com.android.systemui.settings.UserTracker; import com.android.systemui.util.settings.GlobalSettings; import javax.inject.Inject; @@ -74,14 +75,16 @@ public class AirplaneModeTile extends QSTileImpl<BooleanState> { QSLogger qsLogger, BroadcastDispatcher broadcastDispatcher, Lazy<ConnectivityManager> lazyConnectivityManager, - GlobalSettings globalSettings + GlobalSettings globalSettings, + UserTracker userTracker ) { super(host, backgroundLooper, mainHandler, falsingManager, metricsLogger, statusBarStateController, activityStarter, qsLogger); mBroadcastDispatcher = broadcastDispatcher; mLazyConnectivityManager = lazyConnectivityManager; - mSetting = new SettingObserver(globalSettings, mHandler, Global.AIRPLANE_MODE_ON) { + mSetting = new SettingObserver(globalSettings, mHandler, Global.AIRPLANE_MODE_ON, + userTracker.getUserId()) { @Override protected void handleValueChanged(int value, boolean observedChange) { // mHandler is the background handler so calling this is OK 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 bebd5803fabf..5bc209a840cb 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 @@ public class DreamTile extends QSTileImpl<QSTile.BooleanState> { 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,22 +100,22 @@ public class DreamTile extends QSTileImpl<QSTile.BooleanState> { 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); mDreamManager = dreamManager; mBroadcastDispatcher = broadcastDispatcher; mEnabledSettingObserver = new SettingObserver(secureSettings, mHandler, - Settings.Secure.SCREENSAVER_ENABLED) { + Settings.Secure.SCREENSAVER_ENABLED, userTracker.getUserId()) { @Override protected void handleValueChanged(int value, boolean observedChange) { refreshState(); } }; mDreamSettingObserver = new SettingObserver(secureSettings, mHandler, - Settings.Secure.SCREENSAVER_COMPONENTS) { + Settings.Secure.SCREENSAVER_COMPONENTS, userTracker.getUserId()) { @Override protected void handleValueChanged(int value, boolean observedChange) { refreshState(); @@ -123,7 +123,7 @@ public class DreamTile extends QSTileImpl<QSTile.BooleanState> { }; mUserTracker = userTracker; mDreamSupported = dreamSupported; - mDreamOnlyEnabledForSystemUser = dreamOnlyEnabledForSystemUser; + mDreamOnlyEnabledForDockUser = dreamOnlyEnabledForDockUser; } @Override @@ -203,7 +203,8 @@ public class DreamTile extends QSTileImpl<QSTile.BooleanState> { // 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 ae464771bf48..350d8b05dde5 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 @@ public class InternetTile extends QSTileImpl<SignalState> { @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/qs/tiles/dialog/InternetDialog.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java index a895d72a7492..9743c3e64950 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java @@ -249,15 +249,7 @@ public class InternetDialog extends SystemUIDialog implements mBackgroundOn = mContext.getDrawable(R.drawable.settingslib_switch_bar_bg_on); mInternetDialogTitle.setText(getDialogTitleText()); mInternetDialogTitle.setGravity(Gravity.START | Gravity.CENTER_VERTICAL); - - TypedArray typedArray = mContext.obtainStyledAttributes( - new int[]{android.R.attr.selectableItemBackground}); - try { - mBackgroundOff = typedArray.getDrawable(0 /* index */); - } finally { - typedArray.recycle(); - } - + mBackgroundOff = mContext.getDrawable(R.drawable.internet_dialog_selected_effect); setOnClickListener(); mTurnWifiOnLayout.setBackground(null); mAirplaneModeButton.setVisibility( diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java index ba97297421b3..547b496beaff 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java @@ -78,8 +78,8 @@ import com.android.internal.logging.UiEventLogger; import com.android.internal.policy.ScreenDecorationsUtils; import com.android.internal.util.ScreenshotHelper; import com.android.systemui.Dumpable; -import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.ScreenLifecycle; @@ -90,12 +90,11 @@ import com.android.systemui.navigationbar.NavigationBarView; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.navigationbar.buttons.KeyButtonView; import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener; -import com.android.systemui.settings.CurrentUserTracker; +import com.android.systemui.settings.UserTracker; import com.android.systemui.shade.NotificationPanelViewController; import com.android.systemui.shared.recents.IOverviewProxy; import com.android.systemui.shared.recents.ISystemUiProxy; import com.android.systemui.shared.recents.model.Task; -import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.NotificationShadeWindowController; @@ -108,20 +107,19 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.concurrent.Executor; import java.util.function.Supplier; import javax.inject.Inject; import dagger.Lazy; - /** * Class to send information from overview to launcher with a binder. */ @SysUISingleton -public class OverviewProxyService extends CurrentUserTracker implements - CallbackController<OverviewProxyListener>, NavigationModeController.ModeChangedListener, - Dumpable { +public class OverviewProxyService implements CallbackController<OverviewProxyListener>, + NavigationModeController.ModeChangedListener, Dumpable { private static final String ACTION_QUICKSTEP = "android.intent.action.QUICKSTEP_SERVICE"; @@ -133,6 +131,7 @@ public class OverviewProxyService extends CurrentUserTracker implements private static final long MAX_BACKOFF_MILLIS = 10 * 60 * 1000; private final Context mContext; + private final Executor mMainExecutor; private final ShellInterface mShellInterface; private final Lazy<Optional<CentralSurfaces>> mCentralSurfacesOptionalLazy; private SysUiState mSysUiState; @@ -145,6 +144,7 @@ public class OverviewProxyService extends CurrentUserTracker implements private final Intent mQuickStepIntent; private final ScreenshotHelper mScreenshotHelper; private final CommandQueue mCommandQueue; + private final UserTracker mUserTracker; private final KeyguardUnlockAnimationController mSysuiUnlockAnimationController; private final UiEventLogger mUiEventLogger; @@ -417,7 +417,7 @@ public class OverviewProxyService extends CurrentUserTracker implements return; } - mCurrentBoundedUserId = getCurrentUserId(); + mCurrentBoundedUserId = mUserTracker.getUserId(); mOverviewProxy = IOverviewProxy.Stub.asInterface(service); Bundle params = new Bundle(); @@ -498,34 +498,44 @@ public class OverviewProxyService extends CurrentUserTracker implements } }; + private final UserTracker.Callback mUserChangedCallback = + new UserTracker.Callback() { + @Override + public void onUserChanged(int newUser, @NonNull Context userContext) { + mConnectionBackoffAttempts = 0; + internalConnectToCurrentUser(); + } + }; + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") @Inject public OverviewProxyService(Context context, + @Main Executor mainExecutor, CommandQueue commandQueue, ShellInterface shellInterface, Lazy<NavigationBarController> navBarControllerLazy, Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy, NavigationModeController navModeController, NotificationShadeWindowController statusBarWinController, SysUiState sysUiState, - BroadcastDispatcher broadcastDispatcher, + UserTracker userTracker, ScreenLifecycle screenLifecycle, UiEventLogger uiEventLogger, KeyguardUnlockAnimationController sysuiUnlockAnimationController, AssistUtils assistUtils, DumpManager dumpManager) { - super(broadcastDispatcher); - // b/241601880: This component shouldn't be running for a non-primary user if (!Process.myUserHandle().equals(UserHandle.SYSTEM)) { Log.e(TAG_OPS, "Unexpected initialization for non-primary user", new Throwable()); } mContext = context; + mMainExecutor = mainExecutor; mShellInterface = shellInterface; mCentralSurfacesOptionalLazy = centralSurfacesOptionalLazy; mHandler = new Handler(); mNavBarControllerLazy = navBarControllerLazy; mStatusBarWinController = statusBarWinController; + mUserTracker = userTracker; mConnectionBackoffAttempts = 0; mRecentsComponentName = ComponentName.unflattenFromString(context.getString( com.android.internal.R.string.config_recentsComponentName)); @@ -566,7 +576,7 @@ public class OverviewProxyService extends CurrentUserTracker implements mCommandQueue = commandQueue; // Listen for user setup - startTracking(); + mUserTracker.addCallback(mUserChangedCallback, mMainExecutor); screenLifecycle.addObserver(mLifecycleObserver); @@ -579,12 +589,6 @@ public class OverviewProxyService extends CurrentUserTracker implements assistUtils.registerVoiceInteractionSessionListener(mVoiceInteractionSessionListener); } - @Override - public void onUserSwitched(int newUserId) { - mConnectionBackoffAttempts = 0; - internalConnectToCurrentUser(); - } - public void onVoiceSessionWindowVisibilityChanged(boolean visible) { mSysUiState.setFlag(SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING, visible) .commitUpdate(mContext.getDisplayId()); @@ -712,7 +716,7 @@ public class OverviewProxyService extends CurrentUserTracker implements mBound = mContext.bindServiceAsUser(launcherServiceIntent, mOverviewServiceConnection, Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE, - UserHandle.of(getCurrentUserId())); + UserHandle.of(mUserTracker.getUserId())); } catch (SecurityException e) { Log.e(TAG_OPS, "Unable to bind because of security error", e); } @@ -941,7 +945,7 @@ public class OverviewProxyService extends CurrentUserTracker implements } private void updateEnabledState() { - final int currentUser = ActivityManagerWrapper.getInstance().getCurrentUserId(); + final int currentUser = mUserTracker.getUserId(); mIsEnabled = mContext.getPackageManager().resolveServiceAsUser(mQuickStepIntent, MATCH_SYSTEM_ONLY, currentUser) != null; } diff --git a/packages/SystemUI/src/com/android/systemui/ripple/RippleShaderUtilLibrary.kt b/packages/SystemUI/src/com/android/systemui/ripple/RippleShaderUtilLibrary.kt deleted file mode 100644 index 6de46483892b..000000000000 --- a/packages/SystemUI/src/com/android/systemui/ripple/RippleShaderUtilLibrary.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.systemui.ripple - -/** A common utility functions that are used for computing [RippleShader]. */ -class RippleShaderUtilLibrary { - //language=AGSL - companion object { - const val SHADER_LIB = """ - float triangleNoise(vec2 n) { - n = fract(n * vec2(5.3987, 5.4421)); - n += dot(n.yx, n.xy + vec2(21.5351, 14.3137)); - float xy = n.x * n.y; - return fract(xy * 95.4307) + fract(xy * 75.04961) - 1.0; - } - const float PI = 3.1415926535897932384626; - - float sparkles(vec2 uv, float t) { - float n = triangleNoise(uv); - float s = 0.0; - for (float i = 0; i < 4; i += 1) { - float l = i * 0.01; - float h = l + 0.1; - float o = smoothstep(n - l, h, n); - o *= abs(sin(PI * o * (t + 0.55 * i))); - s += o; - } - return s; - } - - vec2 distort(vec2 p, float time, float distort_amount_radial, - float distort_amount_xy) { - float angle = atan(p.y, p.x); - return p + vec2(sin(angle * 8 + time * 0.003 + 1.641), - cos(angle * 5 + 2.14 + time * 0.00412)) * distort_amount_radial - + vec2(sin(p.x * 0.01 + time * 0.00215 + 0.8123), - cos(p.y * 0.01 + time * 0.005931)) * distort_amount_xy; - }""" - } -} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt index 5961635a0dba..01e32b7ada5f 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 android.view.WindowManagerGlobal 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 @@ class ActionIntentExecutor @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 @@ constructor( 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 8bf956b86683..5450db98af52 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.internal.logging.UiEventLogger; 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 @@ public class LongScreenshotActivity extends Activity { 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 @@ public class LongScreenshotActivity extends Activity { 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 @@ public class LongScreenshotActivity extends Activity { private CropView mCropView; private MagnifierView mMagnifierView; private ScrollCaptureResponse mScrollCaptureResponse; + private UserHandle mScreenshotUserHandle; private File mSavedImagePath; private ListenableFuture<File> mCacheSaveFuture; @@ -103,12 +109,15 @@ public class LongScreenshotActivity extends Activity { @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 @@ public class LongScreenshotActivity extends Activity { 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 @@ public class LongScreenshotActivity extends Activity { } 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)); + 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()); } - 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()); } 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); - - startActivityAsUser(sharingChooserIntent, UserHandle.CURRENT); + 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); + } } private void onClicked(View v) { @@ -389,8 +418,8 @@ public class LongScreenshotActivity extends Activity { 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 d395bd33241d..d94c8277b82c 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 @@ public class ScreenshotController { // 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 @@ public class ScreenshotController { 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 @@ public class ScreenshotController { 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 @@ public class ScreenshotController { 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 @@ public class ScreenshotController { 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 @@ public class ScreenshotController { 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 @@ public class ScreenshotController { 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 8b5a24c0e2ff..c891686ada8f 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 @@ public enum ScreenshotEvent implements UiEventLogger.UiEventEnum { @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/settings/CurrentUserObservable.java b/packages/SystemUI/src/com/android/systemui/settings/CurrentUserObservable.java deleted file mode 100644 index dea8c32dc88d..000000000000 --- a/packages/SystemUI/src/com/android/systemui/settings/CurrentUserObservable.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.settings; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; - -import com.android.systemui.broadcast.BroadcastDispatcher; - -/** - * A class that has an observable for the current user. - */ -public class CurrentUserObservable { - - private final CurrentUserTracker mTracker; - - private final MutableLiveData<Integer> mCurrentUser = new MutableLiveData<Integer>() { - @Override - protected void onActive() { - super.onActive(); - mTracker.startTracking(); - } - - @Override - protected void onInactive() { - super.onInactive(); - mTracker.stopTracking(); - } - }; - - public CurrentUserObservable(BroadcastDispatcher broadcastDispatcher) { - mTracker = new CurrentUserTracker(broadcastDispatcher) { - @Override - public void onUserSwitched(int newUserId) { - mCurrentUser.setValue(newUserId); - } - }; - } - - /** - * Returns the current user that can be observed. - */ - public LiveData<Integer> getCurrentUser() { - if (mCurrentUser.getValue() == null) { - mCurrentUser.setValue(mTracker.getCurrentUserId()); - } - return mCurrentUser; - } -} diff --git a/packages/SystemUI/src/com/android/systemui/settings/CurrentUserTracker.java b/packages/SystemUI/src/com/android/systemui/settings/CurrentUserTracker.java deleted file mode 100644 index 9599d77bf65a..000000000000 --- a/packages/SystemUI/src/com/android/systemui/settings/CurrentUserTracker.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright (C) 2013 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.settings; - -import android.app.ActivityManager; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.UserHandle; - -import com.android.internal.annotations.VisibleForTesting; -import com.android.systemui.broadcast.BroadcastDispatcher; - -import java.util.ArrayList; -import java.util.List; -import java.util.function.Consumer; - -public abstract class CurrentUserTracker { - private final UserReceiver mUserReceiver; - - private Consumer<Integer> mCallback = this::onUserSwitched; - - public CurrentUserTracker(BroadcastDispatcher broadcastDispatcher) { - this(UserReceiver.getInstance(broadcastDispatcher)); - } - - @VisibleForTesting - CurrentUserTracker(UserReceiver receiver) { - mUserReceiver = receiver; - } - - public int getCurrentUserId() { - return mUserReceiver.getCurrentUserId(); - } - - public void startTracking() { - mUserReceiver.addTracker(mCallback); - } - - public void stopTracking() { - mUserReceiver.removeTracker(mCallback); - } - - public abstract void onUserSwitched(int newUserId); - - @VisibleForTesting - static class UserReceiver extends BroadcastReceiver { - private static UserReceiver sInstance; - - private boolean mReceiverRegistered; - private int mCurrentUserId; - private final BroadcastDispatcher mBroadcastDispatcher; - - private List<Consumer<Integer>> mCallbacks = new ArrayList<>(); - - @VisibleForTesting - UserReceiver(BroadcastDispatcher broadcastDispatcher) { - mBroadcastDispatcher = broadcastDispatcher; - } - - static UserReceiver getInstance(BroadcastDispatcher broadcastDispatcher) { - if (sInstance == null) { - sInstance = new UserReceiver(broadcastDispatcher); - } - return sInstance; - } - - @Override - public void onReceive(Context context, Intent intent) { - if (Intent.ACTION_USER_SWITCHED.equals(intent.getAction())) { - notifyUserSwitched(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0)); - } - } - - public int getCurrentUserId() { - return mCurrentUserId; - } - - private void addTracker(Consumer<Integer> callback) { - if (!mCallbacks.contains(callback)) { - mCallbacks.add(callback); - } - if (!mReceiverRegistered) { - mCurrentUserId = ActivityManager.getCurrentUser(); - IntentFilter filter = new IntentFilter(Intent.ACTION_USER_SWITCHED); - mBroadcastDispatcher.registerReceiver(this, filter, null, - UserHandle.ALL); - mReceiverRegistered = true; - } - } - - private void removeTracker(Consumer<Integer> callback) { - if (mCallbacks.contains(callback)) { - mCallbacks.remove(callback); - if (mCallbacks.size() == 0 && mReceiverRegistered) { - mBroadcastDispatcher.unregisterReceiver(this); - mReceiverRegistered = false; - } - } - } - - private void notifyUserSwitched(int newUserId) { - if (mCurrentUserId != newUserId) { - mCurrentUserId = newUserId; - List<Consumer<Integer>> callbacks = new ArrayList<>(mCallbacks); - for (Consumer<Integer> consumer : callbacks) { - // Accepting may modify this list - if (mCallbacks.contains(consumer)) { - consumer.accept(newUserId); - } - } - } - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java index 7801c68586f1..5880003cdb1a 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java @@ -21,6 +21,7 @@ import static com.android.settingslib.display.BrightnessUtils.convertGammaToLine import static com.android.settingslib.display.BrightnessUtils.convertLinearToGammaFloat; import android.animation.ValueAnimator; +import android.annotation.NonNull; import android.content.ContentResolver; import android.content.Context; import android.database.ContentObserver; @@ -46,11 +47,13 @@ import com.android.internal.display.BrightnessSynchronizer; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settingslib.RestrictedLockUtilsInternal; -import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.settings.CurrentUserTracker; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.policy.BrightnessMirrorController; +import java.util.concurrent.Executor; + import javax.inject.Inject; public class BrightnessController implements ToggleSlider.Listener, MirroredBrightnessController { @@ -74,9 +77,10 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig private final Context mContext; private final ToggleSlider mControl; private final DisplayManager mDisplayManager; - private final CurrentUserTracker mUserTracker; + private final UserTracker mUserTracker; private final IVrManager mVrManager; + private final Executor mMainExecutor; private final Handler mBackgroundHandler; private final BrightnessObserver mBrightnessObserver; @@ -169,7 +173,7 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig } mBrightnessObserver.startObserving(); - mUserTracker.startTracking(); + mUserTracker.addCallback(mUserChangedCallback, mMainExecutor); // Update the slider and mode before attaching the listener so we don't // receive the onChanged notifications for the initial values. @@ -197,7 +201,7 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig } mBrightnessObserver.stopObserving(); - mUserTracker.stopTracking(); + mUserTracker.removeCallback(mUserChangedCallback); mHandler.sendEmptyMessage(MSG_DETACH_LISTENER); } @@ -275,22 +279,27 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig } }; + private final UserTracker.Callback mUserChangedCallback = + new UserTracker.Callback() { + @Override + public void onUserChanged(int newUser, @NonNull Context userContext) { + mBackgroundHandler.post(mUpdateModeRunnable); + mBackgroundHandler.post(mUpdateSliderRunnable); + } + }; + public BrightnessController( Context context, ToggleSlider control, - BroadcastDispatcher broadcastDispatcher, + UserTracker userTracker, + @Main Executor mainExecutor, @Background Handler bgHandler) { mContext = context; mControl = control; mControl.setMax(GAMMA_SPACE_MAX); + mMainExecutor = mainExecutor; mBackgroundHandler = bgHandler; - mUserTracker = new CurrentUserTracker(broadcastDispatcher) { - @Override - public void onUserSwitched(int newUserId) { - mBackgroundHandler.post(mUpdateModeRunnable); - mBackgroundHandler.post(mUpdateSliderRunnable); - } - }; + mUserTracker = userTracker; mBrightnessObserver = new BrightnessObserver(mHandler); mDisplayId = mContext.getDisplayId(); @@ -364,7 +373,7 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig mControl.setEnforcedAdmin( RestrictedLockUtilsInternal.checkIfRestrictionEnforced(mContext, UserManager.DISALLOW_CONFIG_BRIGHTNESS, - mUserTracker.getCurrentUserId())); + mUserTracker.getUserId())); } }); } @@ -440,16 +449,19 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig /** Factory for creating a {@link BrightnessController}. */ public static class Factory { private final Context mContext; - private final BroadcastDispatcher mBroadcastDispatcher; + private final UserTracker mUserTracker; + private final Executor mMainExecutor; private final Handler mBackgroundHandler; @Inject public Factory( Context context, - BroadcastDispatcher broadcastDispatcher, + UserTracker userTracker, + @Main Executor mainExecutor, @Background Handler bgHandler) { mContext = context; - mBroadcastDispatcher = broadcastDispatcher; + mUserTracker = userTracker; + mMainExecutor = mainExecutor; mBackgroundHandler = bgHandler; } @@ -458,7 +470,8 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig return new BrightnessController( mContext, toggleSlider, - mBroadcastDispatcher, + mUserTracker, + mMainExecutor, mBackgroundHandler); } } diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java index d5a395436271..e208be957510 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java @@ -34,10 +34,12 @@ import android.widget.FrameLayout; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.systemui.R; -import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.settings.UserTracker; import java.util.List; +import java.util.concurrent.Executor; import javax.inject.Inject; @@ -46,16 +48,19 @@ public class BrightnessDialog extends Activity { private BrightnessController mBrightnessController; private final BrightnessSliderController.Factory mToggleSliderFactory; - private final BroadcastDispatcher mBroadcastDispatcher; + private final UserTracker mUserTracker; + private final Executor mMainExecutor; private final Handler mBackgroundHandler; @Inject public BrightnessDialog( - BroadcastDispatcher broadcastDispatcher, + UserTracker userTracker, BrightnessSliderController.Factory factory, + @Main Executor mainExecutor, @Background Handler bgHandler) { - mBroadcastDispatcher = broadcastDispatcher; + mUserTracker = userTracker; mToggleSliderFactory = factory; + mMainExecutor = mainExecutor; mBackgroundHandler = bgHandler; } @@ -101,7 +106,7 @@ public class BrightnessDialog extends Activity { frame.addView(controller.getRootView(), MATCH_PARENT, WRAP_CONTENT); mBrightnessController = new BrightnessController( - this, controller, mBroadcastDispatcher, mBackgroundHandler); + this, controller, mUserTracker, mMainExecutor, mBackgroundHandler); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt index 4063af3cbc36..5011227ad2cc 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt @@ -51,6 +51,8 @@ object CombinedShadeHeadersConstraintManagerImpl : CombinedShadeHeadersConstrain connect(R.id.statusIcons, ConstraintSet.START, R.id.date, ConstraintSet.END) connect(R.id.privacy_container, ConstraintSet.START, R.id.date, ConstraintSet.END) constrainWidth(R.id.statusIcons, ViewGroup.LayoutParams.WRAP_CONTENT) + constrainedWidth(R.id.date, true) + constrainedWidth(R.id.statusIcons, true) } ) } @@ -92,7 +94,8 @@ object CombinedShadeHeadersConstraintManagerImpl : CombinedShadeHeadersConstrain centerEnd, ConstraintSet.END ) - constrainWidth(R.id.statusIcons, 0) + constrainedWidth(R.id.date, true) + constrainedWidth(R.id.statusIcons, true) }, 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 e52170e13292..400b0baea01b 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.Rect; 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 @@ public class NotificationShadeWindowView extends FrameLayout { 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 0deb47d73460..2101efb61443 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -55,7 +55,6 @@ import android.hardware.biometrics.BiometricSourceType; import android.hardware.face.FaceManager; import android.hardware.fingerprint.FingerprintManager; import android.os.BatteryManager; -import android.os.Build; import android.os.Handler; import android.os.Looper; import android.os.Message; @@ -64,7 +63,6 @@ import android.os.UserHandle; import android.os.UserManager; import android.text.TextUtils; import android.text.format.Formatter; -import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityManager; @@ -76,6 +74,7 @@ import com.android.internal.app.IBatteryStats; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; +import com.android.keyguard.logging.KeyguardLogger; import com.android.settingslib.Utils; import com.android.settingslib.fuelgauge.BatteryStatus; import com.android.systemui.R; @@ -123,7 +122,6 @@ public class KeyguardIndicationController { private static final String TAG = "KeyguardIndication"; private static final boolean DEBUG_CHARGING_SPEED = false; - private static final boolean DEBUG = Build.IS_DEBUGGABLE; private static final int MSG_HIDE_TRANSIENT = 1; private static final int MSG_SHOW_ACTION_TO_UNLOCK = 2; @@ -139,6 +137,7 @@ public class KeyguardIndicationController { protected final StatusBarStateController mStatusBarStateController; private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; private final AuthController mAuthController; + private final KeyguardLogger mKeyguardLogger; private ViewGroup mIndicationArea; private KeyguardIndicationTextView mTopIndicationView; private KeyguardIndicationTextView mLockScreenIndicationView; @@ -229,7 +228,8 @@ public class KeyguardIndicationController { ScreenLifecycle screenLifecycle, KeyguardBypassController keyguardBypassController, AccessibilityManager accessibilityManager, - FaceHelpMessageDeferral faceHelpMessageDeferral) { + FaceHelpMessageDeferral faceHelpMessageDeferral, + KeyguardLogger keyguardLogger) { mContext = context; mBroadcastDispatcher = broadcastDispatcher; mDevicePolicyManager = devicePolicyManager; @@ -249,6 +249,7 @@ public class KeyguardIndicationController { mKeyguardBypassController = keyguardBypassController; mAccessibilityManager = accessibilityManager; mScreenLifecycle = screenLifecycle; + mKeyguardLogger = keyguardLogger; mScreenLifecycle.addObserver(mScreenObserver); mFaceAcquiredMessageDeferral = faceHelpMessageDeferral; @@ -1024,7 +1025,7 @@ public class KeyguardIndicationController { mChargingTimeRemaining = mPowerPluggedIn ? mBatteryInfo.computeChargeTimeRemaining() : -1; } catch (RemoteException e) { - Log.e(TAG, "Error calling IBatteryStats: ", e); + mKeyguardLogger.logException(e, "Error calling IBatteryStats"); mChargingTimeRemaining = -1; } updateDeviceEntryIndication(!wasPluggedIn && mPowerPluggedInWired); @@ -1072,8 +1073,10 @@ public class KeyguardIndicationController { final boolean isCoExFaceAcquisitionMessage = faceAuthSoftError && isUnlockWithFingerprintPossible; if (isCoExFaceAcquisitionMessage && !mCoExFaceAcquisitionMsgIdsToShow.contains(msgId)) { - debugLog("skip showing msgId=" + msgId + " helpString=" + helpString - + ", due to co-ex logic"); + mKeyguardLogger.logBiometricMessage( + "skipped showing help message due to co-ex logic", + msgId, + helpString); } else if (mStatusBarKeyguardViewManager.isBouncerShowing()) { mStatusBarKeyguardViewManager.setKeyguardMessage(helpString, mInitialTextColorState); @@ -1131,7 +1134,7 @@ public class KeyguardIndicationController { CharSequence deferredFaceMessage = mFaceAcquiredMessageDeferral.getDeferredMessage(); mFaceAcquiredMessageDeferral.reset(); if (shouldSuppressFaceError(msgId, mKeyguardUpdateMonitor)) { - debugLog("suppressingFaceError msgId=" + msgId + " errString= " + errString); + mKeyguardLogger.logBiometricMessage("suppressingFaceError", msgId, errString); return; } if (msgId == FaceManager.FACE_ERROR_TIMEOUT) { @@ -1145,8 +1148,9 @@ public class KeyguardIndicationController { private void onFingerprintAuthError(int msgId, String errString) { if (shouldSuppressFingerprintError(msgId, mKeyguardUpdateMonitor)) { - debugLog("suppressingFingerprintError msgId=" + msgId - + " errString= " + errString); + mKeyguardLogger.logBiometricMessage("suppressingFingerprintError", + msgId, + errString); } else { showErrorMessageNowOrLater(errString, null); } @@ -1179,16 +1183,14 @@ public class KeyguardIndicationController { @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 +1250,15 @@ public class KeyguardIndicationController { } } + 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; @@ -1275,7 +1286,8 @@ public class KeyguardIndicationController { } private void handleFaceAuthTimeoutError(@Nullable CharSequence deferredFaceMessage) { - debugLog("showDeferredFaceMessage msgId=" + deferredFaceMessage); + mKeyguardLogger.logBiometricMessage("deferred message after face auth timeout", + null, String.valueOf(deferredFaceMessage)); if (canUnlockWithFingerprint()) { // Co-ex: show deferred message OR nothing // if we're on the lock screen (bouncer isn't showing), show the deferred msg @@ -1287,7 +1299,8 @@ public class KeyguardIndicationController { ); } else { // otherwise, don't show any message - debugLog("skip showing FACE_ERROR_TIMEOUT due to co-ex logic"); + mKeyguardLogger.logBiometricMessage( + "skip showing FACE_ERROR_TIMEOUT due to co-ex logic"); } } else if (deferredFaceMessage != null) { // Face-only: The face timeout message is not very actionable, let's ask the @@ -1308,12 +1321,6 @@ public class KeyguardIndicationController { KeyguardUpdateMonitor.getCurrentUser()); } - private void debugLog(String logMsg) { - if (DEBUG) { - Log.d(TAG, logMsg); - } - } - private void showErrorMessageNowOrLater(String errString, @Nullable String followUpMsg) { if (mStatusBarKeyguardViewManager.isBouncerShowing()) { mStatusBarKeyguardViewManager.setKeyguardMessage(errString, mInitialTextColorState); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt index 9d2750fa7b5f..bc456d5d4613 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 android.view.View 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 @@ object LiftReveal : LightRevealEffect { 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 @@ class LinearLightRevealEffect(private val isVertical: Boolean) : LightRevealEffe 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 @@ class PowerButtonReveal( */ 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 @@ class PowerButtonReveal( 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 PowerButtonReveal( */ 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 @@ class LightRevealScrim(context: Context?, attrs: AttributeSet?) : View(context, 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 @@ class LightRevealScrim(context: Context?, attrs: AttributeSet?) : View(context, /** * 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 @@ class LightRevealScrim(context: Context?, attrs: AttributeSet?) : View(context, } } - /** - * 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 @@ class LightRevealScrim(context: Context?, attrs: AttributeSet?) : View(context, * 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) - - // 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) - } + 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) + } /** * Matrix applied to [gradientPaint]'s RadialGradient shader to move the gradient to @@ -347,8 +377,8 @@ class LightRevealScrim(context: Context?, attrs: AttributeSet?) : View(context, * 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 @@ class LightRevealScrim(context: Context?, attrs: AttributeSet?) : View(context, } 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 @@ class LightRevealScrim(context: Context?, attrs: AttributeSet?) : View(context, } 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/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java index 184dc253bfc6..cdefae6b87f9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java @@ -19,7 +19,6 @@ import static android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER import static com.android.systemui.DejankUtils.whitelistIpcs; -import android.app.ActivityManager; import android.app.KeyguardManager; import android.app.Notification; import android.app.admin.DevicePolicyManager; @@ -50,6 +49,7 @@ import com.android.systemui.dump.DumpManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener; import com.android.systemui.recents.OverviewProxyService; +import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection; import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; @@ -93,6 +93,7 @@ public class NotificationLockscreenUserManagerImpl implements private final SparseBooleanArray mUsersInLockdownLatestResult = new SparseBooleanArray(); private final SparseBooleanArray mShouldHideNotifsLatestResult = new SparseBooleanArray(); private final UserManager mUserManager; + private final UserTracker mUserTracker; private final List<UserChangedListener> mListeners = new ArrayList<>(); private final BroadcastDispatcher mBroadcastDispatcher; private final NotificationClickNotifier mClickNotifier; @@ -195,6 +196,7 @@ public class NotificationLockscreenUserManagerImpl implements BroadcastDispatcher broadcastDispatcher, DevicePolicyManager devicePolicyManager, UserManager userManager, + UserTracker userTracker, Lazy<NotificationVisibilityProvider> visibilityProviderLazy, Lazy<CommonNotifCollection> commonNotifCollectionLazy, NotificationClickNotifier clickNotifier, @@ -210,7 +212,8 @@ public class NotificationLockscreenUserManagerImpl implements mMainHandler = mainHandler; mDevicePolicyManager = devicePolicyManager; mUserManager = userManager; - mCurrentUserId = ActivityManager.getCurrentUser(); + mUserTracker = userTracker; + mCurrentUserId = mUserTracker.getUserId(); mVisibilityProviderLazy = visibilityProviderLazy; mCommonNotifCollectionLazy = commonNotifCollectionLazy; mClickNotifier = clickNotifier; @@ -295,7 +298,7 @@ public class NotificationLockscreenUserManagerImpl implements mContext.registerReceiver(mBaseBroadcastReceiver, internalFilter, PERMISSION_SELF, null, Context.RECEIVER_EXPORTED_UNAUDITED); - mCurrentUserId = ActivityManager.getCurrentUser(); // in case we reg'd receiver too late + mCurrentUserId = mUserTracker.getUserId(); // in case we reg'd receiver too late updateCurrentProfilesCache(); mSettingsObserver.onChange(false); // set up diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java index 73d6483e65fb..99ff06a247bc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java @@ -39,6 +39,7 @@ import android.net.wifi.WifiManager; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; +import android.os.HandlerExecutor; import android.os.Looper; import android.provider.Settings; import android.telephony.CarrierConfigManager; @@ -76,7 +77,7 @@ import com.android.systemui.log.dagger.StatusBarNetworkControllerLog; import com.android.systemui.plugins.log.LogBuffer; import com.android.systemui.plugins.log.LogLevel; import com.android.systemui.qs.tiles.dialog.InternetDialogFactory; -import com.android.systemui.settings.CurrentUserTracker; +import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.DataSaverController; import com.android.systemui.statusbar.policy.DataSaverControllerImpl; @@ -128,7 +129,7 @@ public class NetworkControllerImpl extends BroadcastReceiver private final boolean mHasMobileDataFeature; private final SubscriptionDefaults mSubDefaults; private final DataSaverController mDataSaverController; - private final CurrentUserTracker mUserTracker; + private final UserTracker mUserTracker; private final BroadcastDispatcher mBroadcastDispatcher; private final DemoModeController mDemoModeController; private final Object mLock = new Object(); @@ -212,6 +213,14 @@ public class NetworkControllerImpl extends BroadcastReceiver } }; + private final UserTracker.Callback mUserChangedCallback = + new UserTracker.Callback() { + @Override + public void onUserChanged(int newUser, @NonNull Context userContext) { + NetworkControllerImpl.this.onUserSwitched(newUser); + } + }; + /** * Construct this controller object and register for updates. */ @@ -224,6 +233,7 @@ public class NetworkControllerImpl extends BroadcastReceiver CallbackHandler callbackHandler, DeviceProvisionedController deviceProvisionedController, BroadcastDispatcher broadcastDispatcher, + UserTracker userTracker, ConnectivityManager connectivityManager, TelephonyManager telephonyManager, TelephonyListenerManager telephonyListenerManager, @@ -251,6 +261,7 @@ public class NetworkControllerImpl extends BroadcastReceiver new SubscriptionDefaults(), deviceProvisionedController, broadcastDispatcher, + userTracker, demoModeController, carrierConfigTracker, trackerFactory, @@ -277,6 +288,7 @@ public class NetworkControllerImpl extends BroadcastReceiver SubscriptionDefaults defaultsHandler, DeviceProvisionedController deviceProvisionedController, BroadcastDispatcher broadcastDispatcher, + UserTracker userTracker, DemoModeController demoModeController, CarrierConfigTracker carrierConfigTracker, WifiStatusTrackerFactory trackerFactory, @@ -333,13 +345,9 @@ public class NetworkControllerImpl extends BroadcastReceiver // AIRPLANE_MODE_CHANGED is sent at boot; we've probably already missed it updateAirplaneMode(true /* force callback */); - mUserTracker = new CurrentUserTracker(broadcastDispatcher) { - @Override - public void onUserSwitched(int newUserId) { - NetworkControllerImpl.this.onUserSwitched(newUserId); - } - }; - mUserTracker.startTracking(); + mUserTracker = userTracker; + mUserTracker.addCallback(mUserChangedCallback, new HandlerExecutor(mMainHandler)); + deviceProvisionedController.addCallback(new DeviceProvisionedListener() { @Override public void onUserSetupChanged() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt index 58738377a3db..6bd9502263ff 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt @@ -235,19 +235,24 @@ class LockscreenSmartspaceController @Inject constructor( ssView.setIntentStarter(object : BcSmartspaceDataPlugin.IntentStarter { override fun startIntent(view: View, intent: Intent, showOnLockscreen: Boolean) { - activityStarter.startActivity( - intent, - true, /* dismissShade */ - null, /* launch animator - looks bad with the transparent smartspace bg */ - showOnLockscreen - ) + if (showOnLockscreen) { + activityStarter.startActivity( + intent, + true, /* dismissShade */ + // launch animator - looks bad with the transparent smartspace bg + null, + true + ) + } else { + activityStarter.postStartActivityDismissingKeyguard(intent, 0) + } } override fun startPendingIntent(pi: PendingIntent, showOnLockscreen: Boolean) { if (showOnLockscreen) { pi.send() } else { - activityStarter.startPendingIntentDismissingKeyguard(pi) + activityStarter.postStartActivityDismissingKeyguard(pi) } } }) 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 3021414847c0..b93e1500e570 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 @@ public class ExpandableNotificationRow extends ActivatableNotificationView 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 @@ public class ExpandableNotificationRow extends ActivatableNotificationView @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 645a02dbda14..d43ca823089f 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.Canvas; 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 @@ public class NotificationChildrenContainer extends ViewGroup @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 @@ public class NotificationChildrenContainer extends ViewGroup } 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 2c3330e12229..41dbf1d6dc60 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.Paint; 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 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable @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 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable 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/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 334f1aff0d7d..18a08f73e596 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -172,7 +172,6 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.qs.QSFragment; import com.android.systemui.qs.QSPanelController; import com.android.systemui.recents.ScreenPinningRequest; -import com.android.systemui.ripple.RippleShader.RippleShape; import com.android.systemui.scrim.ScrimView; import com.android.systemui.settings.brightness.BrightnessSliderController; import com.android.systemui.shade.CameraLauncher; @@ -232,6 +231,7 @@ import com.android.systemui.statusbar.policy.UserInfoControllerImpl; import com.android.systemui.statusbar.policy.UserSwitcherController; import com.android.systemui.statusbar.window.StatusBarWindowController; import com.android.systemui.statusbar.window.StatusBarWindowStateController; +import com.android.systemui.surfaceeffects.ripple.RippleShader.RippleShape; import com.android.systemui.util.DumpUtilsKt; import com.android.systemui.util.WallpaperController; import com.android.systemui.util.concurrency.DelayableExecutor; @@ -1131,7 +1131,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { // into fragments, but the rest here, it leaves some awkward lifecycle and whatnot. mNotificationIconAreaController.setupShelf(mNotificationShelfController); mShadeExpansionStateManager.addExpansionListener(mWakeUpCoordinator); - mUserSwitcherController.init(mNotificationShadeWindowView); // Allow plugins to reference DarkIconDispatcher and StatusBarStateController mPluginDependencyProvider.allowPluginDependency(DarkIconDispatcher.class); @@ -4266,7 +4265,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { } // TODO: Bring these out of CentralSurfaces. mUserInfoControllerImpl.onDensityOrFontScaleChanged(); - mUserSwitcherController.onDensityOrFontScaleChanged(); mNotificationIconAreaController.onDensityOrFontScaleChanged(mContext); mHeadsUpManager.onDensityOrFontScaleChanged(); } 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 18877f9fb437..7a49a495155b 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.content.res.Resources; 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 @@ public class KeyguardStatusBarView extends RelativeLayout { 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/LightBarTransitionsController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarTransitionsController.java index 16fddb420fc4..6bf54430ab38 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarTransitionsController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarTransitionsController.java @@ -33,6 +33,7 @@ import com.android.systemui.statusbar.CommandQueue.Callbacks; import com.android.systemui.statusbar.policy.KeyguardStateController; import java.io.PrintWriter; +import java.lang.ref.WeakReference; import dagger.assisted.Assisted; import dagger.assisted.AssistedFactory; @@ -41,12 +42,54 @@ import dagger.assisted.AssistedInject; /** * Class to control all aspects about light bar changes. */ -public class LightBarTransitionsController implements Dumpable, Callbacks, - StatusBarStateController.StateListener { +public class LightBarTransitionsController implements Dumpable { public static final int DEFAULT_TINT_ANIMATION_DURATION = 120; private static final String EXTRA_DARK_INTENSITY = "dark_intensity"; + private static class Callback implements Callbacks, StatusBarStateController.StateListener { + private final WeakReference<LightBarTransitionsController> mSelf; + + Callback(LightBarTransitionsController self) { + mSelf = new WeakReference<>(self); + } + + @Override + public void appTransitionPending(int displayId, boolean forced) { + LightBarTransitionsController self = mSelf.get(); + if (self != null) { + self.appTransitionPending(displayId, forced); + } + } + + @Override + public void appTransitionCancelled(int displayId) { + LightBarTransitionsController self = mSelf.get(); + if (self != null) { + self.appTransitionCancelled(displayId); + } + } + + @Override + public void appTransitionStarting(int displayId, long startTime, long duration, + boolean forced) { + LightBarTransitionsController self = mSelf.get(); + if (self != null) { + self.appTransitionStarting(displayId, startTime, duration, forced); + } + } + + @Override + public void onDozeAmountChanged(float linear, float eased) { + LightBarTransitionsController self = mSelf.get(); + if (self != null) { + self.onDozeAmountChanged(linear, eased); + } + } + } + + private final Callback mCallback; + private final Handler mHandler; private final DarkIntensityApplier mApplier; private final KeyguardStateController mKeyguardStateController; @@ -86,8 +129,9 @@ public class LightBarTransitionsController implements Dumpable, Callbacks, mKeyguardStateController = keyguardStateController; mStatusBarStateController = statusBarStateController; mCommandQueue = commandQueue; - mCommandQueue.addCallback(this); - mStatusBarStateController.addCallback(this); + mCallback = new Callback(this); + mCommandQueue.addCallback(mCallback); + mStatusBarStateController.addCallback(mCallback); mDozeAmount = mStatusBarStateController.getDozeAmount(); mContext = context; mDisplayId = mContext.getDisplayId(); @@ -95,8 +139,8 @@ public class LightBarTransitionsController implements Dumpable, Callbacks, /** Call to cleanup the LightBarTransitionsController when done with it. */ public void destroy() { - mCommandQueue.removeCallback(this); - mStatusBarStateController.removeCallback(this); + mCommandQueue.removeCallback(mCallback); + mStatusBarStateController.removeCallback(mCallback); } public void saveState(Bundle outState) { @@ -110,16 +154,14 @@ public class LightBarTransitionsController implements Dumpable, Callbacks, mNextDarkIntensity = mDarkIntensity; } - @Override - public void appTransitionPending(int displayId, boolean forced) { + private void appTransitionPending(int displayId, boolean forced) { if (mDisplayId != displayId || mKeyguardStateController.isKeyguardGoingAway() && !forced) { return; } mTransitionPending = true; } - @Override - public void appTransitionCancelled(int displayId) { + private void appTransitionCancelled(int displayId) { if (mDisplayId != displayId) { return; } @@ -131,9 +173,7 @@ public class LightBarTransitionsController implements Dumpable, Callbacks, mTransitionPending = false; } - @Override - public void appTransitionStarting(int displayId, long startTime, long duration, - boolean forced) { + private void appTransitionStarting(int displayId, long startTime, long duration, boolean forced) { if (mDisplayId != displayId || mKeyguardStateController.isKeyguardGoingAway() && !forced) { return; } @@ -230,10 +270,6 @@ public class LightBarTransitionsController implements Dumpable, Callbacks, pw.print(" mNextDarkIntensity="); pw.println(mNextDarkIntensity); } - @Override - public void onStateChanged(int newState) { } - - @Override public void onDozeAmountChanged(float linear, float eased) { mDozeAmount = eased; dispatchDark(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenWallpaper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenWallpaper.java index 8793a57e4ef8..1d7dfe199871 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenWallpaper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenWallpaper.java @@ -17,7 +17,6 @@ package com.android.systemui.statusbar.phone; import android.annotation.Nullable; -import android.app.ActivityManager; import android.app.IWallpaperManager; import android.app.IWallpaperManagerCallback; import android.app.WallpaperColors; @@ -45,6 +44,7 @@ import com.android.systemui.Dumpable; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; +import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.NotificationMediaManager; import libcore.io.IoUtils; @@ -82,10 +82,11 @@ public class LockscreenWallpaper extends IWallpaperManagerCallback.Stub implemen KeyguardUpdateMonitor keyguardUpdateMonitor, DumpManager dumpManager, NotificationMediaManager mediaManager, - @Main Handler mainHandler) { + @Main Handler mainHandler, + UserTracker userTracker) { dumpManager.registerDumpable(getClass().getSimpleName(), this); mWallpaperManager = wallpaperManager; - mCurrentUserId = ActivityManager.getCurrentUser(); + mCurrentUserId = userTracker.getUserId(); mUpdateMonitor = keyguardUpdateMonitor; mMediaManager = mediaManager; mH = mainHandler; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java index 94d1bf4be806..26e6db664e07 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java @@ -14,7 +14,6 @@ package com.android.systemui.statusbar.phone; -import android.app.ActivityManager; import android.app.StatusBarManager; import android.content.BroadcastReceiver; import android.content.Context; @@ -28,6 +27,7 @@ import androidx.annotation.NonNull; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.settings.UserTracker; import java.util.ArrayList; import java.util.LinkedList; @@ -44,6 +44,7 @@ public class ManagedProfileControllerImpl implements ManagedProfileController { private final Context mContext; private final UserManager mUserManager; + private final UserTracker mUserTracker; private final BroadcastDispatcher mBroadcastDispatcher; private final LinkedList<UserInfo> mProfiles; private boolean mListening; @@ -52,9 +53,11 @@ public class ManagedProfileControllerImpl implements ManagedProfileController { /** */ @Inject - public ManagedProfileControllerImpl(Context context, BroadcastDispatcher broadcastDispatcher) { + public ManagedProfileControllerImpl(Context context, UserTracker userTracker, + BroadcastDispatcher broadcastDispatcher) { mContext = context; mUserManager = UserManager.get(mContext); + mUserTracker = userTracker; mBroadcastDispatcher = broadcastDispatcher; mProfiles = new LinkedList<UserInfo>(); } @@ -90,7 +93,7 @@ public class ManagedProfileControllerImpl implements ManagedProfileController { private void reloadManagedProfiles() { synchronized (mProfiles) { boolean hadProfile = mProfiles.size() > 0; - int user = ActivityManager.getCurrentUser(); + int user = mUserTracker.getUserId(); mProfiles.clear(); for (UserInfo ui : mUserManager.getEnabledProfiles(user)) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java index d54a8638f2e9..c527f30c424c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java @@ -53,7 +53,6 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dock.DockManager; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; -import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.scrim.ScrimView; import com.android.systemui.shade.NotificationPanelViewController; import com.android.systemui.statusbar.notification.stack.ViewState; @@ -205,7 +204,6 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump private final ScreenOffAnimationController mScreenOffAnimationController; private final KeyguardUnlockAnimationController mKeyguardUnlockAnimationController; private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; - private KeyguardViewMediator mKeyguardViewMediator; private GradientColors mColors; private boolean mNeedsDrawableColorUpdate; @@ -275,8 +273,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump @Main Executor mainExecutor, ScreenOffAnimationController screenOffAnimationController, KeyguardUnlockAnimationController keyguardUnlockAnimationController, - StatusBarKeyguardViewManager statusBarKeyguardViewManager, - KeyguardViewMediator keyguardViewMediator) { + StatusBarKeyguardViewManager statusBarKeyguardViewManager) { mScrimStateListener = lightBarController::setScrimState; mDefaultScrimAlpha = BUSY_SCRIM_ALPHA; @@ -315,8 +312,6 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump } }); mColors = new GradientColors(); - - mKeyguardViewMediator = keyguardViewMediator; } /** @@ -812,13 +807,6 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump mBehindTint, interpolatedFraction); } - - // If we're unlocked but still playing the occlude animation, remain at the keyguard - // alpha temporarily. - if (mKeyguardViewMediator.isOccludeAnimationPlaying() - || mState.mLaunchingAffordanceWithPreview) { - mNotificationsAlpha = KEYGUARD_SCRIM_ALPHA; - } } else if (mState == ScrimState.AUTH_SCRIMMED_SHADE) { mNotificationsAlpha = (float) Math.pow(getInterpolatedFraction(), 0.8f); } else if (mState == ScrimState.KEYGUARD || mState == ScrimState.SHADE_LOCKED 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 681d21bf078d..9e075e952175 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 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb // 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 0369845cf6e1..344d23341dc1 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 @@ -28,13 +28,13 @@ import com.android.systemui.R; import com.android.systemui.battery.BatteryMeterView; import com.android.systemui.battery.BatteryMeterViewController; import com.android.systemui.biometrics.AuthRippleView; -import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.privacy.OngoingPrivacyChip; +import com.android.systemui.settings.UserTracker; import com.android.systemui.shade.CombinedShadeHeadersConstraintManager; import com.android.systemui.shade.CombinedShadeHeadersConstraintManagerImpl; import com.android.systemui.shade.NotificationPanelView; @@ -220,20 +220,22 @@ public abstract class StatusBarViewModule { @Named(LARGE_SCREEN_BATTERY_CONTROLLER) static BatteryMeterViewController getBatteryMeterViewController( @Named(SPLIT_SHADE_BATTERY_VIEW) BatteryMeterView batteryMeterView, + UserTracker userTracker, ConfigurationController configurationController, TunerService tunerService, - BroadcastDispatcher broadcastDispatcher, @Main Handler mainHandler, ContentResolver contentResolver, + FeatureFlags featureFlags, BatteryController batteryController ) { return new BatteryMeterViewController( batteryMeterView, + userTracker, configurationController, tunerService, - broadcastDispatcher, mainHandler, contentResolver, + featureFlags, batteryController); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt index cf4106c508cb..68d30d3f3d1e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt @@ -21,7 +21,6 @@ import android.graphics.ColorFilter import android.graphics.ColorMatrix import android.graphics.ColorMatrixColorFilter import android.graphics.drawable.Drawable -import android.os.UserHandle import android.widget.BaseAdapter import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower import com.android.systemui.user.data.source.UserRecord @@ -84,7 +83,7 @@ protected constructor( } fun refresh() { - controller.refreshUsers(UserHandle.USER_NULL) + controller.refreshUsers() } companion object { 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 149ed0a71b91..d10d7cf8460b 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 @@ public interface BatteryController extends DemoMode, 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 c7ad76722929..3c2ac7b7a124 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 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC 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 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC cb.onPowerSaveChanged(mPowerSave); cb.onBatteryUnknownStateChanged(mStateUnknown); cb.onWirelessChargingChanged(mWirelessCharging); + cb.onIsOverheatedChanged(mIsOverheated); } @Override @@ -222,6 +227,13 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC 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 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC 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 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC } } + 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 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC 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 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC mStateUnknown = !present.equals("true"); fireBatteryUnknownStateChanged(); } + if (overheated != null) { + mIsOverheated = overheated.equals("true"); + fireIsOverheatedChanged(); + } fireBatteryLevelChanged(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java index aae0f93a0e19..38b3769c1071 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java @@ -17,7 +17,6 @@ package com.android.systemui.statusbar.policy; import android.annotation.Nullable; -import android.app.ActivityManager; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; @@ -41,6 +40,7 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; +import com.android.systemui.settings.UserTracker; import java.io.PrintWriter; import java.lang.ref.WeakReference; @@ -83,6 +83,7 @@ public class BluetoothControllerImpl implements BluetoothController, BluetoothCa @Inject public BluetoothControllerImpl( Context context, + UserTracker userTracker, DumpManager dumpManager, BluetoothLogger logger, @Background Looper bgLooper, @@ -100,7 +101,7 @@ public class BluetoothControllerImpl implements BluetoothController, BluetoothCa mLocalBluetoothManager.getBluetoothAdapter().getBluetoothState()); } mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE); - mCurrentUser = ActivityManager.getCurrentUser(); + mCurrentUser = userTracker.getUserId(); mDumpManager.registerDumpable(TAG, this); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java index 576962dee747..d84cbcc60853 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.policy; +import android.annotation.NonNull; import android.app.StatusBarManager; import android.content.BroadcastReceiver; import android.content.Context; @@ -49,7 +50,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.demomode.DemoModeCommandReceiver; import com.android.systemui.plugins.DarkIconDispatcher; import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver; -import com.android.systemui.settings.CurrentUserTracker; +import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.phone.StatusBarIconController; import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; @@ -79,7 +80,7 @@ public class Clock extends TextView implements private static final String SHOW_SECONDS = "show_seconds"; private static final String VISIBILITY = "visibility"; - private final CurrentUserTracker mCurrentUserTracker; + private final UserTracker mUserTracker; private final CommandQueue mCommandQueue; private int mCurrentUserId; @@ -114,6 +115,14 @@ public class Clock extends TextView implements private final BroadcastDispatcher mBroadcastDispatcher; + private final UserTracker.Callback mUserChangedCallback = + new UserTracker.Callback() { + @Override + public void onUserChanged(int newUser, @NonNull Context userContext) { + mCurrentUserId = newUser; + } + }; + public Clock(Context context, AttributeSet attrs) { this(context, attrs, 0); } @@ -132,12 +141,7 @@ public class Clock extends TextView implements a.recycle(); } mBroadcastDispatcher = Dependency.get(BroadcastDispatcher.class); - mCurrentUserTracker = new CurrentUserTracker(mBroadcastDispatcher) { - @Override - public void onUserSwitched(int newUserId) { - mCurrentUserId = newUserId; - } - }; + mUserTracker = Dependency.get(UserTracker.class); } @Override @@ -196,8 +200,8 @@ public class Clock extends TextView implements Dependency.get(TunerService.class).addTunable(this, CLOCK_SECONDS, StatusBarIconController.ICON_HIDE_LIST); mCommandQueue.addCallback(this); - mCurrentUserTracker.startTracking(); - mCurrentUserId = mCurrentUserTracker.getCurrentUserId(); + mUserTracker.addCallback(mUserChangedCallback, mContext.getMainExecutor()); + mCurrentUserId = mUserTracker.getUserId(); } // The time zone may have changed while the receiver wasn't registered, so update the Time @@ -227,7 +231,7 @@ public class Clock extends TextView implements mAttached = false; Dependency.get(TunerService.class).removeTunable(this); mCommandQueue.removeCallback(this); - mCurrentUserTracker.stopTracking(); + mUserTracker.removeCallback(mUserChangedCallback); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotControllerImpl.java index 69b55c81f48b..a4821e0e9299 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotControllerImpl.java @@ -18,7 +18,6 @@ package com.android.systemui.statusbar.policy; import static android.net.TetheringManager.TETHERING_WIFI; -import android.app.ActivityManager; import android.content.Context; import android.net.ConnectivityManager; import android.net.TetheringManager; @@ -38,6 +37,7 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; +import com.android.systemui.settings.UserTracker; import java.io.PrintWriter; import java.util.ArrayList; @@ -59,6 +59,7 @@ public class HotspotControllerImpl implements HotspotController, WifiManager.Sof private final WifiManager mWifiManager; private final Handler mMainHandler; private final Context mContext; + private final UserTracker mUserTracker; private int mHotspotState; private volatile int mNumConnectedDevices; @@ -95,10 +96,12 @@ public class HotspotControllerImpl implements HotspotController, WifiManager.Sof @Inject public HotspotControllerImpl( Context context, + UserTracker userTracker, @Main Handler mainHandler, @Background Handler backgroundHandler, DumpManager dumpManager) { mContext = context; + mUserTracker = userTracker; mTetheringManager = context.getSystemService(TetheringManager.class); mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); mMainHandler = mainHandler; @@ -125,7 +128,7 @@ public class HotspotControllerImpl implements HotspotController, WifiManager.Sof @Override public boolean isHotspotSupported() { return mIsTetheringSupportedConfig && mIsTetheringSupported && mHasTetherableWifiRegexs - && UserManager.get(mContext).isUserAdmin(ActivityManager.getCurrentUser()); + && UserManager.get(mContext).isUserAdmin(mUserTracker.getUserId()); } public void dump(PrintWriter pw, String[] args) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java index cc241d924d45..ba947149d287 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java @@ -16,7 +16,6 @@ package com.android.systemui.statusbar.policy; import android.annotation.Nullable; -import android.app.ActivityManager; import android.app.admin.DeviceAdminInfo; import android.app.admin.DevicePolicyManager; import android.app.admin.DevicePolicyManager.DeviceOwnerType; @@ -55,8 +54,9 @@ import com.android.systemui.R; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; -import com.android.systemui.settings.CurrentUserTracker; +import com.android.systemui.settings.UserTracker; import org.xmlpull.v1.XmlPullParserException; @@ -70,7 +70,7 @@ import javax.inject.Inject; /** */ @SysUISingleton -public class SecurityControllerImpl extends CurrentUserTracker implements SecurityController { +public class SecurityControllerImpl implements SecurityController { private static final String TAG = "SecurityController"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); @@ -84,11 +84,13 @@ public class SecurityControllerImpl extends CurrentUserTracker implements Securi private static final int CA_CERT_LOADING_RETRY_TIME_IN_MS = 30_000; private final Context mContext; + private final UserTracker mUserTracker; private final ConnectivityManager mConnectivityManager; private final VpnManager mVpnManager; private final DevicePolicyManager mDevicePolicyManager; private final PackageManager mPackageManager; private final UserManager mUserManager; + private final Executor mMainExecutor; private final Executor mBgExecutor; @GuardedBy("mCallbacks") @@ -102,18 +104,28 @@ public class SecurityControllerImpl extends CurrentUserTracker implements Securi // Needs to be cached here since the query has to be asynchronous private ArrayMap<Integer, Boolean> mHasCACerts = new ArrayMap<Integer, Boolean>(); + private final UserTracker.Callback mUserChangedCallback = + new UserTracker.Callback() { + @Override + public void onUserChanged(int newUser, @NonNull Context userContext) { + onUserSwitched(newUser); + } + }; + /** */ @Inject public SecurityControllerImpl( Context context, + UserTracker userTracker, @Background Handler bgHandler, BroadcastDispatcher broadcastDispatcher, + @Main Executor mainExecutor, @Background Executor bgExecutor, DumpManager dumpManager ) { - super(broadcastDispatcher); mContext = context; + mUserTracker = userTracker; mDevicePolicyManager = (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE); mConnectivityManager = (ConnectivityManager) @@ -121,6 +133,7 @@ public class SecurityControllerImpl extends CurrentUserTracker implements Securi mVpnManager = context.getSystemService(VpnManager.class); mPackageManager = context.getPackageManager(); mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE); + mMainExecutor = mainExecutor; mBgExecutor = bgExecutor; dumpManager.registerDumpable(getClass().getSimpleName(), this); @@ -133,8 +146,8 @@ public class SecurityControllerImpl extends CurrentUserTracker implements Securi // TODO: re-register network callback on user change. mConnectivityManager.registerNetworkCallback(REQUEST, mNetworkCallback); - onUserSwitched(ActivityManager.getCurrentUser()); - startTracking(); + onUserSwitched(mUserTracker.getUserId()); + mUserTracker.addCallback(mUserChangedCallback, mMainExecutor); } public void dump(PrintWriter pw, String[] args) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java index 29285f886f4e..a593d518c207 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java @@ -16,7 +16,6 @@ package com.android.systemui.statusbar.policy; -import android.app.ActivityManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -28,7 +27,6 @@ import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.os.AsyncTask; -import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; import android.provider.ContactsContract; @@ -40,8 +38,11 @@ import com.android.internal.util.UserIcons; import com.android.settingslib.drawable.UserIconDrawable; import com.android.systemui.R; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.settings.UserTracker; import java.util.ArrayList; +import java.util.concurrent.Executor; import javax.inject.Inject; @@ -53,6 +54,7 @@ public class UserInfoControllerImpl implements UserInfoController { private static final String TAG = "UserInfoController"; private final Context mContext; + private final UserTracker mUserTracker; private final ArrayList<OnUserInfoChangedListener> mCallbacks = new ArrayList<OnUserInfoChangedListener>(); private AsyncTask<Void, Void, UserInfoQueryResult> mUserInfoTask; @@ -64,11 +66,11 @@ public class UserInfoControllerImpl implements UserInfoController { /** */ @Inject - public UserInfoControllerImpl(Context context) { + public UserInfoControllerImpl(Context context, @Main Executor mainExecutor, + UserTracker userTracker) { mContext = context; - IntentFilter filter = new IntentFilter(); - filter.addAction(Intent.ACTION_USER_SWITCHED); - mContext.registerReceiver(mReceiver, filter); + mUserTracker = userTracker; + mUserTracker.addCallback(mUserChangedCallback, mainExecutor); IntentFilter profileFilter = new IntentFilter(); profileFilter.addAction(ContactsContract.Intents.ACTION_PROFILE_CHANGED); @@ -88,15 +90,13 @@ public class UserInfoControllerImpl implements UserInfoController { mCallbacks.remove(callback); } - private final BroadcastReceiver mReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - final String action = intent.getAction(); - if (Intent.ACTION_USER_SWITCHED.equals(action)) { - reloadUserInfo(); - } - } - }; + private final UserTracker.Callback mUserChangedCallback = + new UserTracker.Callback() { + @Override + public void onUserChanged(int newUser, @NonNull Context userContext) { + reloadUserInfo(); + } + }; private final BroadcastReceiver mProfileReceiver = new BroadcastReceiver() { @Override @@ -104,15 +104,11 @@ public class UserInfoControllerImpl implements UserInfoController { final String action = intent.getAction(); if (ContactsContract.Intents.ACTION_PROFILE_CHANGED.equals(action) || Intent.ACTION_USER_INFO_CHANGED.equals(action)) { - try { - final int currentUser = ActivityManager.getService().getCurrentUser().id; - final int changedUser = - intent.getIntExtra(Intent.EXTRA_USER_HANDLE, getSendingUserId()); - if (changedUser == currentUser) { - reloadUserInfo(); - } - } catch (RemoteException e) { - Log.e(TAG, "Couldn't get current user id for profile change", e); + final int currentUser = mUserTracker.getUserId(); + final int changedUser = + intent.getIntExtra(Intent.EXTRA_USER_HANDLE, getSendingUserId()); + if (changedUser == currentUser) { + reloadUserInfo(); } } } @@ -130,15 +126,12 @@ public class UserInfoControllerImpl implements UserInfoController { Context currentUserContext; UserInfo userInfo; try { - userInfo = ActivityManager.getService().getCurrentUser(); + userInfo = mUserTracker.getUserInfo(); currentUserContext = mContext.createPackageContextAsUser("android", 0, new UserHandle(userInfo.id)); } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, "Couldn't create user context", e); throw new RuntimeException(e); - } catch (RemoteException e) { - Log.e(TAG, "Couldn't get user info", e); - throw new RuntimeException(e); } final int userId = userInfo.id; final boolean isGuest = userInfo.isGuest(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt index 146b222c94ce..bdb656b9d2d5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt @@ -14,35 +14,74 @@ * limitations under the License. * */ + package com.android.systemui.statusbar.policy -import android.annotation.UserIdInt +import android.content.Context import android.content.Intent import android.view.View -import com.android.systemui.Dumpable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.plugins.ActivityStarter import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower import com.android.systemui.user.data.source.UserRecord +import com.android.systemui.user.domain.interactor.GuestUserInteractor +import com.android.systemui.user.domain.interactor.UserInteractor import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper +import dagger.Lazy +import java.io.PrintWriter import java.lang.ref.WeakReference -import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +/** Access point into multi-user switching logic. */ +@Deprecated("Use UserInteractor or GuestUserInteractor instead.") +@SysUISingleton +class UserSwitcherController +@Inject +constructor( + @Application private val applicationContext: Context, + private val userInteractorLazy: Lazy<UserInteractor>, + private val guestUserInteractorLazy: Lazy<GuestUserInteractor>, + private val keyguardInteractorLazy: Lazy<KeyguardInteractor>, + private val activityStarter: ActivityStarter, +) { -/** Defines interface for a class that provides user switching functionality and state. */ -interface UserSwitcherController : Dumpable { + /** Defines interface for classes that can be called back when the user is switched. */ + fun interface UserSwitchCallback { + /** Notifies that the user has switched. */ + fun onUserSwitched() + } + + private val userInteractor: UserInteractor by lazy { userInteractorLazy.get() } + private val guestUserInteractor: GuestUserInteractor by lazy { guestUserInteractorLazy.get() } + private val keyguardInteractor: KeyguardInteractor by lazy { keyguardInteractorLazy.get() } + + private val callbackCompatMap = mutableMapOf<UserSwitchCallback, UserInteractor.UserCallback>() /** The current list of [UserRecord]. */ val users: ArrayList<UserRecord> + get() = userInteractor.userRecords.value /** Whether the user switcher experience should use the simple experience. */ val isSimpleUserSwitcher: Boolean - - /** Require a view for jank detection */ - fun init(view: View) + get() = userInteractor.isSimpleUserSwitcher /** The [UserRecord] of the current user or `null` when none. */ val currentUserRecord: UserRecord? + get() = userInteractor.selectedUserRecord.value /** The name of the current user of the device or `null`, when none is selected. */ val currentUserName: String? + get() = + currentUserRecord?.let { + LegacyUserUiHelper.getUserRecordName( + context = applicationContext, + record = it, + isGuestUserAutoCreated = userInteractor.isGuestUserAutoCreated, + isGuestUserResetting = userInteractor.isGuestUserResetting, + ) + } /** * Notifies that a user has been selected. @@ -55,34 +94,40 @@ interface UserSwitcherController : Dumpable { * @param userId The ID of the user to switch to. * @param dialogShower An optional [DialogShower] in case we need to show dialogs. */ - fun onUserSelected(userId: Int, dialogShower: DialogShower?) - - /** Whether it is allowed to add users while the device is locked. */ - val isAddUsersFromLockScreenEnabled: Flow<Boolean> + fun onUserSelected(userId: Int, dialogShower: DialogShower?) { + userInteractor.selectUser(userId, dialogShower) + } /** Whether the guest user is configured to always be present on the device. */ val isGuestUserAutoCreated: Boolean + get() = userInteractor.isGuestUserAutoCreated /** Whether the guest user is currently being reset. */ val isGuestUserResetting: Boolean - - /** Creates and switches to the guest user. */ - fun createAndSwitchToGuestUser(dialogShower: DialogShower?) - - /** Shows the add user dialog. */ - fun showAddUserDialog(dialogShower: DialogShower?) - - /** Starts an activity to add a supervised user to the device. */ - fun startSupervisedUserActivity() - - /** Notifies when the display density or font scale has changed. */ - fun onDensityOrFontScaleChanged() + get() = userInteractor.isGuestUserResetting /** Registers an adapter to notify when the users change. */ - fun addAdapter(adapter: WeakReference<BaseUserSwitcherAdapter>) + fun addAdapter(adapter: WeakReference<BaseUserSwitcherAdapter>) { + userInteractor.addCallback( + object : UserInteractor.UserCallback { + override fun isEvictable(): Boolean { + return adapter.get() == null + } + + override fun onUserStateChanged() { + adapter.get()?.notifyDataSetChanged() + } + } + ) + } /** Notifies the item for a user has been clicked. */ - fun onUserListItemClicked(record: UserRecord, dialogShower: DialogShower?) + fun onUserListItemClicked( + record: UserRecord, + dialogShower: DialogShower?, + ) { + userInteractor.onRecordSelected(record, dialogShower) + } /** * Removes guest user and switches to target user. The guest must be the current user and its id @@ -103,7 +148,12 @@ interface UserSwitcherController : Dumpable { * @param targetUserId id of the user to switch to after guest is removed. If * `UserHandle.USER_NULL`, then switch immediately to the newly created guest user. */ - fun removeGuestUser(@UserIdInt guestUserId: Int, @UserIdInt targetUserId: Int) + fun removeGuestUser(guestUserId: Int, targetUserId: Int) { + userInteractor.removeGuestUser( + guestUserId = guestUserId, + targetUserId = targetUserId, + ) + } /** * Exits guest user and switches to previous non-guest user. The guest must be the current user. @@ -114,43 +164,58 @@ interface UserSwitcherController : Dumpable { * @param forceRemoveGuestOnExit true: remove guest before switching user, false: remove guest * only if its ephemeral, else keep guest */ - fun exitGuestUser( - @UserIdInt guestUserId: Int, - @UserIdInt targetUserId: Int, - forceRemoveGuestOnExit: Boolean - ) + fun exitGuestUser(guestUserId: Int, targetUserId: Int, forceRemoveGuestOnExit: Boolean) { + userInteractor.exitGuestUser(guestUserId, targetUserId, forceRemoveGuestOnExit) + } /** * Guarantee guest is present only if the device is provisioned. Otherwise, create a content * observer to wait until the device is provisioned, then schedule the guest creation. */ - fun schedulePostBootGuestCreation() + fun schedulePostBootGuestCreation() { + guestUserInteractor.onDeviceBootCompleted() + } /** Whether keyguard is showing. */ val isKeyguardShowing: Boolean + get() = keyguardInteractor.isKeyguardShowing() /** Starts an activity with the given [Intent]. */ - fun startActivity(intent: Intent) + fun startActivity(intent: Intent) { + activityStarter.startActivity(intent, /* dismissShade= */ true) + } /** * Refreshes users from UserManager. * * The pictures are only loaded if they have not been loaded yet. - * - * @param forcePictureLoadForId forces the picture of the given user to be reloaded. */ - fun refreshUsers(forcePictureLoadForId: Int) + fun refreshUsers() { + userInteractor.refreshUsers() + } /** Adds a subscriber to when user switches. */ - fun addUserSwitchCallback(callback: UserSwitchCallback) + fun addUserSwitchCallback(callback: UserSwitchCallback) { + val interactorCallback = + object : UserInteractor.UserCallback { + override fun onUserStateChanged() { + callback.onUserSwitched() + } + } + callbackCompatMap[callback] = interactorCallback + userInteractor.addCallback(interactorCallback) + } /** Removes a previously-added subscriber. */ - fun removeUserSwitchCallback(callback: UserSwitchCallback) + fun removeUserSwitchCallback(callback: UserSwitchCallback) { + val interactorCallback = callbackCompatMap.remove(callback) + if (interactorCallback != null) { + userInteractor.removeCallback(interactorCallback) + } + } - /** Defines interface for classes that can be called back when the user is switched. */ - fun interface UserSwitchCallback { - /** Notifies that the user has switched. */ - fun onUserSwitched() + fun dump(pw: PrintWriter, args: Array<out String>) { + userInteractor.dump(pw) } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt deleted file mode 100644 index 935fc7f10198..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt +++ /dev/null @@ -1,299 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package com.android.systemui.statusbar.policy - -import android.content.Context -import android.content.Intent -import android.view.View -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags -import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor -import com.android.systemui.plugins.ActivityStarter -import com.android.systemui.qs.user.UserSwitchDialogController -import com.android.systemui.user.data.source.UserRecord -import com.android.systemui.user.domain.interactor.GuestUserInteractor -import com.android.systemui.user.domain.interactor.UserInteractor -import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper -import dagger.Lazy -import java.io.PrintWriter -import java.lang.ref.WeakReference -import javax.inject.Inject -import kotlinx.coroutines.flow.Flow - -/** Implementation of [UserSwitcherController]. */ -@SysUISingleton -class UserSwitcherControllerImpl -@Inject -constructor( - @Application private val applicationContext: Context, - flags: FeatureFlags, - @Suppress("DEPRECATION") private val oldImpl: Lazy<UserSwitcherControllerOldImpl>, - private val userInteractorLazy: Lazy<UserInteractor>, - private val guestUserInteractorLazy: Lazy<GuestUserInteractor>, - private val keyguardInteractorLazy: Lazy<KeyguardInteractor>, - private val activityStarter: ActivityStarter, -) : UserSwitcherController { - - private val useInteractor: Boolean = - flags.isEnabled(Flags.USER_CONTROLLER_USES_INTERACTOR) && - !flags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER) - private val _oldImpl: UserSwitcherControllerOldImpl - get() = oldImpl.get() - private val userInteractor: UserInteractor by lazy { userInteractorLazy.get() } - private val guestUserInteractor: GuestUserInteractor by lazy { guestUserInteractorLazy.get() } - private val keyguardInteractor: KeyguardInteractor by lazy { keyguardInteractorLazy.get() } - - private val callbackCompatMap = - mutableMapOf<UserSwitcherController.UserSwitchCallback, UserInteractor.UserCallback>() - - private fun notSupported(): Nothing { - error("Not supported in the new implementation!") - } - - override val users: ArrayList<UserRecord> - get() = - if (useInteractor) { - userInteractor.userRecords.value - } else { - _oldImpl.users - } - - override val isSimpleUserSwitcher: Boolean - get() = - if (useInteractor) { - userInteractor.isSimpleUserSwitcher - } else { - _oldImpl.isSimpleUserSwitcher - } - - override fun init(view: View) { - if (!useInteractor) { - _oldImpl.init(view) - } - } - - override val currentUserRecord: UserRecord? - get() = - if (useInteractor) { - userInteractor.selectedUserRecord.value - } else { - _oldImpl.currentUserRecord - } - - override val currentUserName: String? - get() = - if (useInteractor) { - currentUserRecord?.let { - LegacyUserUiHelper.getUserRecordName( - context = applicationContext, - record = it, - isGuestUserAutoCreated = userInteractor.isGuestUserAutoCreated, - isGuestUserResetting = userInteractor.isGuestUserResetting, - ) - } - } else { - _oldImpl.currentUserName - } - - override fun onUserSelected( - userId: Int, - dialogShower: UserSwitchDialogController.DialogShower? - ) { - if (useInteractor) { - userInteractor.selectUser(userId, dialogShower) - } else { - _oldImpl.onUserSelected(userId, dialogShower) - } - } - - override val isAddUsersFromLockScreenEnabled: Flow<Boolean> - get() = - if (useInteractor) { - notSupported() - } else { - _oldImpl.isAddUsersFromLockScreenEnabled - } - - override val isGuestUserAutoCreated: Boolean - get() = - if (useInteractor) { - userInteractor.isGuestUserAutoCreated - } else { - _oldImpl.isGuestUserAutoCreated - } - - override val isGuestUserResetting: Boolean - get() = - if (useInteractor) { - userInteractor.isGuestUserResetting - } else { - _oldImpl.isGuestUserResetting - } - - override fun createAndSwitchToGuestUser( - dialogShower: UserSwitchDialogController.DialogShower?, - ) { - if (useInteractor) { - notSupported() - } else { - _oldImpl.createAndSwitchToGuestUser(dialogShower) - } - } - - override fun showAddUserDialog(dialogShower: UserSwitchDialogController.DialogShower?) { - if (useInteractor) { - notSupported() - } else { - _oldImpl.showAddUserDialog(dialogShower) - } - } - - override fun startSupervisedUserActivity() { - if (useInteractor) { - notSupported() - } else { - _oldImpl.startSupervisedUserActivity() - } - } - - override fun onDensityOrFontScaleChanged() { - if (!useInteractor) { - _oldImpl.onDensityOrFontScaleChanged() - } - } - - override fun addAdapter(adapter: WeakReference<BaseUserSwitcherAdapter>) { - if (useInteractor) { - userInteractor.addCallback( - object : UserInteractor.UserCallback { - override fun isEvictable(): Boolean { - return adapter.get() == null - } - - override fun onUserStateChanged() { - adapter.get()?.notifyDataSetChanged() - } - } - ) - } else { - _oldImpl.addAdapter(adapter) - } - } - - override fun onUserListItemClicked( - record: UserRecord, - dialogShower: UserSwitchDialogController.DialogShower?, - ) { - if (useInteractor) { - userInteractor.onRecordSelected(record, dialogShower) - } else { - _oldImpl.onUserListItemClicked(record, dialogShower) - } - } - - override fun removeGuestUser(guestUserId: Int, targetUserId: Int) { - if (useInteractor) { - userInteractor.removeGuestUser( - guestUserId = guestUserId, - targetUserId = targetUserId, - ) - } else { - _oldImpl.removeGuestUser(guestUserId, targetUserId) - } - } - - override fun exitGuestUser( - guestUserId: Int, - targetUserId: Int, - forceRemoveGuestOnExit: Boolean - ) { - if (useInteractor) { - userInteractor.exitGuestUser(guestUserId, targetUserId, forceRemoveGuestOnExit) - } else { - _oldImpl.exitGuestUser(guestUserId, targetUserId, forceRemoveGuestOnExit) - } - } - - override fun schedulePostBootGuestCreation() { - if (useInteractor) { - guestUserInteractor.onDeviceBootCompleted() - } else { - _oldImpl.schedulePostBootGuestCreation() - } - } - - override val isKeyguardShowing: Boolean - get() = - if (useInteractor) { - keyguardInteractor.isKeyguardShowing() - } else { - _oldImpl.isKeyguardShowing - } - - override fun startActivity(intent: Intent) { - if (useInteractor) { - activityStarter.startActivity(intent, /* dismissShade= */ true) - } else { - _oldImpl.startActivity(intent) - } - } - - override fun refreshUsers(forcePictureLoadForId: Int) { - if (useInteractor) { - userInteractor.refreshUsers() - } else { - _oldImpl.refreshUsers(forcePictureLoadForId) - } - } - - override fun addUserSwitchCallback(callback: UserSwitcherController.UserSwitchCallback) { - if (useInteractor) { - val interactorCallback = - object : UserInteractor.UserCallback { - override fun onUserStateChanged() { - callback.onUserSwitched() - } - } - callbackCompatMap[callback] = interactorCallback - userInteractor.addCallback(interactorCallback) - } else { - _oldImpl.addUserSwitchCallback(callback) - } - } - - override fun removeUserSwitchCallback(callback: UserSwitcherController.UserSwitchCallback) { - if (useInteractor) { - val interactorCallback = callbackCompatMap.remove(callback) - if (interactorCallback != null) { - userInteractor.removeCallback(interactorCallback) - } - } else { - _oldImpl.removeUserSwitchCallback(callback) - } - } - - override fun dump(pw: PrintWriter, args: Array<out String>) { - if (useInteractor) { - userInteractor.dump(pw) - } else { - _oldImpl.dump(pw, args) - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java deleted file mode 100644 index c294c370a601..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java +++ /dev/null @@ -1,1063 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.systemui.statusbar.policy; - -import static android.os.UserManager.SWITCHABILITY_STATUS_OK; - -import android.annotation.UserIdInt; -import android.app.AlertDialog; -import android.app.Dialog; -import android.app.IActivityManager; -import android.app.admin.DevicePolicyManager; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.UserInfo; -import android.database.ContentObserver; -import android.graphics.Bitmap; -import android.os.Handler; -import android.os.RemoteException; -import android.os.UserHandle; -import android.os.UserManager; -import android.provider.Settings; -import android.telephony.TelephonyCallback; -import android.text.TextUtils; -import android.util.Log; -import android.util.SparseArray; -import android.util.SparseBooleanArray; -import android.view.View; -import android.view.WindowManagerGlobal; -import android.widget.Toast; - -import androidx.annotation.Nullable; - -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.jank.InteractionJankMonitor; -import com.android.internal.logging.UiEventLogger; -import com.android.internal.util.LatencyTracker; -import com.android.keyguard.KeyguardUpdateMonitor; -import com.android.settingslib.users.UserCreatingDialog; -import com.android.systemui.GuestResetOrExitSessionReceiver; -import com.android.systemui.GuestResumeSessionReceiver; -import com.android.systemui.SystemUISecondaryUserService; -import com.android.systemui.animation.DialogCuj; -import com.android.systemui.animation.DialogLaunchAnimator; -import com.android.systemui.broadcast.BroadcastDispatcher; -import com.android.systemui.broadcast.BroadcastSender; -import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.dagger.qualifiers.LongRunning; -import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.dump.DumpManager; -import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.plugins.FalsingManager; -import com.android.systemui.qs.QSUserSwitcherEvent; -import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower; -import com.android.systemui.settings.UserTracker; -import com.android.systemui.telephony.TelephonyListenerManager; -import com.android.systemui.user.data.source.UserRecord; -import com.android.systemui.user.legacyhelper.data.LegacyUserDataHelper; -import com.android.systemui.user.shared.model.UserActionModel; -import com.android.systemui.user.ui.dialog.AddUserDialog; -import com.android.systemui.user.ui.dialog.ExitGuestDialog; -import com.android.systemui.util.settings.GlobalSettings; -import com.android.systemui.util.settings.SecureSettings; - -import java.io.PrintWriter; -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.Executor; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Consumer; - -import javax.inject.Inject; - -import kotlinx.coroutines.flow.Flow; -import kotlinx.coroutines.flow.MutableStateFlow; -import kotlinx.coroutines.flow.StateFlowKt; - -/** - * Old implementation. Keeps a list of all users on the device for user switching. - * - * @deprecated This is the old implementation. Please depend on {@link UserSwitcherController} - * instead. - */ -@Deprecated -@SysUISingleton -public class UserSwitcherControllerOldImpl implements UserSwitcherController { - - private static final String TAG = "UserSwitcherController"; - private static final boolean DEBUG = false; - private static final String SIMPLE_USER_SWITCHER_GLOBAL_SETTING = - "lockscreenSimpleUserSwitcher"; - private static final int PAUSE_REFRESH_USERS_TIMEOUT_MS = 3000; - - private static final String PERMISSION_SELF = "com.android.systemui.permission.SELF"; - private static final long MULTI_USER_JOURNEY_TIMEOUT = 20000L; - - private static final String INTERACTION_JANK_ADD_NEW_USER_TAG = "add_new_user"; - private static final String INTERACTION_JANK_EXIT_GUEST_MODE_TAG = "exit_guest_mode"; - - protected final Context mContext; - protected final UserTracker mUserTracker; - protected final UserManager mUserManager; - private final ContentObserver mSettingsObserver; - private final ArrayList<WeakReference<BaseUserSwitcherAdapter>> mAdapters = new ArrayList<>(); - @VisibleForTesting - final GuestResumeSessionReceiver mGuestResumeSessionReceiver; - @VisibleForTesting - final GuestResetOrExitSessionReceiver mGuestResetOrExitSessionReceiver; - private final KeyguardStateController mKeyguardStateController; - private final DeviceProvisionedController mDeviceProvisionedController; - private final DevicePolicyManager mDevicePolicyManager; - protected final Handler mHandler; - private final ActivityStarter mActivityStarter; - private final BroadcastDispatcher mBroadcastDispatcher; - private final BroadcastSender mBroadcastSender; - private final TelephonyListenerManager mTelephonyListenerManager; - private final InteractionJankMonitor mInteractionJankMonitor; - private final LatencyTracker mLatencyTracker; - private final DialogLaunchAnimator mDialogLaunchAnimator; - - private ArrayList<UserRecord> mUsers = new ArrayList<>(); - @VisibleForTesting - AlertDialog mExitGuestDialog; - @VisibleForTesting - Dialog mAddUserDialog; - private int mLastNonGuestUser = UserHandle.USER_SYSTEM; - private boolean mSimpleUserSwitcher; - // When false, there won't be any visual affordance to add a new user from the keyguard even if - // the user is unlocked - private final MutableStateFlow<Boolean> mAddUsersFromLockScreen = - StateFlowKt.MutableStateFlow(false); - private boolean mUserSwitcherEnabled; - @VisibleForTesting - boolean mPauseRefreshUsers; - private int mSecondaryUser = UserHandle.USER_NULL; - private Intent mSecondaryUserServiceIntent; - private SparseBooleanArray mForcePictureLoadForUserId = new SparseBooleanArray(2); - private final UiEventLogger mUiEventLogger; - private final IActivityManager mActivityManager; - private final Executor mBgExecutor; - private final Executor mUiExecutor; - private final Executor mLongRunningExecutor; - private final boolean mGuestUserAutoCreated; - private final AtomicBoolean mGuestIsResetting; - private final AtomicBoolean mGuestCreationScheduled; - private FalsingManager mFalsingManager; - @Nullable - private View mView; - private String mCreateSupervisedUserPackage; - private GlobalSettings mGlobalSettings; - private List<UserSwitchCallback> mUserSwitchCallbacks = - Collections.synchronizedList(new ArrayList<>()); - - @Inject - public UserSwitcherControllerOldImpl( - Context context, - IActivityManager activityManager, - UserManager userManager, - UserTracker userTracker, - KeyguardStateController keyguardStateController, - DeviceProvisionedController deviceProvisionedController, - DevicePolicyManager devicePolicyManager, - @Main Handler handler, - ActivityStarter activityStarter, - BroadcastDispatcher broadcastDispatcher, - BroadcastSender broadcastSender, - UiEventLogger uiEventLogger, - FalsingManager falsingManager, - TelephonyListenerManager telephonyListenerManager, - SecureSettings secureSettings, - GlobalSettings globalSettings, - @Background Executor bgExecutor, - @LongRunning Executor longRunningExecutor, - @Main Executor uiExecutor, - InteractionJankMonitor interactionJankMonitor, - LatencyTracker latencyTracker, - DumpManager dumpManager, - DialogLaunchAnimator dialogLaunchAnimator, - GuestResumeSessionReceiver guestResumeSessionReceiver, - GuestResetOrExitSessionReceiver guestResetOrExitSessionReceiver) { - mContext = context; - mActivityManager = activityManager; - mUserTracker = userTracker; - mBroadcastDispatcher = broadcastDispatcher; - mBroadcastSender = broadcastSender; - mTelephonyListenerManager = telephonyListenerManager; - mUiEventLogger = uiEventLogger; - mFalsingManager = falsingManager; - mInteractionJankMonitor = interactionJankMonitor; - mLatencyTracker = latencyTracker; - mGlobalSettings = globalSettings; - mGuestResumeSessionReceiver = guestResumeSessionReceiver; - mGuestResetOrExitSessionReceiver = guestResetOrExitSessionReceiver; - mBgExecutor = bgExecutor; - mLongRunningExecutor = longRunningExecutor; - mUiExecutor = uiExecutor; - mGuestResumeSessionReceiver.register(); - mGuestResetOrExitSessionReceiver.register(); - mGuestUserAutoCreated = mContext.getResources().getBoolean( - com.android.internal.R.bool.config_guestUserAutoCreated); - mGuestIsResetting = new AtomicBoolean(); - mGuestCreationScheduled = new AtomicBoolean(); - mKeyguardStateController = keyguardStateController; - mDeviceProvisionedController = deviceProvisionedController; - mDevicePolicyManager = devicePolicyManager; - mHandler = handler; - mActivityStarter = activityStarter; - mUserManager = userManager; - mDialogLaunchAnimator = dialogLaunchAnimator; - - IntentFilter filter = new IntentFilter(); - filter.addAction(Intent.ACTION_USER_ADDED); - filter.addAction(Intent.ACTION_USER_REMOVED); - filter.addAction(Intent.ACTION_USER_INFO_CHANGED); - filter.addAction(Intent.ACTION_USER_SWITCHED); - filter.addAction(Intent.ACTION_USER_STOPPED); - filter.addAction(Intent.ACTION_USER_UNLOCKED); - mBroadcastDispatcher.registerReceiver( - mReceiver, filter, null /* executor */, - UserHandle.SYSTEM, Context.RECEIVER_EXPORTED, null /* permission */); - - mSimpleUserSwitcher = shouldUseSimpleUserSwitcher(); - - mSecondaryUserServiceIntent = new Intent(context, SystemUISecondaryUserService.class); - - filter = new IntentFilter(); - mContext.registerReceiverAsUser(mReceiver, UserHandle.SYSTEM, filter, - PERMISSION_SELF, null /* scheduler */, - Context.RECEIVER_EXPORTED_UNAUDITED); - - mSettingsObserver = new ContentObserver(mHandler) { - @Override - public void onChange(boolean selfChange) { - mSimpleUserSwitcher = shouldUseSimpleUserSwitcher(); - mAddUsersFromLockScreen.setValue( - mGlobalSettings.getIntForUser( - Settings.Global.ADD_USERS_WHEN_LOCKED, - 0, - UserHandle.USER_SYSTEM) != 0); - mUserSwitcherEnabled = mGlobalSettings.getIntForUser( - Settings.Global.USER_SWITCHER_ENABLED, 0, UserHandle.USER_SYSTEM) != 0; - refreshUsers(UserHandle.USER_NULL); - }; - }; - mContext.getContentResolver().registerContentObserver( - Settings.Global.getUriFor(SIMPLE_USER_SWITCHER_GLOBAL_SETTING), true, - mSettingsObserver); - mContext.getContentResolver().registerContentObserver( - Settings.Global.getUriFor(Settings.Global.USER_SWITCHER_ENABLED), true, - mSettingsObserver); - mContext.getContentResolver().registerContentObserver( - Settings.Global.getUriFor(Settings.Global.ADD_USERS_WHEN_LOCKED), true, - mSettingsObserver); - mContext.getContentResolver().registerContentObserver( - Settings.Global.getUriFor( - Settings.Global.ALLOW_USER_SWITCHING_WHEN_SYSTEM_USER_LOCKED), - true, mSettingsObserver); - // Fetch initial values. - mSettingsObserver.onChange(false); - - keyguardStateController.addCallback(mCallback); - listenForCallState(); - - mCreateSupervisedUserPackage = mContext.getString( - com.android.internal.R.string.config_supervisedUserCreationPackage); - - dumpManager.registerDumpable(getClass().getSimpleName(), this); - - refreshUsers(UserHandle.USER_NULL); - } - - @Override - @SuppressWarnings("unchecked") - public void refreshUsers(int forcePictureLoadForId) { - if (DEBUG) Log.d(TAG, "refreshUsers(forcePictureLoadForId=" + forcePictureLoadForId + ")"); - if (forcePictureLoadForId != UserHandle.USER_NULL) { - mForcePictureLoadForUserId.put(forcePictureLoadForId, true); - } - - if (mPauseRefreshUsers) { - return; - } - - boolean forceAllUsers = mForcePictureLoadForUserId.get(UserHandle.USER_ALL); - SparseArray<Bitmap> bitmaps = new SparseArray<>(mUsers.size()); - final int userCount = mUsers.size(); - for (int i = 0; i < userCount; i++) { - UserRecord r = mUsers.get(i); - if (r == null || r.picture == null || r.info == null || forceAllUsers - || mForcePictureLoadForUserId.get(r.info.id)) { - continue; - } - bitmaps.put(r.info.id, r.picture); - } - mForcePictureLoadForUserId.clear(); - - mBgExecutor.execute(() -> { - List<UserInfo> infos = mUserManager.getAliveUsers(); - if (infos == null) { - return; - } - ArrayList<UserRecord> records = new ArrayList<>(infos.size()); - int currentId = mUserTracker.getUserId(); - // Check user switchability of the foreground user since SystemUI is running in - // User 0 - boolean canSwitchUsers = mUserManager.getUserSwitchability( - UserHandle.of(mUserTracker.getUserId())) == SWITCHABILITY_STATUS_OK; - UserRecord guestRecord = null; - - for (UserInfo info : infos) { - boolean isCurrent = currentId == info.id; - if (!mUserSwitcherEnabled && !info.isPrimary()) { - continue; - } - - if (info.isEnabled()) { - if (info.isGuest()) { - // Tapping guest icon triggers remove and a user switch therefore - // the icon shouldn't be enabled even if the user is current - guestRecord = LegacyUserDataHelper.createRecord( - mContext, - mUserManager, - null /* picture */, - info, - isCurrent, - canSwitchUsers); - } else if (info.supportsSwitchToByUser()) { - records.add( - LegacyUserDataHelper.createRecord( - mContext, - mUserManager, - bitmaps.get(info.id), - info, - isCurrent, - canSwitchUsers)); - } - } - } - - if (guestRecord == null) { - if (mGuestUserAutoCreated) { - // If mGuestIsResetting=true, the switch should be disabled since - // we will just use it as an indicator for "Resetting guest...". - // Otherwise, default to canSwitchUsers. - boolean isSwitchToGuestEnabled = !mGuestIsResetting.get() && canSwitchUsers; - guestRecord = LegacyUserDataHelper.createRecord( - mContext, - currentId, - UserActionModel.ENTER_GUEST_MODE, - false /* isRestricted */, - isSwitchToGuestEnabled); - records.add(guestRecord); - } else if (canCreateGuest(guestRecord != null)) { - guestRecord = LegacyUserDataHelper.createRecord( - mContext, - currentId, - UserActionModel.ENTER_GUEST_MODE, - false /* isRestricted */, - canSwitchUsers); - records.add(guestRecord); - } - } else { - records.add(guestRecord); - } - - if (canCreateUser()) { - final UserRecord userRecord = LegacyUserDataHelper.createRecord( - mContext, - currentId, - UserActionModel.ADD_USER, - createIsRestricted(), - canSwitchUsers); - records.add(userRecord); - } - - if (canCreateSupervisedUser()) { - final UserRecord userRecord = LegacyUserDataHelper.createRecord( - mContext, - currentId, - UserActionModel.ADD_SUPERVISED_USER, - createIsRestricted(), - canSwitchUsers); - records.add(userRecord); - } - - if (canManageUsers()) { - records.add(LegacyUserDataHelper.createRecord( - mContext, - KeyguardUpdateMonitor.getCurrentUser(), - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, - /* isRestricted= */ false, - /* isSwitchToEnabled= */ true - )); - } - - mUiExecutor.execute(() -> { - if (records != null) { - mUsers = records; - notifyAdapters(); - } - }); - }); - } - - private boolean systemCanCreateUsers() { - return !mUserManager.hasBaseUserRestriction( - UserManager.DISALLOW_ADD_USER, UserHandle.SYSTEM); - } - - private boolean currentUserCanCreateUsers() { - UserInfo currentUser = mUserTracker.getUserInfo(); - return currentUser != null - && (currentUser.isAdmin() || mUserTracker.getUserId() == UserHandle.USER_SYSTEM) - && systemCanCreateUsers(); - } - - private boolean anyoneCanCreateUsers() { - return systemCanCreateUsers() && mAddUsersFromLockScreen.getValue(); - } - - @VisibleForTesting - boolean canCreateGuest(boolean hasExistingGuest) { - return mUserSwitcherEnabled - && (currentUserCanCreateUsers() || anyoneCanCreateUsers()) - && !hasExistingGuest; - } - - @VisibleForTesting - boolean canCreateUser() { - return mUserSwitcherEnabled - && (currentUserCanCreateUsers() || anyoneCanCreateUsers()) - && mUserManager.canAddMoreUsers(UserManager.USER_TYPE_FULL_SECONDARY); - } - - @VisibleForTesting - boolean canManageUsers() { - UserInfo currentUser = mUserTracker.getUserInfo(); - return mUserSwitcherEnabled - && ((currentUser != null && currentUser.isAdmin()) - || mAddUsersFromLockScreen.getValue()); - } - - private boolean createIsRestricted() { - return !mAddUsersFromLockScreen.getValue(); - } - - @VisibleForTesting - boolean canCreateSupervisedUser() { - return !TextUtils.isEmpty(mCreateSupervisedUserPackage) && canCreateUser(); - } - - private void pauseRefreshUsers() { - if (!mPauseRefreshUsers) { - mHandler.postDelayed(mUnpauseRefreshUsers, PAUSE_REFRESH_USERS_TIMEOUT_MS); - mPauseRefreshUsers = true; - } - } - - private void notifyAdapters() { - for (int i = mAdapters.size() - 1; i >= 0; i--) { - BaseUserSwitcherAdapter adapter = mAdapters.get(i).get(); - if (adapter != null) { - adapter.notifyDataSetChanged(); - } else { - mAdapters.remove(i); - } - } - } - - @Override - public boolean isSimpleUserSwitcher() { - return mSimpleUserSwitcher; - } - - /** - * Returns whether the current user is a system user. - */ - @VisibleForTesting - boolean isSystemUser() { - return mUserTracker.getUserId() == UserHandle.USER_SYSTEM; - } - - @Override - public @Nullable UserRecord getCurrentUserRecord() { - for (int i = 0; i < mUsers.size(); ++i) { - UserRecord userRecord = mUsers.get(i); - if (userRecord.isCurrent) { - return userRecord; - } - } - return null; - } - - @Override - public void onUserSelected(int userId, @Nullable DialogShower dialogShower) { - UserRecord userRecord = mUsers.stream() - .filter(x -> x.resolveId() == userId) - .findFirst() - .orElse(null); - if (userRecord == null) { - return; - } - - onUserListItemClicked(userRecord, dialogShower); - } - - @Override - public Flow<Boolean> isAddUsersFromLockScreenEnabled() { - return mAddUsersFromLockScreen; - } - - @Override - public boolean isGuestUserAutoCreated() { - return mGuestUserAutoCreated; - } - - @Override - public boolean isGuestUserResetting() { - return mGuestIsResetting.get(); - } - - @Override - public void onUserListItemClicked(UserRecord record, DialogShower dialogShower) { - if (record.isGuest && record.info == null) { - createAndSwitchToGuestUser(dialogShower); - } else if (record.isAddUser) { - showAddUserDialog(dialogShower); - } else if (record.isAddSupervisedUser) { - startSupervisedUserActivity(); - } else if (record.isManageUsers) { - startActivity(new Intent(Settings.ACTION_USER_SETTINGS)); - } else { - onUserListItemClicked(record.info.id, record, dialogShower); - } - } - - private void onUserListItemClicked(int id, UserRecord record, DialogShower dialogShower) { - int currUserId = mUserTracker.getUserId(); - // If switching from guest and guest is ephemeral, then follow the flow - // of showExitGuestDialog to remove current guest, - // and switch to selected user - UserInfo currUserInfo = mUserTracker.getUserInfo(); - if (currUserId == id) { - if (record.isGuest) { - showExitGuestDialog(id, currUserInfo.isEphemeral(), dialogShower); - } - return; - } - - if (currUserInfo != null && currUserInfo.isGuest()) { - showExitGuestDialog(currUserId, currUserInfo.isEphemeral(), - record.resolveId(), dialogShower); - return; - } - - if (dialogShower != null) { - // If we haven't morphed into another dialog, it means we have just switched users. - // Then, dismiss the dialog. - dialogShower.dismiss(); - } - switchToUserId(id); - } - - private void switchToUserId(int id) { - try { - if (mView != null) { - mInteractionJankMonitor.begin(InteractionJankMonitor.Configuration.Builder - .withView(InteractionJankMonitor.CUJ_USER_SWITCH, mView) - .setTimeout(MULTI_USER_JOURNEY_TIMEOUT)); - } - mLatencyTracker.onActionStart(LatencyTracker.ACTION_USER_SWITCH); - pauseRefreshUsers(); - mActivityManager.switchUser(id); - } catch (RemoteException e) { - Log.e(TAG, "Couldn't switch user.", e); - } - } - - private void showExitGuestDialog(int id, boolean isGuestEphemeral, DialogShower dialogShower) { - int newId = UserHandle.USER_SYSTEM; - if (mLastNonGuestUser != UserHandle.USER_SYSTEM) { - UserInfo info = mUserManager.getUserInfo(mLastNonGuestUser); - if (info != null && info.isEnabled() && info.supportsSwitchToByUser()) { - newId = info.id; - } - } - showExitGuestDialog(id, isGuestEphemeral, newId, dialogShower); - } - - private void showExitGuestDialog( - int id, - boolean isGuestEphemeral, - int targetId, - DialogShower dialogShower) { - if (mExitGuestDialog != null && mExitGuestDialog.isShowing()) { - mExitGuestDialog.cancel(); - } - mExitGuestDialog = new ExitGuestDialog( - mContext, - id, - isGuestEphemeral, - targetId, - mKeyguardStateController.isShowing(), - mFalsingManager, - mDialogLaunchAnimator, - this::exitGuestUser); - if (dialogShower != null) { - dialogShower.showDialog(mExitGuestDialog, new DialogCuj( - InteractionJankMonitor.CUJ_USER_DIALOG_OPEN, - INTERACTION_JANK_EXIT_GUEST_MODE_TAG)); - } else { - mExitGuestDialog.show(); - } - } - - @Override - public void createAndSwitchToGuestUser(@Nullable DialogShower dialogShower) { - createGuestAsync(guestId -> { - // guestId may be USER_NULL if we haven't reloaded the user list yet. - if (guestId != UserHandle.USER_NULL) { - mUiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_ADD); - onUserListItemClicked(guestId, UserRecord.createForGuest(), dialogShower); - } - }); - } - - @Override - public void showAddUserDialog(@Nullable DialogShower dialogShower) { - if (mAddUserDialog != null && mAddUserDialog.isShowing()) { - mAddUserDialog.cancel(); - } - final UserInfo currentUser = mUserTracker.getUserInfo(); - mAddUserDialog = new AddUserDialog( - mContext, - currentUser.getUserHandle(), - mKeyguardStateController.isShowing(), - /* showEphemeralMessage= */currentUser.isGuest() && currentUser.isEphemeral(), - mFalsingManager, - mBroadcastSender, - mDialogLaunchAnimator); - if (dialogShower != null) { - dialogShower.showDialog(mAddUserDialog, - new DialogCuj( - InteractionJankMonitor.CUJ_USER_DIALOG_OPEN, - INTERACTION_JANK_ADD_NEW_USER_TAG - )); - } else { - mAddUserDialog.show(); - } - } - - @Override - public void startSupervisedUserActivity() { - final Intent intent = new Intent() - .setAction(UserManager.ACTION_CREATE_SUPERVISED_USER) - .setPackage(mCreateSupervisedUserPackage) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - - mContext.startActivity(intent); - } - - private void listenForCallState() { - mTelephonyListenerManager.addCallStateListener(mPhoneStateListener); - } - - private final TelephonyCallback.CallStateListener mPhoneStateListener = - new TelephonyCallback.CallStateListener() { - private int mCallState; - - @Override - public void onCallStateChanged(int state) { - if (mCallState == state) return; - if (DEBUG) Log.v(TAG, "Call state changed: " + state); - mCallState = state; - refreshUsers(UserHandle.USER_NULL); - } - }; - - private BroadcastReceiver mReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (DEBUG) { - Log.v(TAG, "Broadcast: a=" + intent.getAction() - + " user=" + intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1)); - } - - boolean unpauseRefreshUsers = false; - int forcePictureLoadForId = UserHandle.USER_NULL; - - if (Intent.ACTION_USER_SWITCHED.equals(intent.getAction())) { - if (mExitGuestDialog != null && mExitGuestDialog.isShowing()) { - mExitGuestDialog.cancel(); - mExitGuestDialog = null; - } - - final int currentId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1); - final UserInfo userInfo = mUserManager.getUserInfo(currentId); - final int userCount = mUsers.size(); - for (int i = 0; i < userCount; i++) { - UserRecord record = mUsers.get(i); - if (record.info == null) continue; - boolean shouldBeCurrent = record.info.id == currentId; - if (record.isCurrent != shouldBeCurrent) { - mUsers.set(i, record.copyWithIsCurrent(shouldBeCurrent)); - } - if (shouldBeCurrent && !record.isGuest) { - mLastNonGuestUser = record.info.id; - } - if ((userInfo == null || !userInfo.isAdmin()) && record.isRestricted) { - // Immediately remove restricted records in case the AsyncTask is too slow. - mUsers.remove(i); - i--; - } - } - notifyUserSwitchCallbacks(); - notifyAdapters(); - - // Disconnect from the old secondary user's service - if (mSecondaryUser != UserHandle.USER_NULL) { - context.stopServiceAsUser(mSecondaryUserServiceIntent, - UserHandle.of(mSecondaryUser)); - mSecondaryUser = UserHandle.USER_NULL; - } - // Connect to the new secondary user's service (purely to ensure that a persistent - // SystemUI application is created for that user) - if (userInfo != null && userInfo.id != UserHandle.USER_SYSTEM) { - context.startServiceAsUser(mSecondaryUserServiceIntent, - UserHandle.of(userInfo.id)); - mSecondaryUser = userInfo.id; - } - unpauseRefreshUsers = true; - if (mGuestUserAutoCreated) { - // Guest user must be scheduled for creation AFTER switching to the target user. - // This avoids lock contention which will produce UX bugs on the keyguard - // (b/193933686). - // TODO(b/191067027): Move guest user recreation to system_server - guaranteeGuestPresent(); - } - } else if (Intent.ACTION_USER_INFO_CHANGED.equals(intent.getAction())) { - forcePictureLoadForId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, - UserHandle.USER_NULL); - } else if (Intent.ACTION_USER_UNLOCKED.equals(intent.getAction())) { - // Unlocking the system user may require a refresh - int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL); - if (userId != UserHandle.USER_SYSTEM) { - return; - } - } - refreshUsers(forcePictureLoadForId); - if (unpauseRefreshUsers) { - mUnpauseRefreshUsers.run(); - } - } - }; - - private final Runnable mUnpauseRefreshUsers = new Runnable() { - @Override - public void run() { - mHandler.removeCallbacks(this); - mPauseRefreshUsers = false; - refreshUsers(UserHandle.USER_NULL); - } - }; - - @Override - public void dump(PrintWriter pw, String[] args) { - pw.println("UserSwitcherController state:"); - pw.println(" mLastNonGuestUser=" + mLastNonGuestUser); - pw.print(" mUsers.size="); pw.println(mUsers.size()); - for (int i = 0; i < mUsers.size(); i++) { - final UserRecord u = mUsers.get(i); - pw.print(" "); pw.println(u.toString()); - } - pw.println("mSimpleUserSwitcher=" + mSimpleUserSwitcher); - pw.println("mGuestUserAutoCreated=" + mGuestUserAutoCreated); - } - - @Override - public String getCurrentUserName() { - if (mUsers.isEmpty()) return null; - UserRecord item = mUsers.stream().filter(x -> x.isCurrent).findFirst().orElse(null); - if (item == null || item.info == null) return null; - if (item.isGuest) return mContext.getString(com.android.internal.R.string.guest_name); - return item.info.name; - } - - @Override - public void onDensityOrFontScaleChanged() { - refreshUsers(UserHandle.USER_ALL); - } - - @Override - public void addAdapter(WeakReference<BaseUserSwitcherAdapter> adapter) { - mAdapters.add(adapter); - } - - @Override - public ArrayList<UserRecord> getUsers() { - return mUsers; - } - - @Override - public void removeGuestUser(@UserIdInt int guestUserId, @UserIdInt int targetUserId) { - UserInfo currentUser = mUserTracker.getUserInfo(); - if (currentUser.id != guestUserId) { - Log.w(TAG, "User requesting to start a new session (" + guestUserId + ")" - + " is not current user (" + currentUser.id + ")"); - return; - } - if (!currentUser.isGuest()) { - Log.w(TAG, "User requesting to start a new session (" + guestUserId + ")" - + " is not a guest"); - return; - } - - boolean marked = mUserManager.markGuestForDeletion(currentUser.id); - if (!marked) { - Log.w(TAG, "Couldn't mark the guest for deletion for user " + guestUserId); - return; - } - - if (targetUserId == UserHandle.USER_NULL) { - // Create a new guest in the foreground, and then immediately switch to it - createGuestAsync(newGuestId -> { - if (newGuestId == UserHandle.USER_NULL) { - Log.e(TAG, "Could not create new guest, switching back to system user"); - switchToUserId(UserHandle.USER_SYSTEM); - mUserManager.removeUser(currentUser.id); - try { - WindowManagerGlobal.getWindowManagerService().lockNow(/* options= */ null); - } catch (RemoteException e) { - Log.e(TAG, "Couldn't remove guest because ActivityManager " - + "or WindowManager is dead"); - } - return; - } - switchToUserId(newGuestId); - mUserManager.removeUser(currentUser.id); - }); - } else { - if (mGuestUserAutoCreated) { - mGuestIsResetting.set(true); - } - switchToUserId(targetUserId); - mUserManager.removeUser(currentUser.id); - } - } - - @Override - public void exitGuestUser(@UserIdInt int guestUserId, @UserIdInt int targetUserId, - boolean forceRemoveGuestOnExit) { - UserInfo currentUser = mUserTracker.getUserInfo(); - if (currentUser.id != guestUserId) { - Log.w(TAG, "User requesting to start a new session (" + guestUserId + ")" - + " is not current user (" + currentUser.id + ")"); - return; - } - if (!currentUser.isGuest()) { - Log.w(TAG, "User requesting to start a new session (" + guestUserId + ")" - + " is not a guest"); - return; - } - - int newUserId = UserHandle.USER_SYSTEM; - if (targetUserId == UserHandle.USER_NULL) { - // when target user is not specified switch to last non guest user - if (mLastNonGuestUser != UserHandle.USER_SYSTEM) { - UserInfo info = mUserManager.getUserInfo(mLastNonGuestUser); - if (info != null && info.isEnabled() && info.supportsSwitchToByUser()) { - newUserId = info.id; - } - } - } else { - newUserId = targetUserId; - } - - if (currentUser.isEphemeral() || forceRemoveGuestOnExit) { - mUiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_REMOVE); - removeGuestUser(currentUser.id, newUserId); - } else { - mUiEventLogger.log(QSUserSwitcherEvent.QS_USER_SWITCH); - switchToUserId(newUserId); - } - } - - private void scheduleGuestCreation() { - if (!mGuestCreationScheduled.compareAndSet(false, true)) { - return; - } - - mLongRunningExecutor.execute(() -> { - int newGuestId = createGuest(); - mGuestCreationScheduled.set(false); - mGuestIsResetting.set(false); - if (newGuestId == UserHandle.USER_NULL) { - Log.w(TAG, "Could not create new guest while exiting existing guest"); - // Refresh users so that we still display "Guest" if - // config_guestUserAutoCreated=true - refreshUsers(UserHandle.USER_NULL); - } - }); - - } - - @Override - public void schedulePostBootGuestCreation() { - if (isDeviceAllowedToAddGuest()) { - guaranteeGuestPresent(); - } else { - mDeviceProvisionedController.addCallback(mGuaranteeGuestPresentAfterProvisioned); - } - } - - private boolean isDeviceAllowedToAddGuest() { - return mDeviceProvisionedController.isDeviceProvisioned() - && !mDevicePolicyManager.isDeviceManaged(); - } - - /** - * If there is no guest on the device, schedule creation of a new guest user in the background. - */ - private void guaranteeGuestPresent() { - if (isDeviceAllowedToAddGuest() && mUserManager.findCurrentGuestUser() == null) { - scheduleGuestCreation(); - } - } - - private void createGuestAsync(Consumer<Integer> callback) { - final Dialog guestCreationProgressDialog = - new UserCreatingDialog(mContext, /* isGuest= */true); - guestCreationProgressDialog.show(); - - // userManager.createGuest will block the thread so post is needed for the dialog to show - mBgExecutor.execute(() -> { - final int guestId = createGuest(); - mUiExecutor.execute(() -> { - guestCreationProgressDialog.dismiss(); - if (guestId == UserHandle.USER_NULL) { - Toast.makeText(mContext, - com.android.settingslib.R.string.add_guest_failed, - Toast.LENGTH_SHORT).show(); - } - callback.accept(guestId); - }); - }); - } - - /** - * Creates a guest user and return its multi-user user ID. - * - * This method does not check if a guest already exists before it makes a call to - * {@link UserManager} to create a new one. - * - * @return The multi-user user ID of the newly created guest user, or - * {@link UserHandle#USER_NULL} if the guest couldn't be created. - */ - private @UserIdInt int createGuest() { - UserInfo guest; - try { - guest = mUserManager.createGuest(mContext); - } catch (UserManager.UserOperationException e) { - Log.e(TAG, "Couldn't create guest user", e); - return UserHandle.USER_NULL; - } - if (guest == null) { - Log.e(TAG, "Couldn't create guest, most likely because there already exists one"); - return UserHandle.USER_NULL; - } - return guest.id; - } - - @Override - public void init(View view) { - mView = view; - } - - @Override - public boolean isKeyguardShowing() { - return mKeyguardStateController.isShowing(); - } - - private boolean shouldUseSimpleUserSwitcher() { - int defaultSimpleUserSwitcher = mContext.getResources().getBoolean( - com.android.internal.R.bool.config_expandLockScreenUserSwitcher) ? 1 : 0; - return mGlobalSettings.getIntForUser(SIMPLE_USER_SWITCHER_GLOBAL_SETTING, - defaultSimpleUserSwitcher, UserHandle.USER_SYSTEM) != 0; - } - - @Override - public void startActivity(Intent intent) { - mActivityStarter.startActivity(intent, /* dismissShade= */ true); - } - - @Override - public void addUserSwitchCallback(UserSwitchCallback callback) { - mUserSwitchCallbacks.add(callback); - } - - @Override - public void removeUserSwitchCallback(UserSwitchCallback callback) { - mUserSwitchCallbacks.remove(callback); - } - - /** - * Notify user switch callbacks that user has switched. - */ - private void notifyUserSwitchCallbacks() { - List<UserSwitchCallback> temp; - synchronized (mUserSwitchCallbacks) { - temp = new ArrayList<>(mUserSwitchCallbacks); - } - for (UserSwitchCallback callback : temp) { - callback.onUserSwitched(); - } - } - - private final KeyguardStateController.Callback mCallback = - new KeyguardStateController.Callback() { - @Override - public void onKeyguardShowingChanged() { - - // When Keyguard is going away, we don't need to update our items immediately - // which - // helps making the transition faster. - if (!mKeyguardStateController.isShowing()) { - mHandler.post(UserSwitcherControllerOldImpl.this::notifyAdapters); - } else { - notifyAdapters(); - } - } - }; - - private final DeviceProvisionedController.DeviceProvisionedListener - mGuaranteeGuestPresentAfterProvisioned = - new DeviceProvisionedController.DeviceProvisionedListener() { - @Override - public void onDeviceProvisionedChanged() { - if (isDeviceAllowedToAddGuest()) { - mBgExecutor.execute( - () -> mDeviceProvisionedController.removeCallback( - mGuaranteeGuestPresentAfterProvisioned)); - guaranteeGuestPresent(); - } - } - }; -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java index 9866389a39d7..b135d0d8c9dc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java @@ -16,7 +16,6 @@ package com.android.systemui.statusbar.policy; -import android.app.ActivityManager; import android.app.AlarmManager; import android.app.NotificationManager; import android.content.BroadcastReceiver; @@ -28,6 +27,7 @@ import android.content.IntentFilter; import android.database.ContentObserver; import android.net.Uri; import android.os.Handler; +import android.os.HandlerExecutor; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings.Global; @@ -46,7 +46,7 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.qs.SettingObserver; -import com.android.systemui.settings.CurrentUserTracker; +import com.android.systemui.settings.UserTracker; import com.android.systemui.util.Utils; import com.android.systemui.util.settings.GlobalSettings; @@ -58,14 +58,15 @@ import javax.inject.Inject; /** Platform implementation of the zen mode controller. **/ @SysUISingleton -public class ZenModeControllerImpl extends CurrentUserTracker - implements ZenModeController, Dumpable { +public class ZenModeControllerImpl implements ZenModeController, Dumpable { private static final String TAG = "ZenModeController"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private final ArrayList<Callback> mCallbacks = new ArrayList<>(); private final Object mCallbacksLock = new Object(); private final Context mContext; + private final UserTracker mUserTracker; + private final BroadcastDispatcher mBroadcastDispatcher; private final SettingObserver mModeSetting; private final SettingObserver mConfigSetting; private final NotificationManager mNoMan; @@ -80,23 +81,45 @@ public class ZenModeControllerImpl extends CurrentUserTracker private long mZenUpdateTime; private NotificationManager.Policy mConsolidatedNotificationPolicy; + private final UserTracker.Callback mUserChangedCallback = + new UserTracker.Callback() { + @Override + public void onUserChanged(int newUser, Context userContext) { + mUserId = newUser; + if (mRegistered) { + mBroadcastDispatcher.unregisterReceiver(mReceiver); + } + final IntentFilter filter = new IntentFilter( + AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED); + filter.addAction(NotificationManager.ACTION_EFFECTS_SUPPRESSOR_CHANGED); + mBroadcastDispatcher.registerReceiver(mReceiver, filter, null, + UserHandle.of(mUserId)); + mRegistered = true; + mSetupObserver.register(); + } + }; + @Inject public ZenModeControllerImpl( Context context, @Main Handler handler, BroadcastDispatcher broadcastDispatcher, DumpManager dumpManager, - GlobalSettings globalSettings) { - super(broadcastDispatcher); + GlobalSettings globalSettings, + UserTracker userTracker) { mContext = context; - mModeSetting = new SettingObserver(globalSettings, handler, Global.ZEN_MODE) { + mBroadcastDispatcher = broadcastDispatcher; + mUserTracker = userTracker; + mModeSetting = new SettingObserver(globalSettings, handler, Global.ZEN_MODE, + userTracker.getUserId()) { @Override protected void handleValueChanged(int value, boolean observedChange) { updateZenMode(value); fireZenChanged(value); } }; - mConfigSetting = new SettingObserver(globalSettings, handler, Global.ZEN_MODE_CONFIG_ETAG) { + mConfigSetting = new SettingObserver(globalSettings, handler, Global.ZEN_MODE_CONFIG_ETAG, + userTracker.getUserId()) { @Override protected void handleValueChanged(int value, boolean observedChange) { updateZenModeConfig(); @@ -112,7 +135,7 @@ public class ZenModeControllerImpl extends CurrentUserTracker mSetupObserver = new SetupObserver(handler); mSetupObserver.register(); mUserManager = context.getSystemService(UserManager.class); - startTracking(); + mUserTracker.addCallback(mUserChangedCallback, new HandlerExecutor(handler)); dumpManager.registerDumpable(getClass().getSimpleName(), this); } @@ -120,7 +143,7 @@ public class ZenModeControllerImpl extends CurrentUserTracker @Override public boolean isVolumeRestricted() { return mUserManager.hasUserRestriction(UserManager.DISALLOW_ADJUST_VOLUME, - new UserHandle(mUserId)); + UserHandle.of(mUserId)); } @Override @@ -183,19 +206,6 @@ public class ZenModeControllerImpl extends CurrentUserTracker } @Override - public void onUserSwitched(int userId) { - mUserId = userId; - if (mRegistered) { - mContext.unregisterReceiver(mReceiver); - } - final IntentFilter filter = new IntentFilter(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED); - filter.addAction(NotificationManager.ACTION_EFFECTS_SUPPRESSOR_CHANGED); - mContext.registerReceiverAsUser(mReceiver, new UserHandle(mUserId), filter, null, null); - mRegistered = true; - mSetupObserver.register(); - } - - @Override public ComponentName getEffectsSuppressor() { return NotificationManager.from(mContext).getEffectsSuppressor(); } @@ -208,7 +218,7 @@ public class ZenModeControllerImpl extends CurrentUserTracker @Override public int getCurrentUser() { - return ActivityManager.getCurrentUser(); + return mUserTracker.getUserId(); } private void fireNextAlarmChanged() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java index b1b45b51d8e4..1b7353923ada 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java @@ -58,8 +58,6 @@ import com.android.systemui.statusbar.policy.SecurityController; import com.android.systemui.statusbar.policy.SecurityControllerImpl; import com.android.systemui.statusbar.policy.UserInfoController; import com.android.systemui.statusbar.policy.UserInfoControllerImpl; -import com.android.systemui.statusbar.policy.UserSwitcherController; -import com.android.systemui.statusbar.policy.UserSwitcherControllerImpl; import com.android.systemui.statusbar.policy.WalletController; import com.android.systemui.statusbar.policy.WalletControllerImpl; import com.android.systemui.statusbar.policy.ZenModeController; @@ -198,8 +196,4 @@ public interface StatusBarPolicyModule { static DataSaverController provideDataSaverController(NetworkController networkController) { return networkController.getDataSaverController(); } - - /** Binds {@link UserSwitcherController} to its implementation. */ - @Binds - UserSwitcherController bindUserSwitcherController(UserSwitcherControllerImpl impl); } diff --git a/packages/SystemUI/src/com/android/systemui/ripple/MultiRippleController.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/MultiRippleController.kt index 48df15c78ea5..93e78acc63fd 100644 --- a/packages/SystemUI/src/com/android/systemui/ripple/MultiRippleController.kt +++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/MultiRippleController.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.ripple +package com.android.systemui.surfaceeffects.ripple import androidx.annotation.VisibleForTesting diff --git a/packages/SystemUI/src/com/android/systemui/ripple/MultiRippleView.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/MultiRippleView.kt index c7f0b7e0056e..f558fee776e6 100644 --- a/packages/SystemUI/src/com/android/systemui/ripple/MultiRippleView.kt +++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/MultiRippleView.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.ripple +package com.android.systemui.surfaceeffects.ripple import android.content.Context import android.graphics.Canvas @@ -31,11 +31,21 @@ import android.view.View class MultiRippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) { internal val ripples = ArrayList<RippleAnimation>() + private val listeners = ArrayList<RipplesFinishedListener>() private val ripplePaint = Paint() private var isWarningLogged = false companion object { - const val TAG = "MultiRippleView" + private const val TAG = "MultiRippleView" + + interface RipplesFinishedListener { + /** Triggered when all the ripples finish running. */ + fun onRipplesFinish() + } + } + + fun addRipplesFinishedListener(listener: RipplesFinishedListener) { + listeners.add(listener) } override fun onDraw(canvas: Canvas?) { @@ -62,6 +72,10 @@ class MultiRippleView(context: Context?, attrs: AttributeSet?) : View(context, a shouldInvalidate = shouldInvalidate || anim.isPlaying() } - if (shouldInvalidate) invalidate() + if (shouldInvalidate) { + invalidate() + } else { // Nothing is playing. + listeners.forEach { listener -> listener.onRipplesFinish() } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/ripple/RippleAnimation.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleAnimation.kt index aca9e254e4c3..b2f8994ebf51 100644 --- a/packages/SystemUI/src/com/android/systemui/ripple/RippleAnimation.kt +++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleAnimation.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.ripple +package com.android.systemui.surfaceeffects.ripple import android.animation.Animator import android.animation.AnimatorListenerAdapter diff --git a/packages/SystemUI/src/com/android/systemui/ripple/RippleAnimationConfig.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleAnimationConfig.kt index 88122544c7cd..ae73df201f8d 100644 --- a/packages/SystemUI/src/com/android/systemui/ripple/RippleAnimationConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleAnimationConfig.kt @@ -1,4 +1,4 @@ -package com.android.systemui.ripple +package com.android.systemui.surfaceeffects.ripple import android.graphics.Color diff --git a/packages/SystemUI/src/com/android/systemui/ripple/RippleShader.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt index d2f3a6a7bee1..a950d34513ee 100644 --- a/packages/SystemUI/src/com/android/systemui/ripple/RippleShader.kt +++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt @@ -13,11 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.systemui.ripple +package com.android.systemui.surfaceeffects.ripple import android.graphics.PointF import android.graphics.RuntimeShader import android.util.MathUtils +import com.android.systemui.surfaceeffects.shaderutil.SdfShaderLibrary +import com.android.systemui.surfaceeffects.shaderutil.ShaderUtilLibrary /** * Shader class that renders an expanding ripple effect. The ripple contains three elements: @@ -31,7 +33,7 @@ import android.util.MathUtils * Modeled after frameworks/base/graphics/java/android/graphics/drawable/RippleShader.java. */ class RippleShader internal constructor(rippleShape: RippleShape = RippleShape.CIRCLE) : - RuntimeShader(buildShader(rippleShape)) { + RuntimeShader(buildShader(rippleShape)) { /** Shapes that the [RippleShader] supports. */ enum class RippleShape { @@ -39,25 +41,30 @@ class RippleShader internal constructor(rippleShape: RippleShape = RippleShape.C ROUNDED_BOX, ELLIPSE } - //language=AGSL + // language=AGSL companion object { - private const val SHADER_UNIFORMS = """uniform vec2 in_center; - uniform vec2 in_size; - uniform float in_progress; - uniform float in_cornerRadius; - uniform float in_thickness; - uniform float in_time; - uniform float in_distort_radial; - uniform float in_distort_xy; - uniform float in_fadeSparkle; - uniform float in_fadeFill; - uniform float in_fadeRing; - uniform float in_blur; - uniform float in_pixelDensity; - layout(color) uniform vec4 in_color; - uniform float in_sparkle_strength;""" - - private const val SHADER_CIRCLE_MAIN = """vec4 main(vec2 p) { + private const val SHADER_UNIFORMS = + """ + uniform vec2 in_center; + uniform vec2 in_size; + uniform float in_progress; + uniform float in_cornerRadius; + uniform float in_thickness; + uniform float in_time; + uniform float in_distort_radial; + uniform float in_distort_xy; + uniform float in_fadeSparkle; + uniform float in_fadeFill; + uniform float in_fadeRing; + uniform float in_blur; + uniform float in_pixelDensity; + layout(color) uniform vec4 in_color; + uniform float in_sparkle_strength; + """ + + private const val SHADER_CIRCLE_MAIN = + """ + vec4 main(vec2 p) { vec2 p_distorted = distort(p, in_time, in_distort_radial, in_distort_xy); float radius = in_size.x * 0.5; float sparkleRing = soften(circleRing(p_distorted-in_center, radius), in_blur); @@ -73,7 +80,9 @@ class RippleShader internal constructor(rippleShape: RippleShape = RippleShape.C } """ - private const val SHADER_ROUNDED_BOX_MAIN = """vec4 main(vec2 p) { + private const val SHADER_ROUNDED_BOX_MAIN = + """ + vec4 main(vec2 p) { float sparkleRing = soften(roundedBoxRing(p-in_center, in_size, in_cornerRadius, in_thickness), in_blur); float inside = soften(sdRoundedBox(p-in_center, in_size * 1.2, in_cornerRadius), @@ -89,7 +98,9 @@ class RippleShader internal constructor(rippleShape: RippleShape = RippleShape.C } """ - private const val SHADER_ELLIPSE_MAIN = """vec4 main(vec2 p) { + private const val SHADER_ELLIPSE_MAIN = + """ + vec4 main(vec2 p) { vec2 p_distorted = distort(p, in_time, in_distort_radial, in_distort_xy); float sparkleRing = soften(ellipseRing(p_distorted-in_center, in_size), in_blur); @@ -105,22 +116,31 @@ class RippleShader internal constructor(rippleShape: RippleShape = RippleShape.C } """ - private const val CIRCLE_SHADER = SHADER_UNIFORMS + RippleShaderUtilLibrary.SHADER_LIB + - SdfShaderLibrary.SHADER_SDF_OPERATION_LIB + SdfShaderLibrary.CIRCLE_SDF + + private const val CIRCLE_SHADER = + SHADER_UNIFORMS + + ShaderUtilLibrary.SHADER_LIB + + SdfShaderLibrary.SHADER_SDF_OPERATION_LIB + + SdfShaderLibrary.CIRCLE_SDF + SHADER_CIRCLE_MAIN - private const val ROUNDED_BOX_SHADER = SHADER_UNIFORMS + - RippleShaderUtilLibrary.SHADER_LIB + SdfShaderLibrary.SHADER_SDF_OPERATION_LIB + - SdfShaderLibrary.ROUNDED_BOX_SDF + SHADER_ROUNDED_BOX_MAIN - private const val ELLIPSE_SHADER = SHADER_UNIFORMS + RippleShaderUtilLibrary.SHADER_LIB + - SdfShaderLibrary.SHADER_SDF_OPERATION_LIB + SdfShaderLibrary.ELLIPSE_SDF + + private const val ROUNDED_BOX_SHADER = + SHADER_UNIFORMS + + ShaderUtilLibrary.SHADER_LIB + + SdfShaderLibrary.SHADER_SDF_OPERATION_LIB + + SdfShaderLibrary.ROUNDED_BOX_SDF + + SHADER_ROUNDED_BOX_MAIN + private const val ELLIPSE_SHADER = + SHADER_UNIFORMS + + ShaderUtilLibrary.SHADER_LIB + + SdfShaderLibrary.SHADER_SDF_OPERATION_LIB + + SdfShaderLibrary.ELLIPSE_SDF + SHADER_ELLIPSE_MAIN private fun buildShader(rippleShape: RippleShape): String = - when (rippleShape) { - RippleShape.CIRCLE -> CIRCLE_SHADER - RippleShape.ROUNDED_BOX -> ROUNDED_BOX_SHADER - RippleShape.ELLIPSE -> ELLIPSE_SHADER - } + when (rippleShape) { + RippleShape.CIRCLE -> CIRCLE_SHADER + RippleShape.ROUNDED_BOX -> ROUNDED_BOX_SHADER + RippleShape.ELLIPSE -> ELLIPSE_SHADER + } private fun subProgress(start: Float, end: Float, progress: Float): Float { val min = Math.min(start, end) @@ -130,9 +150,7 @@ class RippleShader internal constructor(rippleShape: RippleShape = RippleShape.C } } - /** - * Sets the center position of the ripple. - */ + /** Sets the center position of the ripple. */ fun setCenter(x: Float, y: Float) { setFloatUniform("in_center", x, y) } @@ -144,21 +162,21 @@ class RippleShader internal constructor(rippleShape: RippleShape = RippleShape.C maxSize.y = height } - /** - * Progress of the ripple. Float value between [0, 1]. - */ + /** Progress of the ripple. Float value between [0, 1]. */ var progress: Float = 0.0f set(value) { field = value setFloatUniform("in_progress", value) val curvedProg = 1 - (1 - value) * (1 - value) * (1 - value) - setFloatUniform("in_size", /* width= */ maxSize.x * curvedProg, - /* height= */ maxSize.y * curvedProg) + setFloatUniform( + "in_size", + /* width= */ maxSize.x * curvedProg, + /* height= */ maxSize.y * curvedProg + ) setFloatUniform("in_thickness", maxSize.y * curvedProg * 0.5f) // radius should not exceed width and height values. - setFloatUniform("in_cornerRadius", - Math.min(maxSize.x, maxSize.y) * curvedProg) + setFloatUniform("in_cornerRadius", Math.min(maxSize.x, maxSize.y) * curvedProg) setFloatUniform("in_blur", MathUtils.lerp(1.25f, 0.5f, value)) @@ -175,18 +193,14 @@ class RippleShader internal constructor(rippleShape: RippleShape = RippleShape.C setFloatUniform("in_fadeRing", Math.min(fadeIn, 1 - fadeOutRipple)) } - /** - * Play time since the start of the effect. - */ + /** Play time since the start of the effect. */ var time: Float = 0.0f set(value) { field = value setFloatUniform("in_time", value) } - /** - * A hex value representing the ripple color, in the format of ARGB - */ + /** A hex value representing the ripple color, in the format of ARGB */ var color: Int = 0xffffff set(value) { field = value @@ -194,9 +208,9 @@ class RippleShader internal constructor(rippleShape: RippleShape = RippleShape.C } /** - * Noise sparkle intensity. Expected value between [0, 1]. The sparkle is white, and thus - * with strength 0 it's transparent, leaving the ripple fully smooth, while with strength 1 - * it's opaque white and looks the most grainy. + * Noise sparkle intensity. Expected value between [0, 1]. The sparkle is white, and thus with + * strength 0 it's transparent, leaving the ripple fully smooth, while with strength 1 it's + * opaque white and looks the most grainy. */ var sparkleStrength: Float = 0.0f set(value) { @@ -204,9 +218,7 @@ class RippleShader internal constructor(rippleShape: RippleShape = RippleShape.C setFloatUniform("in_sparkle_strength", value) } - /** - * Distortion strength of the ripple. Expected value between[0, 1]. - */ + /** Distortion strength of the ripple. Expected value between[0, 1]. */ var distortionStrength: Float = 0.0f set(value) { field = value diff --git a/packages/SystemUI/src/com/android/systemui/ripple/RippleView.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt index a6d79303962f..299469494295 100644 --- a/packages/SystemUI/src/com/android/systemui/ripple/RippleView.kt +++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.ripple +package com.android.systemui.surfaceeffects.ripple import android.animation.Animator import android.animation.AnimatorListenerAdapter @@ -26,7 +26,7 @@ import android.graphics.Paint import android.util.AttributeSet import android.view.View import androidx.core.graphics.ColorUtils -import com.android.systemui.ripple.RippleShader.RippleShape +import com.android.systemui.surfaceeffects.ripple.RippleShader.RippleShape /** * A generic expanding ripple effect. @@ -98,15 +98,18 @@ open class RippleView(context: Context?, attrs: AttributeSet?) : View(context, a rippleShader.time = now.toFloat() invalidate() } - animator.addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { - onAnimationEnd?.run() + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator?) { + onAnimationEnd?.run() + } } - }) + ) animator.start() } - /** Set the color to be used for the ripple. + /** + * Set the color to be used for the ripple. * * The alpha value of the color will be applied to the ripple. The alpha range is [0-100]. */ @@ -123,9 +126,7 @@ open class RippleView(context: Context?, attrs: AttributeSet?) : View(context, a rippleShader.rippleFill = rippleFill } - /** - * Set the intensity of the sparkles. - */ + /** Set the intensity of the sparkles. */ fun setSparkleStrength(strength: Float) { rippleShader.sparkleStrength = strength } @@ -143,20 +144,30 @@ open class RippleView(context: Context?, attrs: AttributeSet?) : View(context, a // active effect area. Values here should be kept in sync with the animation implementation // in the ripple shader. if (rippleShape == RippleShape.CIRCLE) { - val maskRadius = (1 - (1 - rippleShader.progress) * (1 - rippleShader.progress) * - (1 - rippleShader.progress)) * maxWidth + val maskRadius = + (1 - + (1 - rippleShader.progress) * + (1 - rippleShader.progress) * + (1 - rippleShader.progress)) * maxWidth canvas.drawCircle(centerX, centerY, maskRadius, ripplePaint) } else { - val maskWidth = (1 - (1 - rippleShader.progress) * (1 - rippleShader.progress) * - (1 - rippleShader.progress)) * maxWidth * 2 - val maskHeight = (1 - (1 - rippleShader.progress) * (1 - rippleShader.progress) * - (1 - rippleShader.progress)) * maxHeight * 2 + val maskWidth = + (1 - + (1 - rippleShader.progress) * + (1 - rippleShader.progress) * + (1 - rippleShader.progress)) * maxWidth * 2 + val maskHeight = + (1 - + (1 - rippleShader.progress) * + (1 - rippleShader.progress) * + (1 - rippleShader.progress)) * maxHeight * 2 canvas.drawRect( - /* left= */ centerX - maskWidth, - /* top= */ centerY - maskHeight, - /* right= */ centerX + maskWidth, - /* bottom= */ centerY + maskHeight, - ripplePaint) + /* left= */ centerX - maskWidth, + /* top= */ centerY - maskHeight, + /* right= */ centerX + maskWidth, + /* bottom= */ centerY + maskHeight, + ripplePaint + ) } } } diff --git a/packages/SystemUI/src/com/android/systemui/ripple/SdfShaderLibrary.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/shaderutil/SdfShaderLibrary.kt index 5e256c653992..8b2f46648fef 100644 --- a/packages/SystemUI/src/com/android/systemui/ripple/SdfShaderLibrary.kt +++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/shaderutil/SdfShaderLibrary.kt @@ -13,13 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.systemui.ripple +package com.android.systemui.surfaceeffects.shaderutil /** Library class that contains 2D signed distance functions. */ class SdfShaderLibrary { - //language=AGSL + // language=AGSL companion object { - const val CIRCLE_SDF = """ + const val CIRCLE_SDF = + """ float sdCircle(vec2 p, float r) { return (length(p)-r) / r; } @@ -34,7 +35,8 @@ class SdfShaderLibrary { } """ - const val ROUNDED_BOX_SDF = """ + const val ROUNDED_BOX_SDF = + """ float sdRoundedBox(vec2 p, vec2 size, float cornerRadius) { size *= 0.5; cornerRadius *= 0.5; @@ -58,7 +60,8 @@ class SdfShaderLibrary { // Used non-trigonometry parametrization and Halley's method (iterative) for root finding. // This is more expensive than the regular circle SDF, recommend to use the circle SDF if // possible. - const val ELLIPSE_SDF = """float sdEllipse(vec2 p, vec2 wh) { + const val ELLIPSE_SDF = + """float sdEllipse(vec2 p, vec2 wh) { wh *= 0.5; // symmetry @@ -98,7 +101,8 @@ class SdfShaderLibrary { } """ - const val SHADER_SDF_OPERATION_LIB = """ + const val SHADER_SDF_OPERATION_LIB = + """ float soften(float d, float blur) { float blurHalf = blur * 0.5; return smoothstep(-blurHalf, blurHalf, d); diff --git a/packages/SystemUI/src/com/android/systemui/surfaceeffects/shaderutil/ShaderUtilLibrary.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/shaderutil/ShaderUtilLibrary.kt new file mode 100644 index 000000000000..d78e0c153db8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/shaderutil/ShaderUtilLibrary.kt @@ -0,0 +1,148 @@ +/* + * 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.surfaceeffects.shaderutil + +/** A common utility functions that are used for computing shaders. */ +class ShaderUtilLibrary { + // language=AGSL + companion object { + const val SHADER_LIB = + """ + float triangleNoise(vec2 n) { + n = fract(n * vec2(5.3987, 5.4421)); + n += dot(n.yx, n.xy + vec2(21.5351, 14.3137)); + float xy = n.x * n.y; + // compute in [0..2[ and remap to [-1.0..1.0[ + return fract(xy * 95.4307) + fract(xy * 75.04961) - 1.0; + } + + const float PI = 3.1415926535897932384626; + + float sparkles(vec2 uv, float t) { + float n = triangleNoise(uv); + float s = 0.0; + for (float i = 0; i < 4; i += 1) { + float l = i * 0.01; + float h = l + 0.1; + float o = smoothstep(n - l, h, n); + o *= abs(sin(PI * o * (t + 0.55 * i))); + s += o; + } + return s; + } + + vec2 distort(vec2 p, float time, float distort_amount_radial, + float distort_amount_xy) { + float angle = atan(p.y, p.x); + return p + vec2(sin(angle * 8 + time * 0.003 + 1.641), + cos(angle * 5 + 2.14 + time * 0.00412)) * distort_amount_radial + + vec2(sin(p.x * 0.01 + time * 0.00215 + 0.8123), + cos(p.y * 0.01 + time * 0.005931)) * distort_amount_xy; + } + + // Return range [-1, 1]. + vec3 hash(vec3 p) { + p = fract(p * vec3(.3456, .1234, .9876)); + p += dot(p, p.yxz + 43.21); + p = (p.xxy + p.yxx) * p.zyx; + return (fract(sin(p) * 4567.1234567) - .5) * 2.; + } + + // Skew factors (non-uniform). + const float SKEW = 0.3333333; // 1/3 + const float UNSKEW = 0.1666667; // 1/6 + + // Return range roughly [-1,1]. + // It's because the hash function (that returns a random gradient vector) returns + // different magnitude of vectors. Noise doesn't have to be in the precise range thus + // skipped normalize. + float simplex3d(vec3 p) { + // Skew the input coordinate, so that we get squashed cubical grid + vec3 s = floor(p + (p.x + p.y + p.z) * SKEW); + + // Unskew back + vec3 u = s - (s.x + s.y + s.z) * UNSKEW; + + // Unskewed coordinate that is relative to p, to compute the noise contribution + // based on the distance. + vec3 c0 = p - u; + + // We have six simplices (in this case tetrahedron, since we are in 3D) that we + // could possibly in. + // Here, we are finding the correct tetrahedron (simplex shape), and traverse its + // four vertices (c0..3) when computing noise contribution. + // The way we find them is by comparing c0's x,y,z values. + // For example in 2D, we can find the triangle (simplex shape in 2D) that we are in + // by comparing x and y values. i.e. x>y lower, x<y, upper triangle. + // Same applies in 3D. + // + // Below indicates the offsets (or offset directions) when c0=(x0,y0,z0) + // x0>y0>z0: (1,0,0), (1,1,0), (1,1,1) + // x0>z0>y0: (1,0,0), (1,0,1), (1,1,1) + // z0>x0>y0: (0,0,1), (1,0,1), (1,1,1) + // z0>y0>x0: (0,0,1), (0,1,1), (1,1,1) + // y0>z0>x0: (0,1,0), (0,1,1), (1,1,1) + // y0>x0>z0: (0,1,0), (1,1,0), (1,1,1) + // + // The rule is: + // * For offset1, set 1 at the max component, otherwise 0. + // * For offset2, set 0 at the min component, otherwise 1. + // * For offset3, set 1 for all. + // + // Encode x0-y0, y0-z0, z0-x0 in a vec3 + vec3 en = c0 - c0.yzx; + // Each represents whether x0>y0, y0>z0, z0>x0 + en = step(vec3(0.), en); + // en.zxy encodes z0>x0, x0>y0, y0>x0 + vec3 offset1 = en * (1. - en.zxy); // find max + vec3 offset2 = 1. - en.zxy * (1. - en); // 1-(find min) + vec3 offset3 = vec3(1.); + + vec3 c1 = c0 - offset1 + UNSKEW; + vec3 c2 = c0 - offset2 + UNSKEW * 2.; + vec3 c3 = c0 - offset3 + UNSKEW * 3.; + + // Kernel summation: dot(max(0, r^2-d^2))^4, noise contribution) + // + // First compute d^2, squared distance to the point. + vec4 w; // w = max(0, r^2 - d^2)) + w.x = dot(c0, c0); + w.y = dot(c1, c1); + w.z = dot(c2, c2); + w.w = dot(c3, c3); + + // Noise contribution should decay to zero before they cross the simplex boundary. + // Usually r^2 is 0.5 or 0.6; + // 0.5 ensures continuity but 0.6 increases the visual quality for the application + // where discontinuity isn't noticeable. + w = max(0.6 - w, 0.); + + // Noise contribution from each point. + vec4 nc; + nc.x = dot(hash(s), c0); + nc.y = dot(hash(s + offset1), c1); + nc.z = dot(hash(s + offset2), c2); + nc.w = dot(hash(s + offset3), c3); + + nc *= w*w*w*w; + + // Add all the noise contributions. + // Should multiply by the possible max contribution to adjust the range in [-1,1]. + return dot(vec4(32.), nc); + } + """ + } +} diff --git a/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt new file mode 100644 index 000000000000..5ac3aad749fc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt @@ -0,0 +1,67 @@ +/* + * 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.surfaceeffects.turbulencenoise + +import android.graphics.BlendMode +import android.graphics.Color + +/** Turbulence noise animation configuration. */ +data class TurbulenceNoiseAnimationConfig( + /** The number of grids that is used to generate noise. */ + val gridCount: Float = DEFAULT_NOISE_GRID_COUNT, + + /** Multiplier for the noise luma matte. Increase this for brighter effects. */ + val luminosityMultiplier: Float = DEFAULT_LUMINOSITY_MULTIPLIER, + + /** + * Noise move speed variables. + * + * Its sign determines the direction; magnitude determines the speed. <ul> + * ``` + * <li> [noiseMoveSpeedX] positive: right to left; negative: left to right. + * <li> [noiseMoveSpeedY] positive: bottom to top; negative: top to bottom. + * <li> [noiseMoveSpeedZ] its sign doesn't matter much, as it moves in Z direction. Use it + * to add turbulence in place. + * ``` + * </ul> + */ + val noiseMoveSpeedX: Float = 0f, + val noiseMoveSpeedY: Float = 0f, + val noiseMoveSpeedZ: Float = DEFAULT_NOISE_SPEED_Z, + + /** Color of the effect. */ + var color: Int = DEFAULT_COLOR, + /** Background color of the effect. */ + val backgroundColor: Int = DEFAULT_BACKGROUND_COLOR, + val opacity: Int = DEFAULT_OPACITY, + val width: Float = 0f, + val height: Float = 0f, + val duration: Float = DEFAULT_NOISE_DURATION_IN_MILLIS, + val pixelDensity: Float = 1f, + val blendMode: BlendMode = DEFAULT_BLEND_MODE, + val onAnimationEnd: Runnable? = null +) { + companion object { + const val DEFAULT_NOISE_DURATION_IN_MILLIS = 7500F + const val DEFAULT_LUMINOSITY_MULTIPLIER = 1f + const val DEFAULT_NOISE_GRID_COUNT = 1.2f + const val DEFAULT_NOISE_SPEED_Z = 0.3f + const val DEFAULT_OPACITY = 150 // full opacity is 255. + const val DEFAULT_COLOR = Color.WHITE + const val DEFAULT_BACKGROUND_COLOR = Color.BLACK + val DEFAULT_BLEND_MODE = BlendMode.SRC_OVER + } +} diff --git a/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseController.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseController.kt new file mode 100644 index 000000000000..4c7e5f4c7093 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseController.kt @@ -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. + */ +package com.android.systemui.surfaceeffects.turbulencenoise + +/** A controller that plays [TurbulenceNoiseView]. */ +class TurbulenceNoiseController(private val turbulenceNoiseView: TurbulenceNoiseView) { + /** Updates the color of the noise. */ + fun updateNoiseColor(color: Int) { + turbulenceNoiseView.updateColor(color) + } + + // TODO: add cancel and/ or pause once design requirements become clear. + /** Plays [TurbulenceNoiseView] with the given config. */ + fun play(turbulenceNoiseAnimationConfig: TurbulenceNoiseAnimationConfig) { + turbulenceNoiseView.play(turbulenceNoiseAnimationConfig) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt new file mode 100644 index 000000000000..19c114d2693c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.surfaceeffects.turbulencenoise + +import android.graphics.RuntimeShader +import com.android.systemui.surfaceeffects.shaderutil.ShaderUtilLibrary +import java.lang.Float.max + +/** Shader that renders turbulence simplex noise, with no octave. */ +class TurbulenceNoiseShader : RuntimeShader(TURBULENCE_NOISE_SHADER) { + // language=AGSL + companion object { + private const val UNIFORMS = + """ + uniform float in_gridNum; + uniform vec3 in_noiseMove; + uniform vec2 in_size; + uniform float in_aspectRatio; + uniform float in_opacity; + uniform float in_pixelDensity; + layout(color) uniform vec4 in_color; + layout(color) uniform vec4 in_backgroundColor; + """ + + private const val SHADER_LIB = + """ + float getLuminosity(vec3 c) { + return 0.3*c.r + 0.59*c.g + 0.11*c.b; + } + + vec3 maskLuminosity(vec3 dest, float lum) { + dest.rgb *= vec3(lum); + // Clip back into the legal range + dest = clamp(dest, vec3(0.), vec3(1.0)); + return dest; + } + """ + + private const val MAIN_SHADER = + """ + vec4 main(vec2 p) { + vec2 uv = p / in_size.xy; + uv.x *= in_aspectRatio; + + vec3 noiseP = vec3(uv + in_noiseMove.xy, in_noiseMove.z) * in_gridNum; + float luma = simplex3d(noiseP) * in_opacity; + vec3 mask = maskLuminosity(in_color.rgb, luma); + vec3 color = in_backgroundColor.rgb + mask * 0.6; + + // Add dither with triangle distribution to avoid color banding. Ok to dither in the + // shader here as we are in gamma space. + float dither = triangleNoise(p * in_pixelDensity) / 255.; + + // The result color should be pre-multiplied, i.e. [R*A, G*A, B*A, A], thus need to + // multiply rgb with a to get the correct result. + color = (color + dither.rrr) * in_color.a; + return vec4(color, in_color.a); + } + """ + + private const val TURBULENCE_NOISE_SHADER = + ShaderUtilLibrary.SHADER_LIB + UNIFORMS + SHADER_LIB + MAIN_SHADER + } + + /** Sets the number of grid for generating noise. */ + fun setGridCount(gridNumber: Float = 1.0f) { + setFloatUniform("in_gridNum", gridNumber) + } + + /** + * Sets the pixel density of the screen. + * + * Used it for noise dithering. + */ + fun setPixelDensity(pixelDensity: Float) { + setFloatUniform("in_pixelDensity", pixelDensity) + } + + /** Sets the noise color of the effect. */ + fun setColor(color: Int) { + setColorUniform("in_color", color) + } + + /** Sets the background color of the effect. */ + fun setBackgroundColor(color: Int) { + setColorUniform("in_backgroundColor", color) + } + + /** + * Sets the opacity to achieve fade in/ out of the animation. + * + * Expected value range is [1, 0]. + */ + fun setOpacity(opacity: Float) { + setFloatUniform("in_opacity", opacity) + } + + /** Sets the size of the shader. */ + fun setSize(width: Float, height: Float) { + setFloatUniform("in_size", width, height) + setFloatUniform("in_aspectRatio", width / max(height, 0.001f)) + } + + /** Sets noise move speed in x, y, and z direction. */ + fun setNoiseMove(x: Float, y: Float, z: Float) { + setFloatUniform("in_noiseMove", x, y, z) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseView.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseView.kt new file mode 100644 index 000000000000..8649d5924587 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseView.kt @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.surfaceeffects.turbulencenoise + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.util.AttributeSet +import android.view.View +import androidx.annotation.VisibleForTesting +import androidx.core.graphics.ColorUtils +import java.util.Random +import kotlin.math.sin + +/** View that renders turbulence noise effect. */ +class TurbulenceNoiseView(context: Context?, attrs: AttributeSet?) : View(context, attrs) { + + companion object { + private const val MS_TO_SEC = 0.001f + private const val TWO_PI = Math.PI.toFloat() * 2f + } + + @VisibleForTesting val turbulenceNoiseShader = TurbulenceNoiseShader() + private val paint = Paint().apply { this.shader = turbulenceNoiseShader } + private val random = Random() + private val animator: ValueAnimator = ValueAnimator.ofFloat(0f, 1f) + private var config: TurbulenceNoiseAnimationConfig? = null + + val isPlaying: Boolean + get() = animator.isRunning + + init { + // Only visible during the animation. + visibility = INVISIBLE + } + + /** Updates the color during the animation. No-op if there's no animation playing. */ + fun updateColor(color: Int) { + config?.let { + it.color = color + applyConfig(it) + } + } + + override fun onDraw(canvas: Canvas?) { + if (canvas == null || !canvas.isHardwareAccelerated) { + // Drawing with the turbulence noise shader requires hardware acceleration, so skip + // if it's unsupported. + return + } + + canvas.drawPaint(paint) + } + + internal fun play(config: TurbulenceNoiseAnimationConfig) { + if (isPlaying) { + return // Ignore if the animation is playing. + } + visibility = VISIBLE + applyConfig(config) + + // Add random offset to avoid same patterned noise. + val offsetX = random.nextFloat() + val offsetY = random.nextFloat() + + animator.duration = config.duration.toLong() + animator.addUpdateListener { updateListener -> + val timeInSec = updateListener.currentPlayTime * MS_TO_SEC + // Remap [0,1] to [0, 2*PI] + val progress = TWO_PI * updateListener.animatedValue as Float + + turbulenceNoiseShader.setNoiseMove( + offsetX + timeInSec * config.noiseMoveSpeedX, + offsetY + timeInSec * config.noiseMoveSpeedY, + timeInSec * config.noiseMoveSpeedZ + ) + + // Fade in and out the noise as the animation progress. + // TODO: replace it with a better curve + turbulenceNoiseShader.setOpacity(sin(TWO_PI - progress) * config.luminosityMultiplier) + + invalidate() + } + + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + visibility = INVISIBLE + config.onAnimationEnd?.run() + } + } + ) + animator.start() + } + + private fun applyConfig(config: TurbulenceNoiseAnimationConfig) { + this.config = config + with(turbulenceNoiseShader) { + setGridCount(config.gridCount) + setColor(ColorUtils.setAlphaComponent(config.color, config.opacity)) + setBackgroundColor(config.backgroundColor) + setSize(config.width, config.height) + setPixelDensity(config.pixelDensity) + } + paint.blendMode = config.blendMode + } +} diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt index 82703364a1d5..a9d05d11dc00 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 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora 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 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora 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 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora // 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 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora } wakeLock?.acquire(newInfo.wakeReason) wakeReasonAcquired = newInfo.wakeReason - logger.logViewAddition(newInfo.windowTitle) + logger.logViewAddition(id, newInfo.windowTitle) inflateAndUpdateView(newInfo) } @@ -151,9 +167,13 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora // 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 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora } /** - * 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 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora } 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 cbb500296888..df8396051dda 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 @@ abstract class TemporaryViewInfo { * 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 428a104484a7..133a384e7e17 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 @@ open class TemporaryViewLogger( 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 6237365d0cee..b92e0ec0428f 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 @@ data class ChipbarInfo( 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/src/com/android/systemui/usb/StorageNotification.java b/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java index bf706735d531..095718bddf97 100644 --- a/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java +++ b/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java @@ -46,6 +46,7 @@ import com.android.internal.R; import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; import com.android.systemui.CoreStartable; import com.android.systemui.SystemUIApplication; +import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.util.NotificationChannels; @@ -61,15 +62,24 @@ public class StorageNotification implements CoreStartable { private static final String ACTION_SNOOZE_VOLUME = "com.android.systemui.action.SNOOZE_VOLUME"; private static final String ACTION_FINISH_WIZARD = "com.android.systemui.action.FINISH_WIZARD"; private final Context mContext; + private final BroadcastDispatcher mBroadcastDispatcher; // TODO: delay some notifications to avoid bumpy fast operations - private NotificationManager mNotificationManager; - private StorageManager mStorageManager; + private final NotificationManager mNotificationManager; + private final StorageManager mStorageManager; @Inject - public StorageNotification(Context context) { + public StorageNotification( + Context context, + BroadcastDispatcher broadcastDispatcher, + NotificationManager notificationManager, + StorageManager storageManager + ) { mContext = context; + mBroadcastDispatcher = broadcastDispatcher; + mNotificationManager = notificationManager; + mStorageManager = storageManager; } private static class MoveInfo { @@ -168,17 +178,22 @@ public class StorageNotification implements CoreStartable { @Override public void start() { - mNotificationManager = mContext.getSystemService(NotificationManager.class); - - mStorageManager = mContext.getSystemService(StorageManager.class); mStorageManager.registerListener(mListener); - mContext.registerReceiver(mSnoozeReceiver, new IntentFilter(ACTION_SNOOZE_VOLUME), - android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS, null, - Context.RECEIVER_EXPORTED_UNAUDITED); - mContext.registerReceiver(mFinishReceiver, new IntentFilter(ACTION_FINISH_WIZARD), - android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS, null, - Context.RECEIVER_EXPORTED_UNAUDITED); + mBroadcastDispatcher.registerReceiver( + mSnoozeReceiver, + new IntentFilter(ACTION_SNOOZE_VOLUME), + null, + null, + Context.RECEIVER_EXPORTED_UNAUDITED, + android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS); + mBroadcastDispatcher.registerReceiver( + mFinishReceiver, + new IntentFilter(ACTION_FINISH_WIZARD), + null, + null, + Context.RECEIVER_EXPORTED_UNAUDITED, + android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS); // Kick current state into place final List<DiskInfo> disks = mStorageManager.getDisks(); @@ -381,7 +396,7 @@ public class StorageNotification implements CoreStartable { // Don't annoy when user dismissed in past. (But make sure the disk is adoptable; we // used to allow snoozing non-adoptable disks too.) - if (rec.isSnoozed() && disk.isAdoptable()) { + if (rec == null || (rec.isSnoozed() && disk.isAdoptable())) { return null; } if (disk.isAdoptable() && !rec.isInited() && rec.getType() != VolumeInfo.TYPE_PUBLIC diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt index ffaf524bb0d1..ed53de7dbee7 100644 --- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt @@ -19,31 +19,18 @@ package com.android.systemui.user.data.repository import android.content.Context import android.content.pm.UserInfo -import android.graphics.drawable.BitmapDrawable -import android.graphics.drawable.Drawable import android.os.UserHandle import android.os.UserManager import android.provider.Settings import androidx.annotation.VisibleForTesting -import androidx.appcompat.content.res.AppCompatResources -import com.android.internal.util.UserIcons -import com.android.systemui.R import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow -import com.android.systemui.common.shared.model.Text 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 com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.settings.UserTracker -import com.android.systemui.statusbar.policy.UserSwitcherController import com.android.systemui.user.data.model.UserSwitcherSettingsModel -import com.android.systemui.user.data.source.UserRecord -import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper -import com.android.systemui.user.shared.model.UserActionModel -import com.android.systemui.user.shared.model.UserModel import com.android.systemui.util.settings.GlobalSettings import com.android.systemui.util.settings.SettingsProxyExt.observerFlow import java.util.concurrent.atomic.AtomicBoolean @@ -55,7 +42,6 @@ import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map @@ -72,15 +58,6 @@ import kotlinx.coroutines.withContext * upstream changes. */ interface UserRepository { - /** List of all users on the device. */ - val users: Flow<List<UserModel>> - - /** The currently-selected user. */ - val selectedUser: Flow<UserModel> - - /** List of available user-related actions. */ - val actions: Flow<List<UserActionModel>> - /** User switcher related settings. */ val userSwitcherSettings: Flow<UserSwitcherSettingsModel> @@ -93,9 +70,6 @@ interface UserRepository { /** User ID of the last non-guest selected user. */ val lastSelectedNonGuestUserId: Int - /** Whether actions are available even when locked. */ - val isActionableWhenLocked: Flow<Boolean> - /** Whether the device is configured to always have a guest user available. */ val isGuestUserAutoCreated: Boolean @@ -125,18 +99,13 @@ class UserRepositoryImpl constructor( @Application private val appContext: Context, private val manager: UserManager, - private val controller: UserSwitcherController, @Application private val applicationScope: CoroutineScope, @Main private val mainDispatcher: CoroutineDispatcher, @Background private val backgroundDispatcher: CoroutineDispatcher, private val globalSettings: GlobalSettings, private val tracker: UserTracker, - private val featureFlags: FeatureFlags, ) : UserRepository { - private val isNewImpl: Boolean - get() = !featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER) - private val _userSwitcherSettings = MutableStateFlow(runBlocking { getSettings() }) override val userSwitcherSettings: Flow<UserSwitcherSettingsModel> = _userSwitcherSettings.asStateFlow().filterNotNull() @@ -150,58 +119,11 @@ constructor( override var lastSelectedNonGuestUserId: Int = UserHandle.USER_SYSTEM private set - private val userRecords: Flow<List<UserRecord>> = conflatedCallbackFlow { - fun send() { - trySendWithFailureLogging( - controller.users, - TAG, - ) - } - - val callback = UserSwitcherController.UserSwitchCallback { send() } - - controller.addUserSwitchCallback(callback) - send() - - awaitClose { controller.removeUserSwitchCallback(callback) } - } - - override val users: Flow<List<UserModel>> = - userRecords.map { records -> records.filter { it.isUser() }.map { it.toUserModel() } } - - override val selectedUser: Flow<UserModel> = - users.map { users -> users.first { user -> user.isSelected } } - - override val actions: Flow<List<UserActionModel>> = - userRecords.map { records -> records.filter { it.isNotUser() }.map { it.toActionModel() } } - - override val isActionableWhenLocked: Flow<Boolean> = - if (isNewImpl) { - emptyFlow() - } else { - controller.isAddUsersFromLockScreenEnabled - } - override val isGuestUserAutoCreated: Boolean = - if (isNewImpl) { - appContext.resources.getBoolean(com.android.internal.R.bool.config_guestUserAutoCreated) - } else { - controller.isGuestUserAutoCreated - } + appContext.resources.getBoolean(com.android.internal.R.bool.config_guestUserAutoCreated) private var _isGuestUserResetting: Boolean = false - override var isGuestUserResetting: Boolean = - if (isNewImpl) { - _isGuestUserResetting - } else { - controller.isGuestUserResetting - } - set(value) = - if (isNewImpl) { - _isGuestUserResetting = value - } else { - error("Not supported in the old implementation!") - } + override var isGuestUserResetting: Boolean = _isGuestUserResetting override val isGuestUserCreationScheduled = AtomicBoolean() @@ -210,10 +132,8 @@ constructor( override var isRefreshUsersPaused: Boolean = false init { - if (isNewImpl) { - observeSelectedUser() - observeUserSettings() - } + observeSelectedUser() + observeUserSettings() } override fun refreshUsers() { @@ -327,64 +247,6 @@ constructor( } } - private fun UserRecord.isUser(): Boolean { - return when { - isAddUser -> false - isAddSupervisedUser -> false - isManageUsers -> false - isGuest -> info != null - else -> true - } - } - - private fun UserRecord.isNotUser(): Boolean { - return !isUser() - } - - private fun UserRecord.toUserModel(): UserModel { - return UserModel( - id = resolveId(), - name = getUserName(this), - image = getUserImage(this), - isSelected = isCurrent, - isSelectable = isSwitchToEnabled || isGuest, - isGuest = isGuest, - ) - } - - private fun UserRecord.toActionModel(): UserActionModel { - return when { - isAddUser -> UserActionModel.ADD_USER - isAddSupervisedUser -> UserActionModel.ADD_SUPERVISED_USER - isGuest -> UserActionModel.ENTER_GUEST_MODE - isManageUsers -> UserActionModel.NAVIGATE_TO_USER_MANAGEMENT - else -> error("Don't know how to convert to UserActionModel: $this") - } - } - - private fun getUserName(record: UserRecord): Text { - val resourceId: Int? = LegacyUserUiHelper.getGuestUserRecordNameResourceId(record) - return if (resourceId != null) { - Text.Resource(resourceId) - } else { - Text.Loaded(checkNotNull(record.info).name) - } - } - - private fun getUserImage(record: UserRecord): Drawable { - if (record.isGuest) { - return checkNotNull( - AppCompatResources.getDrawable(appContext, R.drawable.ic_account_circle) - ) - } - - val userId = checkNotNull(record.info?.id) - return manager.getUserIcon(userId)?.let { userSelectedIcon -> - BitmapDrawable(userSelectedIcon) - } - ?: UserIcons.getDefaultUserIcon(appContext.resources, userId, /* light= */ false) - } - companion object { private const val TAG = "UserRepository" @VisibleForTesting const val SETTING_SIMPLE_USER_SWITCHER = "lockscreenSimpleUserSwitcher" diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt index dda78aad54c6..6b81bf2cfb08 100644 --- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt @@ -39,12 +39,9 @@ import com.android.systemui.common.shared.model.Text import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.plugins.ActivityStarter import com.android.systemui.qs.user.UserSwitchDialogController -import com.android.systemui.statusbar.policy.UserSwitcherController import com.android.systemui.telephony.domain.interactor.TelephonyInteractor import com.android.systemui.user.data.repository.UserRepository import com.android.systemui.user.data.source.UserRecord @@ -64,8 +61,6 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @@ -82,10 +77,8 @@ class UserInteractor constructor( @Application private val applicationContext: Context, private val repository: UserRepository, - private val controller: UserSwitcherController, private val activityStarter: ActivityStarter, private val keyguardInteractor: KeyguardInteractor, - private val featureFlags: FeatureFlags, private val manager: UserManager, @Application private val applicationScope: CoroutineScope, telephonyInteractor: TelephonyInteractor, @@ -107,9 +100,6 @@ constructor( fun onUserStateChanged() } - private val isNewImpl: Boolean - get() = !featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER) - private val supervisedUserPackageName: String? get() = applicationContext.getString( @@ -122,181 +112,146 @@ constructor( /** List of current on-device users to select from. */ val users: Flow<List<UserModel>> get() = - if (isNewImpl) { - combine( - repository.userInfos, - repository.selectedUserInfo, - repository.userSwitcherSettings, - ) { userInfos, selectedUserInfo, settings -> - toUserModels( - userInfos = userInfos, - selectedUserId = selectedUserInfo.id, - isUserSwitcherEnabled = settings.isUserSwitcherEnabled, - ) - } - } else { - repository.users + combine( + repository.userInfos, + repository.selectedUserInfo, + repository.userSwitcherSettings, + ) { userInfos, selectedUserInfo, settings -> + toUserModels( + userInfos = userInfos, + selectedUserId = selectedUserInfo.id, + isUserSwitcherEnabled = settings.isUserSwitcherEnabled, + ) } /** The currently-selected user. */ val selectedUser: Flow<UserModel> get() = - if (isNewImpl) { - combine( - repository.selectedUserInfo, - repository.userSwitcherSettings, - ) { selectedUserInfo, settings -> - val selectedUserId = selectedUserInfo.id - checkNotNull( - toUserModel( - userInfo = selectedUserInfo, - selectedUserId = selectedUserId, - canSwitchUsers = canSwitchUsers(selectedUserId), - isUserSwitcherEnabled = settings.isUserSwitcherEnabled, - ) + combine( + repository.selectedUserInfo, + repository.userSwitcherSettings, + ) { selectedUserInfo, settings -> + val selectedUserId = selectedUserInfo.id + checkNotNull( + toUserModel( + userInfo = selectedUserInfo, + selectedUserId = selectedUserId, + canSwitchUsers = canSwitchUsers(selectedUserId), + isUserSwitcherEnabled = settings.isUserSwitcherEnabled, ) - } - } else { - repository.selectedUser + ) } /** List of user-switcher related actions that are available. */ val actions: Flow<List<UserActionModel>> get() = - if (isNewImpl) { - combine( - repository.selectedUserInfo, - repository.userInfos, - repository.userSwitcherSettings, - keyguardInteractor.isKeyguardShowing, - ) { _, userInfos, settings, isDeviceLocked -> - buildList { - val hasGuestUser = userInfos.any { it.isGuest } - if ( - !hasGuestUser && - (guestUserInteractor.isGuestUserAutoCreated || - UserActionsUtil.canCreateGuest( - manager, - repository, - settings.isUserSwitcherEnabled, - settings.isAddUsersFromLockscreen, - )) - ) { - add(UserActionModel.ENTER_GUEST_MODE) - } - - if (!isDeviceLocked || settings.isAddUsersFromLockscreen) { - // The device is locked and our setting to allow actions that add users - // from the lock-screen is not enabled. The guest action from above is - // always allowed, even when the device is locked, but the various "add - // user" actions below are not. We can finish building the list here. - - val canCreateUsers = - UserActionsUtil.canCreateUser( + combine( + repository.selectedUserInfo, + repository.userInfos, + repository.userSwitcherSettings, + keyguardInteractor.isKeyguardShowing, + ) { _, userInfos, settings, isDeviceLocked -> + buildList { + val hasGuestUser = userInfos.any { it.isGuest } + if ( + !hasGuestUser && + (guestUserInteractor.isGuestUserAutoCreated || + UserActionsUtil.canCreateGuest( manager, repository, settings.isUserSwitcherEnabled, settings.isAddUsersFromLockscreen, - ) + )) + ) { + add(UserActionModel.ENTER_GUEST_MODE) + } - if (canCreateUsers) { - add(UserActionModel.ADD_USER) - } + if (!isDeviceLocked || settings.isAddUsersFromLockscreen) { + // The device is locked and our setting to allow actions that add users + // from the lock-screen is not enabled. The guest action from above is + // always allowed, even when the device is locked, but the various "add + // user" actions below are not. We can finish building the list here. - if ( - UserActionsUtil.canCreateSupervisedUser( - manager, - repository, - settings.isUserSwitcherEnabled, - settings.isAddUsersFromLockscreen, - supervisedUserPackageName, - ) - ) { - add(UserActionModel.ADD_SUPERVISED_USER) - } + val canCreateUsers = + UserActionsUtil.canCreateUser( + manager, + repository, + settings.isUserSwitcherEnabled, + settings.isAddUsersFromLockscreen, + ) + + if (canCreateUsers) { + add(UserActionModel.ADD_USER) } if ( - UserActionsUtil.canManageUsers( + UserActionsUtil.canCreateSupervisedUser( + manager, repository, settings.isUserSwitcherEnabled, settings.isAddUsersFromLockscreen, + supervisedUserPackageName, ) ) { - add(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) + add(UserActionModel.ADD_SUPERVISED_USER) } } - } - } else { - combine( - repository.isActionableWhenLocked, - keyguardInteractor.isKeyguardShowing, - ) { isActionableWhenLocked, isLocked -> - isActionableWhenLocked || !isLocked - } - .flatMapLatest { isActionable -> - if (isActionable) { - repository.actions - } else { - // If not actionable it means that we're not allowed to show actions - // when - // locked and we are locked. Therefore, we should show no actions. - flowOf(emptyList()) - } + + if ( + UserActionsUtil.canManageUsers( + repository, + settings.isUserSwitcherEnabled, + settings.isAddUsersFromLockscreen, + ) + ) { + add(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) } + } } val userRecords: StateFlow<ArrayList<UserRecord>> = - if (isNewImpl) { - combine( - repository.userInfos, - repository.selectedUserInfo, - actions, - repository.userSwitcherSettings, - ) { userInfos, selectedUserInfo, actionModels, settings -> - ArrayList( - userInfos.map { + combine( + repository.userInfos, + repository.selectedUserInfo, + actions, + repository.userSwitcherSettings, + ) { userInfos, selectedUserInfo, actionModels, settings -> + ArrayList( + userInfos.map { + toRecord( + userInfo = it, + selectedUserId = selectedUserInfo.id, + ) + } + + actionModels.map { toRecord( - userInfo = it, + action = it, selectedUserId = selectedUserInfo.id, + isRestricted = + it != UserActionModel.ENTER_GUEST_MODE && + it != UserActionModel.NAVIGATE_TO_USER_MANAGEMENT && + !settings.isAddUsersFromLockscreen, ) - } + - actionModels.map { - toRecord( - action = it, - selectedUserId = selectedUserInfo.id, - isRestricted = - it != UserActionModel.ENTER_GUEST_MODE && - it != UserActionModel.NAVIGATE_TO_USER_MANAGEMENT && - !settings.isAddUsersFromLockscreen, - ) - } - ) - } - .onEach { notifyCallbacks() } - .stateIn( - scope = applicationScope, - started = SharingStarted.Eagerly, - initialValue = ArrayList(), + } ) - } else { - MutableStateFlow(ArrayList()) - } + } + .onEach { notifyCallbacks() } + .stateIn( + scope = applicationScope, + started = SharingStarted.Eagerly, + initialValue = ArrayList(), + ) val selectedUserRecord: StateFlow<UserRecord?> = - if (isNewImpl) { - repository.selectedUserInfo - .map { selectedUserInfo -> - toRecord(userInfo = selectedUserInfo, selectedUserId = selectedUserInfo.id) - } - .stateIn( - scope = applicationScope, - started = SharingStarted.Eagerly, - initialValue = null, - ) - } else { - MutableStateFlow(null) - } + repository.selectedUserInfo + .map { selectedUserInfo -> + toRecord(userInfo = selectedUserInfo, selectedUserId = selectedUserInfo.id) + } + .stateIn( + scope = applicationScope, + started = SharingStarted.Eagerly, + initialValue = null, + ) /** Whether the device is configured to always have a guest user available. */ val isGuestUserAutoCreated: Boolean = guestUserInteractor.isGuestUserAutoCreated @@ -311,44 +266,37 @@ constructor( val dialogDismissRequests: Flow<Unit?> = _dialogDismissRequests.asStateFlow() val isSimpleUserSwitcher: Boolean - get() = - if (isNewImpl) { - repository.isSimpleUserSwitcher() - } else { - error("Not supported in the old implementation!") - } + get() = repository.isSimpleUserSwitcher() init { - if (isNewImpl) { - refreshUsersScheduler.refreshIfNotPaused() - telephonyInteractor.callState - .distinctUntilChanged() - .onEach { refreshUsersScheduler.refreshIfNotPaused() } - .launchIn(applicationScope) - - combine( - broadcastDispatcher.broadcastFlow( - filter = - IntentFilter().apply { - addAction(Intent.ACTION_USER_ADDED) - addAction(Intent.ACTION_USER_REMOVED) - addAction(Intent.ACTION_USER_INFO_CHANGED) - addAction(Intent.ACTION_USER_SWITCHED) - addAction(Intent.ACTION_USER_STOPPED) - addAction(Intent.ACTION_USER_UNLOCKED) - }, - user = UserHandle.SYSTEM, - map = { intent, _ -> intent }, - ), - repository.selectedUserInfo.pairwise(null), - ) { intent, selectedUserChange -> - Pair(intent, selectedUserChange.previousValue) - } - .onEach { (intent, previousSelectedUser) -> - onBroadcastReceived(intent, previousSelectedUser) - } - .launchIn(applicationScope) - } + refreshUsersScheduler.refreshIfNotPaused() + telephonyInteractor.callState + .distinctUntilChanged() + .onEach { refreshUsersScheduler.refreshIfNotPaused() } + .launchIn(applicationScope) + + combine( + broadcastDispatcher.broadcastFlow( + filter = + IntentFilter().apply { + addAction(Intent.ACTION_USER_ADDED) + addAction(Intent.ACTION_USER_REMOVED) + addAction(Intent.ACTION_USER_INFO_CHANGED) + addAction(Intent.ACTION_USER_SWITCHED) + addAction(Intent.ACTION_USER_STOPPED) + addAction(Intent.ACTION_USER_UNLOCKED) + }, + user = UserHandle.SYSTEM, + map = { intent, _ -> intent }, + ), + repository.selectedUserInfo.pairwise(null), + ) { intent, selectedUserChange -> + Pair(intent, selectedUserChange.previousValue) + } + .onEach { (intent, previousSelectedUser) -> + onBroadcastReceived(intent, previousSelectedUser) + } + .launchIn(applicationScope) } fun addCallback(callback: UserCallback) { @@ -414,48 +362,43 @@ constructor( newlySelectedUserId: Int, dialogShower: UserSwitchDialogController.DialogShower? = null, ) { - if (isNewImpl) { - val currentlySelectedUserInfo = repository.getSelectedUserInfo() - if ( - newlySelectedUserId == currentlySelectedUserInfo.id && - currentlySelectedUserInfo.isGuest - ) { - // Here when clicking on the currently-selected guest user to leave guest mode - // and return to the previously-selected non-guest user. - showDialog( - ShowDialogRequestModel.ShowExitGuestDialog( - guestUserId = currentlySelectedUserInfo.id, - targetUserId = repository.lastSelectedNonGuestUserId, - isGuestEphemeral = currentlySelectedUserInfo.isEphemeral, - isKeyguardShowing = keyguardInteractor.isKeyguardShowing(), - onExitGuestUser = this::exitGuestUser, - dialogShower = dialogShower, - ) + val currentlySelectedUserInfo = repository.getSelectedUserInfo() + if ( + newlySelectedUserId == currentlySelectedUserInfo.id && currentlySelectedUserInfo.isGuest + ) { + // Here when clicking on the currently-selected guest user to leave guest mode + // and return to the previously-selected non-guest user. + showDialog( + ShowDialogRequestModel.ShowExitGuestDialog( + guestUserId = currentlySelectedUserInfo.id, + targetUserId = repository.lastSelectedNonGuestUserId, + isGuestEphemeral = currentlySelectedUserInfo.isEphemeral, + isKeyguardShowing = keyguardInteractor.isKeyguardShowing(), + onExitGuestUser = this::exitGuestUser, + dialogShower = dialogShower, ) - return - } + ) + return + } - if (currentlySelectedUserInfo.isGuest) { - // Here when switching from guest to a non-guest user. - showDialog( - ShowDialogRequestModel.ShowExitGuestDialog( - guestUserId = currentlySelectedUserInfo.id, - targetUserId = newlySelectedUserId, - isGuestEphemeral = currentlySelectedUserInfo.isEphemeral, - isKeyguardShowing = keyguardInteractor.isKeyguardShowing(), - onExitGuestUser = this::exitGuestUser, - dialogShower = dialogShower, - ) + if (currentlySelectedUserInfo.isGuest) { + // Here when switching from guest to a non-guest user. + showDialog( + ShowDialogRequestModel.ShowExitGuestDialog( + guestUserId = currentlySelectedUserInfo.id, + targetUserId = newlySelectedUserId, + isGuestEphemeral = currentlySelectedUserInfo.isEphemeral, + isKeyguardShowing = keyguardInteractor.isKeyguardShowing(), + onExitGuestUser = this::exitGuestUser, + dialogShower = dialogShower, ) - return - } + ) + return + } - dialogShower?.dismiss() + dialogShower?.dismiss() - switchUser(newlySelectedUserId) - } else { - controller.onUserSelected(newlySelectedUserId, dialogShower) - } + switchUser(newlySelectedUserId) } /** Executes the given action. */ @@ -463,51 +406,38 @@ constructor( action: UserActionModel, dialogShower: UserSwitchDialogController.DialogShower? = null, ) { - if (isNewImpl) { - when (action) { - UserActionModel.ENTER_GUEST_MODE -> - guestUserInteractor.createAndSwitchTo( - this::showDialog, - this::dismissDialog, - ) { userId -> - selectUser(userId, dialogShower) - } - UserActionModel.ADD_USER -> { - val currentUser = repository.getSelectedUserInfo() - showDialog( - ShowDialogRequestModel.ShowAddUserDialog( - userHandle = currentUser.userHandle, - isKeyguardShowing = keyguardInteractor.isKeyguardShowing(), - showEphemeralMessage = currentUser.isGuest && currentUser.isEphemeral, - dialogShower = dialogShower, - ) - ) + when (action) { + UserActionModel.ENTER_GUEST_MODE -> + guestUserInteractor.createAndSwitchTo( + this::showDialog, + this::dismissDialog, + ) { userId -> + selectUser(userId, dialogShower) } - UserActionModel.ADD_SUPERVISED_USER -> - activityStarter.startActivity( - Intent() - .setAction(UserManager.ACTION_CREATE_SUPERVISED_USER) - .setPackage(supervisedUserPackageName) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), - /* dismissShade= */ true, - ) - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT -> - activityStarter.startActivity( - Intent(Settings.ACTION_USER_SETTINGS), - /* dismissShade= */ true, - ) - } - } else { - when (action) { - UserActionModel.ENTER_GUEST_MODE -> controller.createAndSwitchToGuestUser(null) - UserActionModel.ADD_USER -> controller.showAddUserDialog(null) - UserActionModel.ADD_SUPERVISED_USER -> controller.startSupervisedUserActivity() - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT -> - activityStarter.startActivity( - Intent(Settings.ACTION_USER_SETTINGS), - /* dismissShade= */ false, + UserActionModel.ADD_USER -> { + val currentUser = repository.getSelectedUserInfo() + showDialog( + ShowDialogRequestModel.ShowAddUserDialog( + userHandle = currentUser.userHandle, + isKeyguardShowing = keyguardInteractor.isKeyguardShowing(), + showEphemeralMessage = currentUser.isGuest && currentUser.isEphemeral, + dialogShower = dialogShower, ) + ) } + UserActionModel.ADD_SUPERVISED_USER -> + activityStarter.startActivity( + Intent() + .setAction(UserManager.ACTION_CREATE_SUPERVISED_USER) + .setPackage(supervisedUserPackageName) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), + /* dismissShade= */ true, + ) + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT -> + activityStarter.startActivity( + Intent(Settings.ACTION_USER_SETTINGS), + /* dismissShade= */ true, + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt b/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt index ad09ee3c10d9..e13710786fbb 100644 --- a/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt @@ -133,7 +133,9 @@ object UserSwitcherViewBinder { launch { viewModel.users.collect { users -> val viewPool = - view.children.filter { it.tag == USER_VIEW_TAG }.toMutableList() + gridContainerView.children + .filter { it.tag == USER_VIEW_TAG } + .toMutableList() viewPool.forEach { gridContainerView.removeView(it) flowWidget.removeView(it) diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt index e9217209530b..58a4473186b3 100644 --- a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt @@ -27,15 +27,12 @@ import com.android.systemui.animation.DialogLaunchAnimator import com.android.systemui.broadcast.BroadcastSender import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.plugins.FalsingManager import com.android.systemui.user.domain.interactor.UserInteractor import com.android.systemui.user.domain.model.ShowDialogRequestModel import dagger.Lazy import javax.inject.Inject import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.launch @@ -50,16 +47,11 @@ constructor( private val broadcastSender: Lazy<BroadcastSender>, private val dialogLaunchAnimator: Lazy<DialogLaunchAnimator>, private val interactor: Lazy<UserInteractor>, - private val featureFlags: Lazy<FeatureFlags>, ) : CoreStartable { private var currentDialog: Dialog? = null override fun start() { - if (featureFlags.get().isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)) { - return - } - startHandlingDialogShowRequests() startHandlingDialogDismissRequests() } diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt index d857e85bac53..0910ea36b7ff 100644 --- a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt @@ -20,8 +20,6 @@ package com.android.systemui.user.ui.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.android.systemui.common.ui.drawable.CircularDrawable -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.user.domain.interactor.GuestUserInteractor import com.android.systemui.user.domain.interactor.UserInteractor @@ -41,12 +39,8 @@ private constructor( private val userInteractor: UserInteractor, private val guestUserInteractor: GuestUserInteractor, private val powerInteractor: PowerInteractor, - private val featureFlags: FeatureFlags, ) : ViewModel() { - private val isNewImpl: Boolean - get() = !featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER) - /** On-device users. */ val users: Flow<List<UserViewModel>> = userInteractor.users.map { models -> models.map { user -> toViewModel(user) } } @@ -216,7 +210,6 @@ private constructor( private val userInteractor: UserInteractor, private val guestUserInteractor: GuestUserInteractor, private val powerInteractor: PowerInteractor, - private val featureFlags: FeatureFlags, ) : ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>): T { @Suppress("UNCHECKED_CAST") @@ -224,7 +217,6 @@ private constructor( userInteractor = userInteractor, guestUserInteractor = guestUserInteractor, powerInteractor = powerInteractor, - featureFlags = featureFlags, ) as T } diff --git a/packages/SystemUI/src/com/android/systemui/util/time/DateFormatUtil.java b/packages/SystemUI/src/com/android/systemui/util/time/DateFormatUtil.java index d7c4e93bb26e..3c570810b318 100644 --- a/packages/SystemUI/src/com/android/systemui/util/time/DateFormatUtil.java +++ b/packages/SystemUI/src/com/android/systemui/util/time/DateFormatUtil.java @@ -16,10 +16,11 @@ package com.android.systemui.util.time; -import android.app.ActivityManager; import android.content.Context; import android.text.format.DateFormat; +import com.android.systemui.settings.UserTracker; + import javax.inject.Inject; /** @@ -27,14 +28,16 @@ import javax.inject.Inject; */ public class DateFormatUtil { private final Context mContext; + private final UserTracker mUserTracker; @Inject - public DateFormatUtil(Context context) { + public DateFormatUtil(Context context, UserTracker userTracker) { mContext = context; + mUserTracker = userTracker; } /** Returns true if the phone is in 24 hour format. */ public boolean is24HourFormat() { - return DateFormat.is24HourFormat(mContext, ActivityManager.getCurrentUser()); + return DateFormat.is24HourFormat(mContext, mUserTracker.getUserId()); } } diff --git a/packages/SystemUI/tests/AndroidManifest.xml b/packages/SystemUI/tests/AndroidManifest.xml index f92a1baec627..48913390c23d 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/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt index 52b6b38ca8ef..e8f8e25364b3 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt @@ -155,7 +155,8 @@ class ClockEventControllerTest : SysuiTestCase() { verify(configurationController).addCallback(capture(captor)) captor.value.onDensityOrFontScaleChanged() - verify(events).onFontSettingChanged() + verify(smallClockEvents, times(2)).onFontSettingChanged(anyFloat()) + verify(largeClockEvents, times(2)).onFontSettingChanged(anyFloat()) } @Test diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java index aa4469f12161..4d58b09f1076 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 @@ public class KeyguardSecurityContainerControllerTest extends SysuiTestCase { 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 1d2b09ce0ce2..36ed669e299c 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 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase { } @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 9296d3d5ec82..fd02ac97cec2 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 @@ public class KeyguardSecurityViewFlipperControllerTest extends SysuiTestCase { } } } + + @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 6d043c595eb5..5514fd0f9311 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java @@ -113,6 +113,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dump.DumpManager; import com.android.systemui.log.SessionTracker; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.telephony.TelephonyListenerManager; @@ -157,6 +158,8 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { private static final int FINGERPRINT_SENSOR_ID = 1; @Mock + private UserTracker mUserTracker; + @Mock private DumpManager mDumpManager; @Mock private KeyguardUpdateMonitor.StrongAuthTracker mStrongAuthTracker; @@ -306,8 +309,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { ExtendedMockito.doReturn(SubscriptionManager.INVALID_SUBSCRIPTION_ID) .when(SubscriptionManager::getDefaultSubscriptionId); KeyguardUpdateMonitor.setCurrentUser(mCurrentUserId); - ExtendedMockito.doReturn(KeyguardUpdateMonitor.getCurrentUser()) - .when(ActivityManager::getCurrentUser); + when(mUserTracker.getUserId()).thenReturn(mCurrentUserId); ExtendedMockito.doReturn(mActivityService).when(ActivityManager::getService); mFaceWakeUpTriggersConfig = new FaceWakeUpTriggersConfig( @@ -1032,6 +1034,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Test public void testSecondaryLockscreenRequirement() { KeyguardUpdateMonitor.setCurrentUser(UserHandle.myUserId()); + when(mUserTracker.getUserId()).thenReturn(UserHandle.myUserId()); int user = KeyguardUpdateMonitor.getCurrentUser(); String packageName = "fake.test.package"; String cls = "FakeService"; @@ -1313,7 +1316,10 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { 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 @@ -1883,7 +1889,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { AtomicBoolean mSimStateChanged = new AtomicBoolean(false); protected TestableKeyguardUpdateMonitor(Context context) { - super(context, + super(context, mUserTracker, TestableLooper.get(KeyguardUpdateMonitorTest.this).getLooper(), mBroadcastDispatcher, mSecureSettings, mDumpManager, mBackgroundExecutor, mMainExecutor, diff --git a/packages/SystemUI/tests/src/com/android/keyguard/clock/ClockManagerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/clock/ClockManagerTest.java index ff4412e97c04..27701be66761 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/clock/ClockManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/clock/ClockManagerTest.java @@ -17,6 +17,7 @@ package com.android.keyguard.clock; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; @@ -30,15 +31,15 @@ import android.testing.AndroidTestingRunner; import android.testing.TestableLooper.RunWithLooper; import android.view.LayoutInflater; -import androidx.lifecycle.MutableLiveData; - import com.android.systemui.SysuiTestCase; import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.dock.DockManager; import com.android.systemui.dock.DockManagerFake; import com.android.systemui.plugins.ClockPlugin; -import com.android.systemui.settings.CurrentUserObservable; +import com.android.systemui.settings.UserTracker; import com.android.systemui.shared.plugins.PluginManager; +import com.android.systemui.util.concurrency.FakeExecutor; +import com.android.systemui.util.time.FakeSystemClock; import org.junit.After; import org.junit.Before; @@ -52,8 +53,7 @@ import java.util.Arrays; @SmallTest @RunWith(AndroidTestingRunner.class) -// Need to run tests on main looper because LiveData operations such as setData, observe, -// removeObserver cannot be invoked on a background thread. +// Need to run tests on main looper to allow for onClockChanged operation to happen synchronously. @RunWithLooper(setAsMainLooper = true) public final class ClockManagerTest extends SysuiTestCase { @@ -63,14 +63,16 @@ public final class ClockManagerTest extends SysuiTestCase { private static final int SECONDARY_USER_ID = 11; private static final Uri SETTINGS_URI = null; + private final FakeSystemClock mFakeSystemClock = new FakeSystemClock(); + private final FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock); private ClockManager mClockManager; private ContentObserver mContentObserver; private DockManagerFake mFakeDockManager; - private MutableLiveData<Integer> mCurrentUser; + private ArgumentCaptor<UserTracker.Callback> mUserTrackerCallbackCaptor; @Mock PluginManager mMockPluginManager; @Mock SysuiColorExtractor mMockColorExtractor; @Mock ContentResolver mMockContentResolver; - @Mock CurrentUserObservable mMockCurrentUserObserable; + @Mock UserTracker mUserTracker; @Mock SettingsWrapper mMockSettingsWrapper; @Mock ClockManager.ClockChangedListener mMockListener1; @Mock ClockManager.ClockChangedListener mMockListener2; @@ -83,18 +85,18 @@ public final class ClockManagerTest extends SysuiTestCase { mFakeDockManager = new DockManagerFake(); - mCurrentUser = new MutableLiveData<>(); - mCurrentUser.setValue(MAIN_USER_ID); - when(mMockCurrentUserObserable.getCurrentUser()).thenReturn(mCurrentUser); + when(mUserTracker.getUserId()).thenReturn(MAIN_USER_ID); + mUserTrackerCallbackCaptor = ArgumentCaptor.forClass(UserTracker.Callback.class); mClockManager = new ClockManager(getContext(), inflater, mMockPluginManager, mMockColorExtractor, mMockContentResolver, - mMockCurrentUserObserable, mMockSettingsWrapper, mFakeDockManager); + mUserTracker, mMainExecutor, mMockSettingsWrapper, mFakeDockManager); mClockManager.addBuiltinClock(() -> new BubbleClockController( getContext().getResources(), inflater, mMockColorExtractor)); mClockManager.addOnClockChangedListener(mMockListener1); mClockManager.addOnClockChangedListener(mMockListener2); + verify(mUserTracker).addCallback(mUserTrackerCallbackCaptor.capture(), any()); reset(mMockListener1, mMockListener2); mContentObserver = mClockManager.getContentObserver(); @@ -221,7 +223,7 @@ public final class ClockManagerTest extends SysuiTestCase { @Test public void onUserChanged_defaultClock() { // WHEN the user changes - mCurrentUser.setValue(SECONDARY_USER_ID); + switchUser(SECONDARY_USER_ID); // THEN the plugin is null for the default clock face assertThat(mClockManager.getCurrentClock()).isNull(); } @@ -232,7 +234,7 @@ public final class ClockManagerTest extends SysuiTestCase { when(mMockSettingsWrapper.getLockScreenCustomClockFace(SECONDARY_USER_ID)).thenReturn( BUBBLE_CLOCK); // WHEN the user changes - mCurrentUser.setValue(SECONDARY_USER_ID); + switchUser(SECONDARY_USER_ID); // THEN the plugin is the bubble clock face. assertThat(mClockManager.getCurrentClock()).isInstanceOf(BUBBLE_CLOCK_CLASS); } @@ -244,8 +246,13 @@ public final class ClockManagerTest extends SysuiTestCase { // AND the second user as selected the bubble clock for the dock when(mMockSettingsWrapper.getDockedClockFace(SECONDARY_USER_ID)).thenReturn(BUBBLE_CLOCK); // WHEN the user changes - mCurrentUser.setValue(SECONDARY_USER_ID); + switchUser(SECONDARY_USER_ID); // THEN the plugin is the bubble clock face. assertThat(mClockManager.getCurrentClock()).isInstanceOf(BUBBLE_CLOCK_CLASS); } + + private void switchUser(int newUser) { + when(mUserTracker.getUserId()).thenReturn(newUser); + mUserTrackerCallbackCaptor.getValue().onUserChanged(newUser, mContext); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/DisplayCutoutBaseViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/DisplayCutoutBaseViewTest.kt index a4e0825360df..588620646b73 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/DisplayCutoutBaseViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/DisplayCutoutBaseViewTest.kt @@ -16,6 +16,7 @@ package com.android.systemui +import android.content.Context import android.graphics.Canvas import android.graphics.Insets import android.graphics.Path @@ -48,6 +49,7 @@ class DisplayCutoutBaseViewTest : SysuiTestCase() { @Mock private lateinit var mockCanvas: Canvas @Mock private lateinit var mockRootView: View @Mock private lateinit var mockDisplay: Display + @Mock private lateinit var mockContext: Context private lateinit var cutoutBaseView: DisplayCutoutBaseView private val cutout: DisplayCutout = DisplayCutout.Builder() @@ -168,7 +170,9 @@ class DisplayCutoutBaseViewTest : SysuiTestCase() { R.bool.config_fillMainBuiltInDisplayCutout, fillCutout) cutoutBaseView = spy(DisplayCutoutBaseView(mContext)) - whenever(cutoutBaseView.display).thenReturn(mockDisplay) + + whenever(cutoutBaseView.context).thenReturn(mockContext) + whenever(mockContext.display).thenReturn(mockDisplay) whenever(mockDisplay.uniqueId).thenReturn("mockDisplayUniqueId") whenever(cutoutBaseView.rootView).thenReturn(mockRootView) whenever(cutoutBaseView.getPhysicalPixelDisplaySizeRatio()).thenReturn(1f) diff --git a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorHwcLayerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorHwcLayerTest.kt index 054650bb8a75..8207fa6958f3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorHwcLayerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorHwcLayerTest.kt @@ -16,6 +16,7 @@ package com.android.systemui +import android.content.Context import android.graphics.Insets import android.graphics.PixelFormat import android.graphics.Rect @@ -44,6 +45,7 @@ class ScreenDecorHwcLayerTest : SysuiTestCase() { @Mock private lateinit var mockDisplay: Display @Mock private lateinit var mockRootView: View + @Mock private lateinit var mockContext: Context private val displayWidth = 100 private val displayHeight = 200 @@ -75,7 +77,8 @@ class ScreenDecorHwcLayerTest : SysuiTestCase() { decorHwcLayer = Mockito.spy(ScreenDecorHwcLayer(mContext, decorationSupport)) whenever(decorHwcLayer.width).thenReturn(displayWidth) whenever(decorHwcLayer.height).thenReturn(displayHeight) - whenever(decorHwcLayer.display).thenReturn(mockDisplay) + whenever(decorHwcLayer.context).thenReturn(mockContext) + whenever(mockContext.display).thenReturn(mockDisplay) whenever(decorHwcLayer.rootView).thenReturn(mockRootView) whenever(mockRootView.left).thenReturn(0) whenever(mockRootView.top).thenReturn(0) diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java index 69ccc8b88bd8..57ca9c064224 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java @@ -903,6 +903,97 @@ public class WindowMagnificationControllerTest extends SysuiTestCase { } @Test + public void changeMagnificationSize_expectedWindowSize() { + final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds(); + + final float magnificationScaleLarge = 2.5f; + final int initSize = Math.min(bounds.width(), bounds.height()) / 3; + final int magnificationSize = (int) (initSize * magnificationScaleLarge); + + final int expectedWindowHeight = magnificationSize; + final int expectedWindowWidth = magnificationSize; + + mInstrumentation.runOnMainSync( + () -> + mWindowMagnificationController.enableWindowMagnificationInternal( + Float.NaN, Float.NaN, Float.NaN)); + + final AtomicInteger actualWindowHeight = new AtomicInteger(); + final AtomicInteger actualWindowWidth = new AtomicInteger(); + mInstrumentation.runOnMainSync( + () -> { + mWindowMagnificationController.changeMagnificationSize( + WindowMagnificationSettings.MagnificationSize.LARGE); + actualWindowHeight.set(mWindowManager.getLayoutParamsFromAttachedView().height); + actualWindowWidth.set(mWindowManager.getLayoutParamsFromAttachedView().width); + }); + + assertEquals(expectedWindowHeight, actualWindowHeight.get()); + assertEquals(expectedWindowWidth, actualWindowWidth.get()); + } + + @Test + public void editModeOnDragCorner_resizesWindow() { + final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds(); + + final int startingSize = (int) (bounds.width() / 2); + + mInstrumentation.runOnMainSync( + () -> + mWindowMagnificationController.enableWindowMagnificationInternal( + Float.NaN, Float.NaN, Float.NaN)); + + final AtomicInteger actualWindowHeight = new AtomicInteger(); + final AtomicInteger actualWindowWidth = new AtomicInteger(); + + mInstrumentation.runOnMainSync( + () -> { + mWindowMagnificationController.setWindowSize(startingSize, startingSize); + mWindowMagnificationController.setEditMagnifierSizeMode(true); + }); + + waitForIdleSync(); + + mInstrumentation.runOnMainSync( + () -> { + mWindowMagnificationController + .onDrag(getInternalView(R.id.bottom_right_corner), 2f, 1f); + actualWindowHeight.set(mWindowManager.getLayoutParamsFromAttachedView().height); + actualWindowWidth.set(mWindowManager.getLayoutParamsFromAttachedView().width); + }); + + assertEquals(startingSize + 1, actualWindowHeight.get()); + assertEquals(startingSize + 2, actualWindowWidth.get()); + } + + @Test + public void editModeOnDragEdge_resizesWindowInOnlyOneDirection() { + final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds(); + + final int startingSize = (int) (bounds.width() / 2f); + + mInstrumentation.runOnMainSync( + () -> + mWindowMagnificationController.enableWindowMagnificationInternal( + Float.NaN, Float.NaN, Float.NaN)); + + final AtomicInteger actualWindowHeight = new AtomicInteger(); + final AtomicInteger actualWindowWidth = new AtomicInteger(); + + mInstrumentation.runOnMainSync( + () -> { + mWindowMagnificationController.setWindowSize(startingSize, startingSize); + mWindowMagnificationController.setEditMagnifierSizeMode(true); + mWindowMagnificationController + .onDrag(getInternalView(R.id.bottom_handle), 2f, 1f); + actualWindowHeight.set(mWindowManager.getLayoutParamsFromAttachedView().height); + actualWindowWidth.set(mWindowManager.getLayoutParamsFromAttachedView().width); + }); + assertEquals(startingSize + 1, actualWindowHeight.get()); + assertEquals(startingSize, actualWindowWidth.get()); + } + + @Test public void setWindowCenterOutOfScreen_enabled_magnificationCenterIsInsideTheScreen() { final int minimumWindowSize = mResources.getDimensionPixelSize( diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java index d20eeafde09c..2d5188fcb95a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java @@ -19,12 +19,22 @@ package com.android.systemui.accessibility.floatingmenu; import static android.view.View.GONE; import static android.view.View.VISIBLE; +import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_COMPONENT_NAME; import static com.android.systemui.accessibility.floatingmenu.MenuViewLayer.LayerIndex; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.verify; - +import static org.mockito.Mockito.when; + +import android.accessibilityservice.AccessibilityServiceInfo; +import android.content.ComponentName; +import android.content.pm.ApplicationInfo; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.os.Build; +import android.os.UserHandle; +import android.provider.Settings; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.View; @@ -35,6 +45,7 @@ import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; +import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -43,12 +54,23 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import java.util.ArrayList; +import java.util.List; + /** Tests for {@link MenuViewLayer}. */ @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper(setAsMainLooper = true) @SmallTest public class MenuViewLayerTest extends SysuiTestCase { + private static final String SELECT_TO_SPEAK_PACKAGE_NAME = "com.google.android.marvin.talkback"; + private static final String SELECT_TO_SPEAK_SERVICE_NAME = + "com.google.android.accessibility.selecttospeak.SelectToSpeakService"; + private static final ComponentName TEST_SELECT_TO_SPEAK_COMPONENT_NAME = new ComponentName( + SELECT_TO_SPEAK_PACKAGE_NAME, SELECT_TO_SPEAK_SERVICE_NAME); + private MenuViewLayer mMenuViewLayer; + private String mLastAccessibilityButtonTargets; + private String mLastEnabledAccessibilityServices; @Rule public MockitoRule mockito = MockitoJUnit.rule(); @@ -56,13 +78,31 @@ public class MenuViewLayerTest extends SysuiTestCase { @Mock private IAccessibilityFloatingMenu mFloatingMenu; + @Mock + private AccessibilityManager mStubAccessibilityManager; + @Before public void setUp() throws Exception { final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class); - final AccessibilityManager stubAccessibilityManager = mContext.getSystemService( - AccessibilityManager.class); - mMenuViewLayer = new MenuViewLayer(mContext, stubWindowManager, stubAccessibilityManager, + mMenuViewLayer = new MenuViewLayer(mContext, stubWindowManager, mStubAccessibilityManager, mFloatingMenu); + + mLastAccessibilityButtonTargets = + Settings.Secure.getStringForUser(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, UserHandle.USER_CURRENT); + mLastEnabledAccessibilityServices = + Settings.Secure.getStringForUser(mContext.getContentResolver(), + Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, UserHandle.USER_CURRENT); + } + + @After + public void tearDown() throws Exception { + Settings.Secure.putStringForUser(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, mLastAccessibilityButtonTargets, + UserHandle.USER_CURRENT); + Settings.Secure.putStringForUser(mContext.getContentResolver(), + Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, mLastEnabledAccessibilityServices, + UserHandle.USER_CURRENT); } @Test @@ -87,4 +127,45 @@ public class MenuViewLayerTest extends SysuiTestCase { verify(mFloatingMenu).hide(); } + + @Test + public void tiggerDismissMenuAction_matchA11yButtonTargetsResult() { + Settings.Secure.putStringForUser(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, + MAGNIFICATION_COMPONENT_NAME.flattenToString(), UserHandle.USER_CURRENT); + + mMenuViewLayer.mDismissMenuAction.run(); + final String value = + Settings.Secure.getStringForUser(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, UserHandle.USER_CURRENT); + + assertThat(value).isEqualTo(""); + } + + @Test + public void tiggerDismissMenuAction_matchEnabledA11yServicesResult() { + Settings.Secure.putString(mContext.getContentResolver(), + Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, + TEST_SELECT_TO_SPEAK_COMPONENT_NAME.flattenToString()); + final ResolveInfo resolveInfo = new ResolveInfo(); + final ServiceInfo serviceInfo = new ServiceInfo(); + final ApplicationInfo applicationInfo = new ApplicationInfo(); + resolveInfo.serviceInfo = serviceInfo; + serviceInfo.applicationInfo = applicationInfo; + applicationInfo.targetSdkVersion = Build.VERSION_CODES.R; + final AccessibilityServiceInfo accessibilityServiceInfo = new AccessibilityServiceInfo(); + accessibilityServiceInfo.setResolveInfo(resolveInfo); + accessibilityServiceInfo.flags = AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON; + final List<AccessibilityServiceInfo> serviceInfoList = new ArrayList<>(); + accessibilityServiceInfo.setComponentName(TEST_SELECT_TO_SPEAK_COMPONENT_NAME); + serviceInfoList.add(accessibilityServiceInfo); + when(mStubAccessibilityManager.getEnabledAccessibilityServiceList( + AccessibilityServiceInfo.FEEDBACK_ALL_MASK)).thenReturn(serviceInfoList); + + mMenuViewLayer.mDismissMenuAction.run(); + final String value = Settings.Secure.getString(mContext.getContentResolver(), + Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES); + + assertThat(value).isEqualTo(""); + } } 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 000000000000..982f033d05e5 --- /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 1d038a43ec8e..1482f291b57a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java @@ -34,7 +34,9 @@ import android.provider.Settings; import androidx.test.filters.SmallTest; 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.settings.UserTracker; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.tuner.TunerService; @@ -50,15 +52,16 @@ public class BatteryMeterViewControllerTest extends SysuiTestCase { private BatteryMeterView mBatteryMeterView; @Mock + private UserTracker mUserTracker; + @Mock private ConfigurationController mConfigurationController; @Mock private TunerService mTunerService; @Mock - private BroadcastDispatcher mBroadcastDispatcher; - @Mock private Handler mHandler; @Mock private ContentResolver mContentResolver; + private FakeFeatureFlags mFeatureFlags; @Mock private BatteryController mBatteryController; @@ -71,19 +74,13 @@ public class BatteryMeterViewControllerTest extends SysuiTestCase { 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 @@ public class BatteryMeterViewControllerTest extends SysuiTestCase { @Test public void onViewDetached_callbacksUnregistered() { + initController(); // Set everything up first. mController.onViewAttached(); @@ -114,6 +112,7 @@ public class BatteryMeterViewControllerTest extends SysuiTestCase { @Test public void ignoreTunerUpdates_afterOnViewAttached_callbackUnregistered() { + initController(); // Start out receiving tuner updates mController.onViewAttached(); @@ -124,10 +123,43 @@ public class BatteryMeterViewControllerTest extends SysuiTestCase { @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, + mUserTracker, + mConfigurationController, + mTunerService, + 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 b4ff2a5e1fbf..eb7d9c3900f1 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 @@ package com.android.systemui.battery 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 @@ class BatteryMeterViewTest : SysuiTestCase() { // 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 @@ class BatteryMeterViewTest : SysuiTestCase() { 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 000000000000..39cb0476e429 --- /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/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt index 12c2bbfd464b..898f37048eec 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt @@ -125,6 +125,21 @@ class AuthContainerViewTest : SysuiTestCase() { } @Test + fun testCredentialPasswordDismissesOnBack() { + val container = initializeCredentialPasswordContainer(addToView = true) + assertThat(container.parent).isNotNull() + val root = container.rootView + + // Simulate back invocation + container.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK)) + container.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK)) + waitForIdleSync() + + assertThat(container.parent).isNull() + assertThat(root.isAttachedToWindow).isFalse() + } + + @Test fun testIgnoresAnimatedInWhenDismissed() { val container = initializeFingerprintContainer(addToView = false) container.dismissFromSystemServer() @@ -369,20 +384,7 @@ class AuthContainerViewTest : SysuiTestCase() { @Test fun testCredentialUI_disablesClickingOnBackground() { - whenever(userManager.getCredentialOwnerProfile(anyInt())).thenReturn(20) - whenever(lockPatternUtils.getKeyguardStoredPasswordQuality(eq(20))).thenReturn( - DevicePolicyManager.PASSWORD_QUALITY_NUMERIC - ) - - // In the credential view, clicking on the background (to cancel authentication) is not - // valid. Thus, the listener should be null, and it should not be in the accessibility - // hierarchy. - val container = initializeFingerprintContainer( - authenticators = BiometricManager.Authenticators.DEVICE_CREDENTIAL - ) - waitForIdleSync() - - assertThat(container.hasCredentialPasswordView()).isTrue() + val container = initializeCredentialPasswordContainer() assertThat(container.hasBiometricPrompt()).isFalse() assertThat( container.findViewById<View>(R.id.background)?.isImportantForAccessibility @@ -442,6 +444,27 @@ class AuthContainerViewTest : SysuiTestCase() { verify(callback).onTryAgainPressed(authContainer?.requestId ?: 0L) } + private fun initializeCredentialPasswordContainer( + addToView: Boolean = true, + ): TestAuthContainerView { + whenever(userManager.getCredentialOwnerProfile(anyInt())).thenReturn(20) + whenever(lockPatternUtils.getKeyguardStoredPasswordQuality(eq(20))).thenReturn( + DevicePolicyManager.PASSWORD_QUALITY_NUMERIC + ) + + // In the credential view, clicking on the background (to cancel authentication) is not + // valid. Thus, the listener should be null, and it should not be in the accessibility + // hierarchy. + val container = initializeFingerprintContainer( + authenticators = BiometricManager.Authenticators.DEVICE_CREDENTIAL, + addToView = addToView, + ) + waitForIdleSync() + + assertThat(container.hasCredentialPasswordView()).isTrue() + return container + } + private fun initializeFingerprintContainer( authenticators: Int = BiometricManager.Authenticators.BIOMETRIC_WEAK, addToView: Boolean = true diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java index 1b5f9b6d45cd..acdafe3e1c7d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java @@ -114,10 +114,8 @@ public class UdfpsControllerTest extends SysuiTestCase { @Rule public MockitoRule rule = MockitoJUnit.rule(); - // Unit under test private UdfpsController mUdfpsController; - // Dependencies private FakeExecutor mBiometricsExecutor; @Mock @@ -171,7 +169,6 @@ public class UdfpsControllerTest extends SysuiTestCase { private UdfpsDisplayMode mUdfpsDisplayMode; @Mock private FeatureFlags mFeatureFlags; - // Stuff for configuring mocks @Mock private UdfpsView mUdfpsView; @@ -249,54 +246,42 @@ public class UdfpsControllerTest extends SysuiTestCase { FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC, true /* resetLockoutRequiresHardwareAuthToken */); - List<FingerprintSensorPropertiesInternal> props = new ArrayList<>(); - props.add(mOpticalProps); - props.add(mUltrasonicProps); - when(mFingerprintManager.getSensorPropertiesInternal()).thenReturn(props); - mFgExecutor = new FakeExecutor(new FakeSystemClock()); // Create a fake background executor. mBiometricsExecutor = new FakeExecutor(new FakeSystemClock()); - mUdfpsController = new UdfpsController( - mContext, - execution, - mLayoutInflater, - mFingerprintManager, - mWindowManager, - mStatusBarStateController, - mFgExecutor, - new ShadeExpansionStateManager(), - mStatusBarKeyguardViewManager, - mDumpManager, - mKeyguardUpdateMonitor, - mFeatureFlags, - mFalsingManager, - mPowerManager, - mAccessibilityManager, - mLockscreenShadeTransitionController, - mScreenLifecycle, - mVibrator, - mUdfpsHapticsSimulator, - mUdfpsShell, - mKeyguardStateController, - mDisplayManager, - mHandler, - mConfigurationController, - mSystemClock, - mUnlockedScreenOffAnimationController, - mSystemUIDialogManager, - mLatencyTracker, - mActivityLaunchAnimator, - Optional.of(mAlternateTouchProvider), - mBiometricsExecutor, + initUdfpsController(true /* hasAlternateTouchProvider */); + } + + private void initUdfpsController(boolean hasAlternateTouchProvider) { + initUdfpsController(mOpticalProps, hasAlternateTouchProvider); + } + + private void initUdfpsController(FingerprintSensorPropertiesInternal sensorProps, + boolean hasAlternateTouchProvider) { + reset(mFingerprintManager); + reset(mScreenLifecycle); + + final Optional<AlternateUdfpsTouchProvider> alternateTouchProvider = + hasAlternateTouchProvider ? Optional.of(mAlternateTouchProvider) : Optional.empty(); + + mUdfpsController = new UdfpsController(mContext, new FakeExecution(), mLayoutInflater, + mFingerprintManager, mWindowManager, mStatusBarStateController, mFgExecutor, + new ShadeExpansionStateManager(), mStatusBarKeyguardViewManager, mDumpManager, + mKeyguardUpdateMonitor, mFeatureFlags, mFalsingManager, mPowerManager, + mAccessibilityManager, mLockscreenShadeTransitionController, mScreenLifecycle, + mVibrator, mUdfpsHapticsSimulator, mUdfpsShell, mKeyguardStateController, + mDisplayManager, mHandler, mConfigurationController, mSystemClock, + mUnlockedScreenOffAnimationController, mSystemUIDialogManager, mLatencyTracker, + mActivityLaunchAnimator, alternateTouchProvider, mBiometricsExecutor, mPrimaryBouncerInteractor); verify(mFingerprintManager).setUdfpsOverlayController(mOverlayCaptor.capture()); mOverlayController = mOverlayCaptor.getValue(); verify(mScreenLifecycle).addObserver(mScreenObserverCaptor.capture()); mScreenObserver = mScreenObserverCaptor.getValue(); - mUdfpsController.updateOverlayParams(mOpticalProps, new UdfpsOverlayParams()); + + mUdfpsController.updateOverlayParams(sensorProps, new UdfpsOverlayParams()); mUdfpsController.setUdfpsDisplayMode(mUdfpsDisplayMode); } @@ -333,8 +318,7 @@ public class UdfpsControllerTest extends SysuiTestCase { } @Test - public void onActionMoveTouch_whenCanDismissLockScreen_entersDevice() - throws RemoteException { + public void onActionMoveTouch_whenCanDismissLockScreen_entersDevice() throws RemoteException { onActionMoveTouch_whenCanDismissLockScreen_entersDevice(false /* stale */); } @@ -521,8 +505,37 @@ public class UdfpsControllerTest extends SysuiTestCase { new MotionEvent.PointerCoords[]{pc}, 0, 0, 1f, 1f, 0, 0, 0, 0); } + private static class TestParams { + public final FingerprintSensorPropertiesInternal sensorProps; + public final boolean hasAlternateTouchProvider; + + TestParams(FingerprintSensorPropertiesInternal sensorProps, + boolean hasAlternateTouchProvider) { + this.sensorProps = sensorProps; + this.hasAlternateTouchProvider = hasAlternateTouchProvider; + } + } + + private void runWithAllParams(ThrowingConsumer<TestParams> testParamsConsumer) { + for (FingerprintSensorPropertiesInternal sensorProps : List.of(mOpticalProps, + mUltrasonicProps)) { + for (boolean hasAlternateTouchProvider : new boolean[]{false, true}) { + initUdfpsController(sensorProps, hasAlternateTouchProvider); + testParamsConsumer.accept(new TestParams(sensorProps, hasAlternateTouchProvider)); + } + } + } + @Test - public void onTouch_propagatesTouchInNativeOrientationAndResolution() throws RemoteException { + public void onTouch_propagatesTouchInNativeOrientationAndResolution() { + runWithAllParams( + this::onTouch_propagatesTouchInNativeOrientationAndResolutionParameterized); + } + + private void onTouch_propagatesTouchInNativeOrientationAndResolutionParameterized( + TestParams testParams) throws RemoteException { + reset(mUdfpsView); + final Rect sensorBounds = new Rect(1000, 1900, 1080, 1920); // Bottom right corner. final int displayWidth = 1080; final int displayHeight = 1920; @@ -541,13 +554,13 @@ public class UdfpsControllerTest extends SysuiTestCase { when(mUdfpsView.isWithinSensorArea(anyFloat(), anyFloat())).thenReturn(true); // Show the overlay. - mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId, + mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, testParams.sensorProps.sensorId, BiometricOverlayConstants.REASON_ENROLL_ENROLLING, mUdfpsOverlayControllerCallback); mFgExecutor.runAllReady(); verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture()); // Test ROTATION_0 - mUdfpsController.updateOverlayParams(mOpticalProps, + mUdfpsController.updateOverlayParams(testParams.sensorProps, new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight, scaleFactor, Surface.ROTATION_0)); MotionEvent event = obtainMotionEvent(ACTION_DOWN, displayWidth, displayHeight, touchMinor, @@ -559,12 +572,19 @@ public class UdfpsControllerTest extends SysuiTestCase { mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event); mBiometricsExecutor.runAllReady(); event.recycle(); - verify(mAlternateTouchProvider).onPointerDown(eq(TEST_REQUEST_ID), eq(expectedX), - eq(expectedY), eq(expectedMinor), eq(expectedMajor)); + if (testParams.hasAlternateTouchProvider) { + verify(mAlternateTouchProvider).onPointerDown(eq(TEST_REQUEST_ID), eq(expectedX), + eq(expectedY), eq(expectedMinor), eq(expectedMajor)); + } else { + verify(mFingerprintManager).onPointerDown(eq(TEST_REQUEST_ID), + eq(testParams.sensorProps.sensorId), eq(expectedX), eq(expectedY), + eq(expectedMinor), eq(expectedMajor)); + } // Test ROTATION_90 reset(mAlternateTouchProvider); - mUdfpsController.updateOverlayParams(mOpticalProps, + reset(mFingerprintManager); + mUdfpsController.updateOverlayParams(testParams.sensorProps, new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight, scaleFactor, Surface.ROTATION_90)); event = obtainMotionEvent(ACTION_DOWN, displayHeight, 0, touchMinor, touchMajor); @@ -575,12 +595,19 @@ public class UdfpsControllerTest extends SysuiTestCase { mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event); mBiometricsExecutor.runAllReady(); event.recycle(); - verify(mAlternateTouchProvider).onPointerDown(eq(TEST_REQUEST_ID), eq(expectedX), - eq(expectedY), eq(expectedMinor), eq(expectedMajor)); + if (testParams.hasAlternateTouchProvider) { + verify(mAlternateTouchProvider).onPointerDown(eq(TEST_REQUEST_ID), eq(expectedX), + eq(expectedY), eq(expectedMinor), eq(expectedMajor)); + } else { + verify(mFingerprintManager).onPointerDown(eq(TEST_REQUEST_ID), + eq(testParams.sensorProps.sensorId), eq(expectedX), eq(expectedY), + eq(expectedMinor), eq(expectedMajor)); + } // Test ROTATION_270 reset(mAlternateTouchProvider); - mUdfpsController.updateOverlayParams(mOpticalProps, + reset(mFingerprintManager); + mUdfpsController.updateOverlayParams(testParams.sensorProps, new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight, scaleFactor, Surface.ROTATION_270)); event = obtainMotionEvent(ACTION_DOWN, 0, displayWidth, touchMinor, touchMajor); @@ -591,12 +618,19 @@ public class UdfpsControllerTest extends SysuiTestCase { mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event); mBiometricsExecutor.runAllReady(); event.recycle(); - verify(mAlternateTouchProvider).onPointerDown(eq(TEST_REQUEST_ID), eq(expectedX), - eq(expectedY), eq(expectedMinor), eq(expectedMajor)); + if (testParams.hasAlternateTouchProvider) { + verify(mAlternateTouchProvider).onPointerDown(eq(TEST_REQUEST_ID), eq(expectedX), + eq(expectedY), eq(expectedMinor), eq(expectedMajor)); + } else { + verify(mFingerprintManager).onPointerDown(eq(TEST_REQUEST_ID), + eq(testParams.sensorProps.sensorId), eq(expectedX), eq(expectedY), + eq(expectedMinor), eq(expectedMajor)); + } // Test ROTATION_180 reset(mAlternateTouchProvider); - mUdfpsController.updateOverlayParams(mOpticalProps, + reset(mFingerprintManager); + mUdfpsController.updateOverlayParams(testParams.sensorProps, new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight, scaleFactor, Surface.ROTATION_180)); // ROTATION_180 is not supported. It should be treated like ROTATION_0. @@ -608,26 +642,22 @@ public class UdfpsControllerTest extends SysuiTestCase { mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event); mBiometricsExecutor.runAllReady(); event.recycle(); - verify(mAlternateTouchProvider).onPointerDown(eq(TEST_REQUEST_ID), eq(expectedX), - eq(expectedY), eq(expectedMinor), eq(expectedMajor)); - } - - private void runForAllUdfpsTypes( - ThrowingConsumer<FingerprintSensorPropertiesInternal> sensorPropsConsumer) { - for (FingerprintSensorPropertiesInternal sensorProps : List.of(mOpticalProps, - mUltrasonicProps)) { - mUdfpsController.updateOverlayParams(sensorProps, new UdfpsOverlayParams()); - sensorPropsConsumer.accept(sensorProps); + if (testParams.hasAlternateTouchProvider) { + verify(mAlternateTouchProvider).onPointerDown(eq(TEST_REQUEST_ID), eq(expectedX), + eq(expectedY), eq(expectedMinor), eq(expectedMajor)); + } else { + verify(mFingerprintManager).onPointerDown(eq(TEST_REQUEST_ID), + eq(testParams.sensorProps.sensorId), eq(expectedX), eq(expectedY), + eq(expectedMinor), eq(expectedMajor)); } } @Test public void fingerDown() { - runForAllUdfpsTypes(this::fingerDownForSensor); + runWithAllParams(this::fingerDownParameterized); } - private void fingerDownForSensor(FingerprintSensorPropertiesInternal sensorProps) - throws RemoteException { + private void fingerDownParameterized(TestParams testParams) throws RemoteException { reset(mUdfpsView, mAlternateTouchProvider, mFingerprintManager, mLatencyTracker, mKeyguardUpdateMonitor); @@ -637,7 +667,7 @@ public class UdfpsControllerTest extends SysuiTestCase { when(mKeyguardUpdateMonitor.isFingerprintDetectionRunning()).thenReturn(true); // GIVEN that the overlay is showing - mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, sensorProps.sensorId, + mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, testParams.sensorProps.sensorId, BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); mFgExecutor.runAllReady(); verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture()); @@ -655,14 +685,22 @@ public class UdfpsControllerTest extends SysuiTestCase { mFgExecutor.runAllReady(); - // THEN FingerprintManager is notified about onPointerDown - verify(mAlternateTouchProvider).onPointerDown(eq(TEST_REQUEST_ID), eq(0), eq(0), eq(0f), - eq(0f)); - verify(mFingerprintManager, never()).onPointerDown(anyLong(), anyInt(), anyInt(), anyInt(), - anyFloat(), anyFloat()); + // THEN the touch provider is notified about onPointerDown. + if (testParams.hasAlternateTouchProvider) { + verify(mAlternateTouchProvider).onPointerDown(eq(TEST_REQUEST_ID), eq(0), eq(0), eq(0f), + eq(0f)); + verify(mFingerprintManager, never()).onPointerDown(anyLong(), anyInt(), anyInt(), + anyInt(), anyFloat(), anyFloat()); + verify(mKeyguardUpdateMonitor).onUdfpsPointerDown(eq((int) TEST_REQUEST_ID)); + } else { + verify(mFingerprintManager).onPointerDown(eq(TEST_REQUEST_ID), + eq(testParams.sensorProps.sensorId), eq(0), eq(0), eq(0f), eq(0f)); + verify(mAlternateTouchProvider, never()).onPointerDown(anyInt(), anyInt(), anyInt(), + anyFloat(), anyFloat()); + } // AND display configuration begins - if (sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) { + if (testParams.sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) { verify(mLatencyTracker).onActionStart(eq(LatencyTracker.ACTION_UDFPS_ILLUMINATE)); verify(mUdfpsView).configureDisplay(mOnDisplayConfiguredCaptor.capture()); } else { @@ -671,16 +709,27 @@ public class UdfpsControllerTest extends SysuiTestCase { verify(mUdfpsView, never()).configureDisplay(any()); } verify(mLatencyTracker, never()).onActionEnd(eq(LatencyTracker.ACTION_UDFPS_ILLUMINATE)); - verify(mKeyguardUpdateMonitor).onUdfpsPointerDown(eq((int) TEST_REQUEST_ID)); - if (sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) { + if (testParams.sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) { // AND onDisplayConfigured notifies FingerprintManager about onUiReady mOnDisplayConfiguredCaptor.getValue().run(); mBiometricsExecutor.runAllReady(); - InOrder inOrder = inOrder(mAlternateTouchProvider, mLatencyTracker); - inOrder.verify(mAlternateTouchProvider).onUiReady(); - inOrder.verify(mLatencyTracker).onActionEnd(eq(LatencyTracker.ACTION_UDFPS_ILLUMINATE)); + if (testParams.hasAlternateTouchProvider) { + InOrder inOrder = inOrder(mAlternateTouchProvider, mLatencyTracker); + inOrder.verify(mAlternateTouchProvider).onUiReady(); + inOrder.verify(mLatencyTracker).onActionEnd( + eq(LatencyTracker.ACTION_UDFPS_ILLUMINATE)); + verify(mFingerprintManager, never()).onUiReady(anyLong(), anyInt()); + } else { + InOrder inOrder = inOrder(mFingerprintManager, mLatencyTracker); + inOrder.verify(mFingerprintManager).onUiReady(eq(TEST_REQUEST_ID), + eq(testParams.sensorProps.sensorId)); + inOrder.verify(mLatencyTracker).onActionEnd( + eq(LatencyTracker.ACTION_UDFPS_ILLUMINATE)); + verify(mAlternateTouchProvider, never()).onUiReady(); + } } else { + verify(mFingerprintManager, never()).onUiReady(anyLong(), anyInt()); verify(mAlternateTouchProvider, never()).onUiReady(); verify(mLatencyTracker, never()).onActionEnd( eq(LatencyTracker.ACTION_UDFPS_ILLUMINATE)); @@ -689,24 +738,23 @@ public class UdfpsControllerTest extends SysuiTestCase { @Test public void aodInterrupt() { - runForAllUdfpsTypes(this::aodInterruptForSensor); + runWithAllParams(this::aodInterruptParameterized); } - private void aodInterruptForSensor(FingerprintSensorPropertiesInternal sensorProps) - throws RemoteException { + private void aodInterruptParameterized(TestParams testParams) throws RemoteException { mUdfpsController.cancelAodInterrupt(); reset(mUdfpsView, mAlternateTouchProvider, mFingerprintManager, mKeyguardUpdateMonitor); when(mKeyguardUpdateMonitor.isFingerprintDetectionRunning()).thenReturn(true); // GIVEN that the overlay is showing and screen is on and fp is running - mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, sensorProps.sensorId, + mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, testParams.sensorProps.sensorId, BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); mScreenObserver.onScreenTurnedOn(); mFgExecutor.runAllReady(); // WHEN fingerprint is requested because of AOD interrupt mUdfpsController.onAodInterrupt(0, 0, 2f, 3f); mFgExecutor.runAllReady(); - if (sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) { + if (testParams.sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) { // THEN display configuration begins // AND onDisplayConfigured notifies FingerprintManager about onUiReady verify(mUdfpsView).configureDisplay(mOnDisplayConfiguredCaptor.capture()); @@ -715,29 +763,37 @@ public class UdfpsControllerTest extends SysuiTestCase { verify(mUdfpsView, never()).configureDisplay(mOnDisplayConfiguredCaptor.capture()); } mBiometricsExecutor.runAllReady(); - verify(mAlternateTouchProvider).onPointerDown(eq(TEST_REQUEST_ID), - eq(0), eq(0), eq(3f) /* minor */, eq(2f) /* major */); - verify(mFingerprintManager, never()).onPointerDown(anyLong(), anyInt(), anyInt(), anyInt(), - anyFloat(), anyFloat()); - verify(mKeyguardUpdateMonitor).onUdfpsPointerDown(eq((int) TEST_REQUEST_ID)); + + if (testParams.hasAlternateTouchProvider) { + verify(mAlternateTouchProvider).onPointerDown(eq(TEST_REQUEST_ID), eq(0), eq(0), + eq(3f) /* minor */, eq(2f) /* major */); + verify(mFingerprintManager, never()).onPointerDown(anyLong(), anyInt(), anyInt(), + anyInt(), anyFloat(), anyFloat()); + verify(mKeyguardUpdateMonitor).onUdfpsPointerDown(eq((int) TEST_REQUEST_ID)); + } else { + verify(mFingerprintManager).onPointerDown(eq(TEST_REQUEST_ID), + eq(testParams.sensorProps.sensorId), eq(0), eq(0), eq(3f) /* minor */, + eq(2f) /* major */); + verify(mAlternateTouchProvider, never()).onPointerDown(anyLong(), anyInt(), anyInt(), + anyFloat(), anyFloat()); + } } @Test public void cancelAodInterrupt() { - runForAllUdfpsTypes(this::cancelAodInterruptForSensor); + runWithAllParams(this::cancelAodInterruptParameterized); } - private void cancelAodInterruptForSensor(FingerprintSensorPropertiesInternal sensorProps) - throws RemoteException { + private void cancelAodInterruptParameterized(TestParams testParams) throws RemoteException { reset(mUdfpsView); // GIVEN AOD interrupt - mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, sensorProps.sensorId, + mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, testParams.sensorProps.sensorId, BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); mScreenObserver.onScreenTurnedOn(); mFgExecutor.runAllReady(); mUdfpsController.onAodInterrupt(0, 0, 0f, 0f); - if (sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) { + if (testParams.sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) { when(mUdfpsView.isDisplayConfigured()).thenReturn(true); // WHEN it is cancelled mUdfpsController.cancelAodInterrupt(); @@ -754,21 +810,20 @@ public class UdfpsControllerTest extends SysuiTestCase { @Test public void aodInterruptTimeout() { - runForAllUdfpsTypes(this::aodInterruptTimeoutForSensor); + runWithAllParams(this::aodInterruptTimeoutParameterized); } - private void aodInterruptTimeoutForSensor(FingerprintSensorPropertiesInternal sensorProps) - throws RemoteException { + private void aodInterruptTimeoutParameterized(TestParams testParams) throws RemoteException { reset(mUdfpsView); // GIVEN AOD interrupt - mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, sensorProps.sensorId, + mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, testParams.sensorProps.sensorId, BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); mScreenObserver.onScreenTurnedOn(); mFgExecutor.runAllReady(); mUdfpsController.onAodInterrupt(0, 0, 0f, 0f); mFgExecutor.runAllReady(); - if (sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) { + if (testParams.sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) { when(mUdfpsView.isDisplayConfigured()).thenReturn(true); } else { when(mUdfpsView.isDisplayConfigured()).thenReturn(false); @@ -776,7 +831,7 @@ public class UdfpsControllerTest extends SysuiTestCase { // WHEN it times out mFgExecutor.advanceClockToNext(); mFgExecutor.runAllReady(); - if (sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) { + if (testParams.sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) { // THEN the display is unconfigured. verify(mUdfpsView).unconfigureDisplay(); } else { @@ -787,23 +842,23 @@ public class UdfpsControllerTest extends SysuiTestCase { @Test public void aodInterruptCancelTimeoutActionOnFingerUp() { - runForAllUdfpsTypes(this::aodInterruptCancelTimeoutActionOnFingerUpForSensor); + runWithAllParams(this::aodInterruptCancelTimeoutActionOnFingerUpParameterized); } - private void aodInterruptCancelTimeoutActionOnFingerUpForSensor( - FingerprintSensorPropertiesInternal sensorProps) throws RemoteException { + private void aodInterruptCancelTimeoutActionOnFingerUpParameterized(TestParams testParams) + throws RemoteException { reset(mUdfpsView); when(mUdfpsView.isWithinSensorArea(anyFloat(), anyFloat())).thenReturn(true); // GIVEN AOD interrupt - mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, sensorProps.sensorId, + mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, testParams.sensorProps.sensorId, BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); mScreenObserver.onScreenTurnedOn(); mFgExecutor.runAllReady(); mUdfpsController.onAodInterrupt(0, 0, 0f, 0f); mFgExecutor.runAllReady(); - if (sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) { + if (testParams.sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) { // Configure UdfpsView to accept the ACTION_UP event when(mUdfpsView.isDisplayConfigured()).thenReturn(true); } else { @@ -833,7 +888,7 @@ public class UdfpsControllerTest extends SysuiTestCase { moveEvent.recycle(); mFgExecutor.runAllReady(); - if (sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) { + if (testParams.sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) { // Configure UdfpsView to accept the finger up event when(mUdfpsView.isDisplayConfigured()).thenReturn(true); } else { @@ -844,7 +899,7 @@ public class UdfpsControllerTest extends SysuiTestCase { mFgExecutor.advanceClockToNext(); mFgExecutor.runAllReady(); - if (sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) { + if (testParams.sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) { // THEN the display should be unconfigured once. If the timeout action is not // cancelled, the display would be unconfigured twice which would cause two // FP attempts. @@ -856,23 +911,23 @@ public class UdfpsControllerTest extends SysuiTestCase { @Test public void aodInterruptCancelTimeoutActionOnAcquired() { - runForAllUdfpsTypes(this::aodInterruptCancelTimeoutActionOnAcquiredForSensor); + runWithAllParams(this::aodInterruptCancelTimeoutActionOnAcquiredParameterized); } - private void aodInterruptCancelTimeoutActionOnAcquiredForSensor( - FingerprintSensorPropertiesInternal sensorProps) throws RemoteException { + private void aodInterruptCancelTimeoutActionOnAcquiredParameterized(TestParams testParams) + throws RemoteException { reset(mUdfpsView); when(mUdfpsView.isWithinSensorArea(anyFloat(), anyFloat())).thenReturn(true); // GIVEN AOD interrupt - mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, sensorProps.sensorId, + mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, testParams.sensorProps.sensorId, BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); mScreenObserver.onScreenTurnedOn(); mFgExecutor.runAllReady(); mUdfpsController.onAodInterrupt(0, 0, 0f, 0f); mFgExecutor.runAllReady(); - if (sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) { + if (testParams.sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) { // Configure UdfpsView to accept the acquired event when(mUdfpsView.isDisplayConfigured()).thenReturn(true); } else { @@ -880,7 +935,7 @@ public class UdfpsControllerTest extends SysuiTestCase { } // WHEN acquired is received - mOverlayController.onAcquired(sensorProps.sensorId, + mOverlayController.onAcquired(testParams.sensorProps.sensorId, BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_GOOD); // Configure UdfpsView to accept the ACTION_DOWN event @@ -900,7 +955,7 @@ public class UdfpsControllerTest extends SysuiTestCase { moveEvent.recycle(); mFgExecutor.runAllReady(); - if (sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) { + if (testParams.sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) { // Configure UdfpsView to accept the finger up event when(mUdfpsView.isDisplayConfigured()).thenReturn(true); } else { @@ -911,7 +966,7 @@ public class UdfpsControllerTest extends SysuiTestCase { mFgExecutor.advanceClockToNext(); mFgExecutor.runAllReady(); - if (sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) { + if (testParams.sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) { // THEN the display should be unconfigured once. If the timeout action is not // cancelled, the display would be unconfigured twice which would cause two // FP attempts. @@ -923,15 +978,14 @@ public class UdfpsControllerTest extends SysuiTestCase { @Test public void aodInterruptScreenOff() { - runForAllUdfpsTypes(this::aodInterruptScreenOffForSensor); + runWithAllParams(this::aodInterruptScreenOffParameterized); } - private void aodInterruptScreenOffForSensor(FingerprintSensorPropertiesInternal sensorProps) - throws RemoteException { + private void aodInterruptScreenOffParameterized(TestParams testParams) throws RemoteException { reset(mUdfpsView); // GIVEN screen off - mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, sensorProps.sensorId, + mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, testParams.sensorProps.sensorId, BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); mScreenObserver.onScreenTurnedOff(); mFgExecutor.runAllReady(); @@ -945,17 +999,16 @@ public class UdfpsControllerTest extends SysuiTestCase { @Test public void aodInterrupt_fingerprintNotRunning() { - runForAllUdfpsTypes(this::aodInterrupt_fingerprintNotRunningForSensor); + runWithAllParams(this::aodInterrupt_fingerprintNotRunningParameterized); } - private void aodInterrupt_fingerprintNotRunningForSensor( - FingerprintSensorPropertiesInternal sensorProps) throws RemoteException { + private void aodInterrupt_fingerprintNotRunningParameterized(TestParams testParams) + throws RemoteException { reset(mUdfpsView); // GIVEN showing overlay - mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, sensorProps.sensorId, - BiometricOverlayConstants.REASON_AUTH_KEYGUARD, - mUdfpsOverlayControllerCallback); + mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, testParams.sensorProps.sensorId, + BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); mScreenObserver.onScreenTurnedOn(); mFgExecutor.runAllReady(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/charging/WiredChargingRippleControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/charging/WiredChargingRippleControllerTest.kt index 2af055783c22..d1597149ce0b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/charging/WiredChargingRippleControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/charging/WiredChargingRippleControllerTest.kt @@ -24,7 +24,7 @@ import com.android.internal.logging.UiEventLogger import com.android.systemui.SysuiTestCase import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags -import com.android.systemui.ripple.RippleView +import com.android.systemui.surfaceeffects.ripple.RippleView import com.android.systemui.statusbar.commandline.CommandRegistry import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.ConfigurationController diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java index f8579fff488b..0fadc138637a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java @@ -120,6 +120,7 @@ public class BrightLineClassifierTest extends SysuiTestCase { mGestureFinalizedListener = gestureCompleteListenerCaptor.getValue(); mFakeFeatureFlags.set(Flags.FALSING_FOR_LONG_TAPS, true); + mFakeFeatureFlags.set(Flags.MEDIA_FALSING_PENALTY, true); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsEditingActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsEditingActivityTest.kt index 0b72a68005a6..3b6f7d19e93e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsEditingActivityTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsEditingActivityTest.kt @@ -10,10 +10,12 @@ import androidx.test.filters.SmallTest import androidx.test.rule.ActivityTestRule import androidx.test.runner.intercepting.SingleActivityFactory import com.android.systemui.SysuiTestCase -import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.controls.CustomIconCache import com.android.systemui.controls.controller.ControlsControllerImpl import com.android.systemui.controls.ui.ControlsUiController +import com.android.systemui.settings.UserTracker +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.time.FakeSystemClock import java.util.concurrent.CountDownLatch import org.junit.Before import org.junit.Rule @@ -30,9 +32,11 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper class ControlsEditingActivityTest : SysuiTestCase() { + private val uiExecutor = FakeExecutor(FakeSystemClock()) + @Mock lateinit var controller: ControlsControllerImpl - @Mock lateinit var broadcastDispatcher: BroadcastDispatcher + @Mock lateinit var userTracker: UserTracker @Mock lateinit var customIconCache: CustomIconCache @@ -54,8 +58,9 @@ class ControlsEditingActivityTest : SysuiTestCase() { ) { override fun create(intent: Intent?): TestableControlsEditingActivity { return TestableControlsEditingActivity( + uiExecutor, controller, - broadcastDispatcher, + userTracker, customIconCache, uiController, mockDispatcher, @@ -92,13 +97,14 @@ class ControlsEditingActivityTest : SysuiTestCase() { } public class TestableControlsEditingActivity( + private val executor: FakeExecutor, private val controller: ControlsControllerImpl, - private val broadcastDispatcher: BroadcastDispatcher, + private val userTracker: UserTracker, private val customIconCache: CustomIconCache, private val uiController: ControlsUiController, private val mockDispatcher: OnBackInvokedDispatcher, private val latch: CountDownLatch - ) : ControlsEditingActivity(controller, broadcastDispatcher, customIconCache, uiController) { + ) : ControlsEditingActivity(executor, controller, userTracker, customIconCache, uiController) { override fun getOnBackInvokedDispatcher(): OnBackInvokedDispatcher { return mockDispatcher } diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsFavoritingActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsFavoritingActivityTest.kt index 4b0f7e6cd736..0f06de2a0684 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsFavoritingActivityTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsFavoritingActivityTest.kt @@ -9,10 +9,10 @@ import androidx.test.filters.SmallTest import androidx.test.rule.ActivityTestRule import androidx.test.runner.intercepting.SingleActivityFactory import com.android.systemui.SysuiTestCase -import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.controls.controller.ControlsControllerImpl import com.android.systemui.controls.ui.ControlsUiController import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.settings.UserTracker import com.google.common.util.concurrent.MoreExecutors import java.util.concurrent.CountDownLatch import java.util.concurrent.Executor @@ -37,7 +37,7 @@ class ControlsFavoritingActivityTest : SysuiTestCase() { @Mock lateinit var listingController: ControlsListingController - @Mock lateinit var broadcastDispatcher: BroadcastDispatcher + @Mock lateinit var userTracker: UserTracker @Mock lateinit var uiController: ControlsUiController @@ -60,7 +60,7 @@ class ControlsFavoritingActivityTest : SysuiTestCase() { executor, controller, listingController, - broadcastDispatcher, + userTracker, uiController, mockDispatcher, latch @@ -97,7 +97,7 @@ class ControlsFavoritingActivityTest : SysuiTestCase() { executor: Executor, controller: ControlsControllerImpl, listingController: ControlsListingController, - broadcastDispatcher: BroadcastDispatcher, + userTracker: UserTracker, uiController: ControlsUiController, private val mockDispatcher: OnBackInvokedDispatcher, private val latch: CountDownLatch @@ -106,7 +106,7 @@ class ControlsFavoritingActivityTest : SysuiTestCase() { executor, controller, listingController, - broadcastDispatcher, + userTracker, uiController ) { override fun getOnBackInvokedDispatcher(): OnBackInvokedDispatcher { diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsProviderSelectorActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsProviderSelectorActivityTest.kt index acc62227cde6..56c3efe1b8e6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsProviderSelectorActivityTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsProviderSelectorActivityTest.kt @@ -25,11 +25,11 @@ import androidx.test.filters.SmallTest import androidx.test.rule.ActivityTestRule import androidx.test.runner.intercepting.SingleActivityFactory import com.android.systemui.SysuiTestCase -import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.ui.ControlsUiController import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.settings.UserTracker import com.google.common.util.concurrent.MoreExecutors import java.util.concurrent.CountDownLatch import java.util.concurrent.Executor @@ -56,7 +56,7 @@ class ControlsProviderSelectorActivityTest : SysuiTestCase() { @Mock lateinit var controlsController: ControlsController - @Mock lateinit var broadcastDispatcher: BroadcastDispatcher + @Mock lateinit var userTracker: UserTracker @Mock lateinit var uiController: ControlsUiController @@ -80,7 +80,7 @@ class ControlsProviderSelectorActivityTest : SysuiTestCase() { backExecutor, listingController, controlsController, - broadcastDispatcher, + userTracker, uiController, mockDispatcher, latch @@ -118,7 +118,7 @@ class ControlsProviderSelectorActivityTest : SysuiTestCase() { backExecutor: Executor, listingController: ControlsListingController, controlsController: ControlsController, - broadcastDispatcher: BroadcastDispatcher, + userTracker: UserTracker, uiController: ControlsUiController, private val mockDispatcher: OnBackInvokedDispatcher, private val latch: CountDownLatch @@ -128,7 +128,7 @@ class ControlsProviderSelectorActivityTest : SysuiTestCase() { backExecutor, listingController, controlsController, - broadcastDispatcher, + userTracker, uiController ) { override fun getOnBackInvokedDispatcher(): OnBackInvokedDispatcher { diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsRequestDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsRequestDialogTest.kt index efb3db700804..314b17625f00 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsRequestDialogTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsRequestDialogTest.kt @@ -34,6 +34,7 @@ import androidx.test.runner.intercepting.SingleActivityFactory import com.android.systemui.SysuiTestCase import com.android.systemui.controls.controller.ControlInfo import com.android.systemui.controls.controller.ControlsController +import com.android.systemui.settings.UserTracker import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.eq import org.junit.After @@ -46,9 +47,10 @@ import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.Captor import org.mockito.Mock -import org.mockito.Mockito.`when` import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations +import java.util.concurrent.Executor @MediumTest @RunWith(AndroidTestingRunner::class) @@ -67,6 +69,10 @@ class ControlsRequestDialogTest : SysuiTestCase() { private lateinit var controller: ControlsController @Mock + private lateinit var mainExecutor: Executor + @Mock + private lateinit var userTracker: UserTracker + @Mock private lateinit var listingController: ControlsListingController @Mock private lateinit var iIntentSender: IIntentSender @@ -81,8 +87,9 @@ class ControlsRequestDialogTest : SysuiTestCase() { ) { override fun create(intent: Intent?): TestControlsRequestDialog { return TestControlsRequestDialog( + mainExecutor, controller, - fakeBroadcastDispatcher, + userTracker, listingController ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/TestControlsRequestDialog.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/TestControlsRequestDialog.kt index 3f6308b18d03..ec239f64e254 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/TestControlsRequestDialog.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/TestControlsRequestDialog.kt @@ -16,11 +16,13 @@ package com.android.systemui.controls.management -import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.controls.controller.ControlsController +import com.android.systemui.settings.UserTracker +import java.util.concurrent.Executor class TestControlsRequestDialog( + mainExecutor: Executor, controller: ControlsController, - dispatcher: BroadcastDispatcher, + userTracker: UserTracker, listingController: ControlsListingController -) : ControlsRequestDialog(controller, dispatcher, listingController)
\ No newline at end of file +) : ControlsRequestDialog(mainExecutor, controller, userTracker, listingController) diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java index 2f206adc5acf..07d7e79ec256 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java @@ -51,6 +51,7 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.biometrics.AuthController; import com.android.systemui.doze.DozeSensors.TriggerSensor; import com.android.systemui.plugins.SensorManagerPlugin; +import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.statusbar.policy.DevicePostureController; import com.android.systemui.util.sensors.AsyncSensorManager; @@ -99,6 +100,8 @@ public class DozeSensorsTest extends SysuiTestCase { @Mock private DevicePostureController mDevicePostureController; @Mock + private UserTracker mUserTracker; + @Mock private ProximitySensor mProximitySensor; // Capture listeners so that they can be used to send events @@ -428,7 +431,7 @@ public class DozeSensorsTest extends SysuiTestCase { DozeSensors dozeSensors = new DozeSensors(mSensorManager, mDozeParameters, mAmbientDisplayConfiguration, mWakeLock, mCallback, mProxCallback, mDozeLog, mProximitySensor, mFakeSettings, mAuthController, - mDevicePostureController); + mDevicePostureController, mUserTracker); for (TriggerSensor sensor : dozeSensors.mTriggerSensors) { assertFalse(sensor.mIgnoresSetting); @@ -440,7 +443,7 @@ public class DozeSensorsTest extends SysuiTestCase { super(mSensorManager, mDozeParameters, mAmbientDisplayConfiguration, mWakeLock, mCallback, mProxCallback, mDozeLog, mProximitySensor, mFakeSettings, mAuthController, - mDevicePostureController); + mDevicePostureController, mUserTracker); for (TriggerSensor sensor : mTriggerSensors) { if (sensor instanceof PluginSensor && ((PluginSensor) sensor).mPluginSensor.getType() diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java index 6091d3a93f14..82432ce31fa2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java @@ -49,6 +49,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dock.DockManager; import com.android.systemui.doze.DozeTriggers.DozingUpdateUiEvent; import com.android.systemui.log.SessionTracker; +import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.statusbar.policy.DevicePostureController; import com.android.systemui.statusbar.policy.KeyguardStateController; @@ -98,6 +99,8 @@ public class DozeTriggersTest extends SysuiTestCase { @Mock private DevicePostureController mDevicePostureController; @Mock + private UserTracker mUserTracker; + @Mock private SessionTracker mSessionTracker; private DozeTriggers mTriggers; @@ -131,7 +134,7 @@ public class DozeTriggersTest extends SysuiTestCase { asyncSensorManager, wakeLock, mDockManager, mProximitySensor, mProximityCheck, mDozeLog, mBroadcastDispatcher, new FakeSettings(), mAuthController, mUiEventLogger, mSessionTracker, mKeyguardStateController, - mDevicePostureController); + mDevicePostureController, mUserTracker); mTriggers.setDozeMachine(mMachine); waitForSensorManager(); } 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 849ac5ef90d7..7a2ba95f74a0 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 @@ public class ComplicationLayoutEngineTest extends SysuiTestCase { 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 @@ public class ComplicationLayoutEngineTest extends SysuiTestCase { 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 @@ public class ComplicationLayoutEngineTest extends SysuiTestCase { } /** + * 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 cb7e47b28bcd..ce7561e95f1e 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 @@ public class ComplicationLayoutParamsTest extends SysuiTestCase { } /** + * 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 aa8c93edce68..30ad485d7ac3 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 @@ public class DreamHomeControlsComplicationTest extends SysuiTestCase { private ActivityStarter mActivityStarter; @Mock - UiEventLogger mUiEventLogger; + private UiEventLogger mUiEventLogger; + + @Captor + private ArgumentCaptor<DreamOverlayStateController.Callback> mStateCallbackCaptor; @Before public void setup() { @@ -164,6 +167,29 @@ public class DreamHomeControlsComplicationTest extends SysuiTestCase { 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 @@ public class DreamHomeControlsComplicationTest extends SysuiTestCase { 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/globalactions/GlobalActionsDialogLiteTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java index 8b1554c1f66f..d52616bfefcc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java @@ -63,6 +63,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.plugins.GlobalActions; import com.android.systemui.settings.UserContextProvider; +import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.phone.CentralSurfaces; @@ -103,6 +104,7 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase { @Mock private SecureSettings mSecureSettings; @Mock private Resources mResources; @Mock private ConfigurationController mConfigurationController; + @Mock private UserTracker mUserTracker; @Mock private KeyguardStateController mKeyguardStateController; @Mock private UserManager mUserManager; @Mock private TrustManager mTrustManager; @@ -152,6 +154,7 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase { mVibratorHelper, mResources, mConfigurationController, + mUserTracker, mKeyguardStateController, mUserManager, mTrustManager, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java index 23516c94d851..729a1ccd30f9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java @@ -48,6 +48,7 @@ import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.SystemUIInitializerImpl; import com.android.systemui.SysuiTestCase; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.NotificationMediaManager; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.phone.DozeParameters; @@ -93,6 +94,8 @@ public class KeyguardSliceProviderTest extends SysuiTestCase { private NextAlarmController mNextAlarmController; @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor; + @Mock + private UserTracker mUserTracker; private TestableKeyguardSliceProvider mProvider; private boolean mIsZenMode; @@ -105,6 +108,7 @@ public class KeyguardSliceProviderTest extends SysuiTestCase { mProvider.attachInfo(getContext(), null); reset(mContentResolver); SliceProvider.setSpecs(new HashSet<>(Arrays.asList(SliceSpecs.LIST))); + when(mUserTracker.getUserId()).thenReturn(100); } @After @@ -267,6 +271,7 @@ public class KeyguardSliceProviderTest extends SysuiTestCase { mKeyguardBypassController = KeyguardSliceProviderTest.this.mKeyguardBypassController; mMediaManager = KeyguardSliceProviderTest.this.mNotificationMediaManager; mKeyguardUpdateMonitor = KeyguardSliceProviderTest.this.mKeyguardUpdateMonitor; + mUserTracker = KeyguardSliceProviderTest.this.mUserTracker; } @Override diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java index b6780a12e6a9..45aaaa2418a1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java @@ -57,6 +57,7 @@ import com.android.systemui.classifier.FalsingCollectorFake; import com.android.systemui.dreams.DreamOverlayStateController; import com.android.systemui.dump.DumpManager; import com.android.systemui.navigationbar.NavigationModeController; +import com.android.systemui.settings.UserTracker; import com.android.systemui.shade.ShadeController; import com.android.systemui.statusbar.NotificationShadeDepthController; import com.android.systemui.statusbar.NotificationShadeWindowController; @@ -86,6 +87,7 @@ import dagger.Lazy; public class KeyguardViewMediatorTest extends SysuiTestCase { private KeyguardViewMediator mViewMediator; + private @Mock UserTracker mUserTracker; private @Mock DevicePolicyManager mDevicePolicyManager; private @Mock LockPatternUtils mLockPatternUtils; private @Mock KeyguardUpdateMonitor mUpdateMonitor; @@ -286,6 +288,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { private void createAndStartViewMediator() { mViewMediator = new KeyguardViewMediator( mContext, + mUserTracker, mFalsingCollector, mLockPatternUtils, mBroadcastDispatcher, 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 21e506830d88..3269f5a913ae 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 @@ class PrimaryBouncerInteractorTest : SysuiTestCase() { @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/controls/models/player/SeekBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarViewModelTest.kt index 7cd8e749a6e9..56c91bc4525d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarViewModelTest.kt @@ -42,6 +42,7 @@ import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.Mock import org.mockito.Mockito.any +import org.mockito.Mockito.anyInt import org.mockito.Mockito.eq import org.mockito.Mockito.mock import org.mockito.Mockito.never @@ -464,7 +465,7 @@ public class SeekBarViewModelTest : SysuiTestCase() { fun onFalseTapOrTouch() { whenever(mockController.getTransportControls()).thenReturn(mockTransport) whenever(falsingManager.isFalseTouch(Classifier.MEDIA_SEEKBAR)).thenReturn(true) - whenever(falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)).thenReturn(true) + whenever(falsingManager.isFalseTap(anyInt())).thenReturn(true) viewModel.updateController(mockController) val pos = 169 diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataFilterTest.kt index 575b1c6b126e..9d33e6f84972 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataFilterTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataFilterTest.kt @@ -22,13 +22,13 @@ import android.testing.TestableLooper import androidx.test.filters.SmallTest import com.android.internal.logging.InstanceId import com.android.systemui.SysuiTestCase -import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.broadcast.BroadcastSender import com.android.systemui.media.controls.MediaTestUtils import com.android.systemui.media.controls.models.player.MediaData import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData import com.android.systemui.media.controls.ui.MediaPlayerData import com.android.systemui.media.controls.util.MediaUiEventLogger +import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq @@ -64,7 +64,7 @@ private val SMARTSPACE_INSTANCE_ID = InstanceId.fakeInstanceId(456)!! class MediaDataFilterTest : SysuiTestCase() { @Mock private lateinit var listener: MediaDataManager.Listener - @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher + @Mock private lateinit var userTracker: UserTracker @Mock private lateinit var broadcastSender: BroadcastSender @Mock private lateinit var mediaDataManager: MediaDataManager @Mock private lateinit var lockscreenUserManager: NotificationLockscreenUserManager @@ -85,7 +85,7 @@ class MediaDataFilterTest : SysuiTestCase() { mediaDataFilter = MediaDataFilter( context, - broadcastDispatcher, + userTracker, broadcastSender, lockscreenUserManager, executor, diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/ColorSchemeTransitionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/ColorSchemeTransitionTest.kt index a8f413848009..a94374680b91 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/ColorSchemeTransitionTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/ColorSchemeTransitionTest.kt @@ -25,7 +25,8 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.media.controls.models.GutsViewHolder import com.android.systemui.media.controls.models.player.MediaViewHolder import com.android.systemui.monet.ColorScheme -import com.android.systemui.ripple.MultiRippleController +import com.android.systemui.surfaceeffects.ripple.MultiRippleController +import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseController import junit.framework.Assert.assertEquals import org.junit.After import org.junit.Before @@ -62,6 +63,7 @@ class ColorSchemeTransitionTest : SysuiTestCase() { @Mock private lateinit var mediaViewHolder: MediaViewHolder @Mock private lateinit var gutsViewHolder: GutsViewHolder @Mock private lateinit var multiRippleController: MultiRippleController + @Mock private lateinit var turbulenceNoiseController: TurbulenceNoiseController @JvmField @Rule val mockitoRule = MockitoJUnit.rule() @@ -76,6 +78,7 @@ class ColorSchemeTransitionTest : SysuiTestCase() { context, mediaViewHolder, multiRippleController, + turbulenceNoiseController, animatingColorTransitionFactory ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt index 1ad2ca9b9db3..761773b1a345 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt @@ -78,9 +78,10 @@ import com.android.systemui.media.controls.util.MediaUiEventLogger import com.android.systemui.media.dialog.MediaOutputDialogFactory import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager -import com.android.systemui.ripple.MultiRippleView import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.surfaceeffects.ripple.MultiRippleView +import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseView import com.android.systemui.util.animation.TransitionLayout import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.KotlinArgumentCaptor @@ -178,6 +179,7 @@ public class MediaControlPanelTest : SysuiTestCase() { private lateinit var dismiss: FrameLayout private lateinit var dismissText: TextView private lateinit var multiRippleView: MultiRippleView + private lateinit var turbulenceNoiseView: TurbulenceNoiseView private lateinit var session: MediaSession private lateinit var device: MediaDeviceData @@ -210,7 +212,10 @@ public class MediaControlPanelTest : SysuiTestCase() { private lateinit var recSubtitle3: TextView private var shouldShowBroadcastButton: Boolean = false private val fakeFeatureFlag = - FakeFeatureFlags().apply { this.set(Flags.UMO_SURFACE_RIPPLE, false) } + FakeFeatureFlags().apply { + this.set(Flags.UMO_SURFACE_RIPPLE, false) + this.set(Flags.MEDIA_FALSING_PENALTY, true) + } @JvmField @Rule val mockito = MockitoJUnit.rule() @@ -382,6 +387,7 @@ public class MediaControlPanelTest : SysuiTestCase() { } multiRippleView = MultiRippleView(context, null) + turbulenceNoiseView = TurbulenceNoiseView(context, null) whenever(viewHolder.player).thenReturn(view) whenever(viewHolder.appIcon).thenReturn(appIcon) @@ -425,6 +431,7 @@ public class MediaControlPanelTest : SysuiTestCase() { whenever(viewHolder.actionsTopBarrier).thenReturn(actionsTopBarrier) whenever(viewHolder.multiRippleView).thenReturn(multiRippleView) + whenever(viewHolder.turbulenceNoiseView).thenReturn(turbulenceNoiseView) } /** Initialize elements for the recommendation view holder */ 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 68a5f47c5e0b..885cc54af7cb 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 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() { @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 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() { 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 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() { 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 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() { .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/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt index 939af16d81cd..d35a21236ae8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt @@ -4,6 +4,7 @@ import android.app.ActivityManager.RecentTaskInfo import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.settings.UserTracker import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever @@ -11,11 +12,11 @@ import com.android.wm.shell.recents.RecentTasks import com.android.wm.shell.util.GroupedRecentTaskInfo import com.google.common.truth.Truth.assertThat import java.util.* +import java.util.function.Consumer import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import org.junit.Test import org.junit.runner.RunWith -import java.util.function.Consumer @RunWith(AndroidTestingRunner::class) @SmallTest @@ -23,8 +24,14 @@ class ShellRecentTaskListProviderTest : SysuiTestCase() { private val dispatcher = Dispatchers.Unconfined private val recentTasks: RecentTasks = mock() + private val userTracker: UserTracker = mock() private val recentTaskListProvider = - ShellRecentTaskListProvider(dispatcher, Runnable::run, Optional.of(recentTasks)) + ShellRecentTaskListProvider( + dispatcher, + Runnable::run, + Optional.of(recentTasks), + userTracker + ) @Test fun loadRecentTasks_oneTask_returnsTheSameTask() { 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 cd7a949443c9..72e022ed7bbb 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 @@ public class QSFragmentTest extends SysuiBaseFragmentTest { 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/qs/TileLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java index 5abc0e1f9c89..35c8cc70953d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java @@ -27,6 +27,8 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import android.content.Context; +import android.content.res.Resources; import android.test.suitebuilder.annotation.SmallTest; import android.view.accessibility.AccessibilityNodeInfo; @@ -42,16 +44,22 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; @SmallTest @RunWith(AndroidJUnit4.class) public class TileLayoutTest extends SysuiTestCase { - private TileLayout mTileLayout; + private Resources mResources; private int mLayoutSizeForOneTile; + private TileLayout mTileLayout; // under test @Before public void setUp() throws Exception { - mTileLayout = new TileLayout(mContext); + Context context = Mockito.spy(mContext); + mResources = Mockito.spy(context.getResources()); + Mockito.when(mContext.getResources()).thenReturn(mResources); + + mTileLayout = new TileLayout(context); // Layout needs to leave space for the tile margins. Three times the margin size is // sufficient for any number of columns. mLayoutSizeForOneTile = @@ -203,4 +211,21 @@ public class TileLayoutTest extends SysuiTestCase { verify(tileRecord1.tileView).setPosition(0); verify(tileRecord2.tileView).setPosition(1); } + + @Test + public void resourcesChanged_updateResources_returnsTrue() { + Mockito.when(mResources.getInteger(R.integer.quick_settings_num_columns)).thenReturn(1); + mTileLayout.updateResources(); // setup with 1 + Mockito.when(mResources.getInteger(R.integer.quick_settings_num_columns)).thenReturn(2); + + assertEquals(true, mTileLayout.updateResources()); + } + + @Test + public void resourcesSame_updateResources_returnsFalse() { + Mockito.when(mResources.getInteger(R.integer.quick_settings_num_columns)).thenReturn(1); + mTileLayout.updateResources(); // setup with 1 + + assertEquals(false, mTileLayout.updateResources()); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/AirplaneModeTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/AirplaneModeTileTest.kt index 73a0cbc5054d..030c59faa696 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/AirplaneModeTileTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/AirplaneModeTileTest.kt @@ -34,6 +34,7 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.qs.QSHost import com.android.systemui.qs.logging.QSLogger import com.android.systemui.qs.tileimpl.QSTileImpl +import com.android.systemui.settings.UserTracker import com.android.systemui.util.settings.GlobalSettings import com.google.common.truth.Truth.assertThat import dagger.Lazy @@ -64,6 +65,8 @@ class AirplaneModeTileTest : SysuiTestCase() { private lateinit var mConnectivityManager: Lazy<ConnectivityManager> @Mock private lateinit var mGlobalSettings: GlobalSettings + @Mock + private lateinit var mUserTracker: UserTracker private lateinit var mTestableLooper: TestableLooper private lateinit var mTile: AirplaneModeTile @@ -87,7 +90,8 @@ class AirplaneModeTileTest : SysuiTestCase() { mQsLogger, mBroadcastDispatcher, mConnectivityManager, - mGlobalSettings) + mGlobalSettings, + mUserTracker) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt index 3131f60893c7..08a90b79089e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt @@ -42,27 +42,21 @@ import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock -import org.mockito.Mockito.`when` import org.mockito.Mockito.mock import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @SmallTest class UserDetailViewAdapterTest : SysuiTestCase() { - @Mock - private lateinit var mUserSwitcherController: UserSwitcherController - @Mock - private lateinit var mParent: ViewGroup - @Mock - private lateinit var mUserDetailItemView: UserDetailItemView - @Mock - private lateinit var mOtherView: View - @Mock - private lateinit var mInflatedUserDetailItemView: UserDetailItemView - @Mock - private lateinit var mLayoutInflater: LayoutInflater + @Mock private lateinit var mUserSwitcherController: UserSwitcherController + @Mock private lateinit var mParent: ViewGroup + @Mock private lateinit var mUserDetailItemView: UserDetailItemView + @Mock private lateinit var mOtherView: View + @Mock private lateinit var mInflatedUserDetailItemView: UserDetailItemView + @Mock private lateinit var mLayoutInflater: LayoutInflater private var falsingManagerFake: FalsingManagerFake = FalsingManagerFake() private lateinit var adapter: UserDetailView.Adapter private lateinit var uiEventLogger: UiEventLoggerFake @@ -77,10 +71,13 @@ class UserDetailViewAdapterTest : SysuiTestCase() { `when`(mLayoutInflater.inflate(anyInt(), any(ViewGroup::class.java), anyBoolean())) .thenReturn(mInflatedUserDetailItemView) `when`(mParent.context).thenReturn(mContext) - adapter = UserDetailView.Adapter( - mContext, mUserSwitcherController, uiEventLogger, - falsingManagerFake - ) + adapter = + UserDetailView.Adapter( + mContext, + mUserSwitcherController, + uiEventLogger, + falsingManagerFake + ) mPicture = UserIcons.convertToBitmap(mContext.getDrawable(R.drawable.ic_avatar_user)) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogControllerTest.java index 2ef731236851..48a53bc659b3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogControllerTest.java @@ -60,8 +60,8 @@ import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.broadcast.BroadcastDispatcher; -import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.flags.UnreleasedFlag; +import com.android.systemui.flags.FakeFeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.statusbar.connectivity.AccessPointController; import com.android.systemui.statusbar.policy.KeyguardStateController; @@ -169,8 +169,8 @@ public class InternetDialogControllerTest extends SysuiTestCase { private WifiStateWorker mWifiStateWorker; @Mock private SignalStrength mSignalStrength; - @Mock - private FeatureFlags mFlags; + + private FakeFeatureFlags mFlags = new FakeFeatureFlags(); private TestableResources mTestableResources; private InternetDialogController mInternetDialogController; @@ -221,6 +221,7 @@ public class InternetDialogControllerTest extends SysuiTestCase { mInternetDialogController.onAccessPointsChanged(mAccessPoints); mInternetDialogController.mActivityStarter = mActivityStarter; mInternetDialogController.mWifiIconInjector = mWifiIconInjector; + mFlags.set(Flags.QS_SECONDARY_DATA_SUB_INFO, false); } @After @@ -410,7 +411,7 @@ public class InternetDialogControllerTest extends SysuiTestCase { @Test public void getSubtitleText_withNoService_returnNoNetworksAvailable() { - when(mFlags.isEnabled(any(UnreleasedFlag.class))).thenReturn(true); + mFlags.set(Flags.QS_SECONDARY_DATA_SUB_INFO, true); InternetDialogController spyController = spy(mInternetDialogController); fakeAirplaneModeEnabled(false); when(mWifiStateWorker.isWifiEnabled()).thenReturn(true); @@ -767,7 +768,7 @@ public class InternetDialogControllerTest extends SysuiTestCase { @Test public void getSignalStrengthIcon_differentSubId() { - when(mFlags.isEnabled(any(UnreleasedFlag.class))).thenReturn(true); + mFlags.set(Flags.QS_SECONDARY_DATA_SUB_INFO, true); InternetDialogController spyController = spy(mInternetDialogController); Drawable icons = spyController.getSignalStrengthIcon(SUB_ID, mContext, 1, 1, 0, false); Drawable icons2 = spyController.getSignalStrengthIcon(SUB_ID2, mContext, 1, 1, 0, false); @@ -777,7 +778,7 @@ public class InternetDialogControllerTest extends SysuiTestCase { @Test public void getActiveAutoSwitchNonDdsSubId() { - when(mFlags.isEnabled(any(UnreleasedFlag.class))).thenReturn(true); + mFlags.set(Flags.QS_SECONDARY_DATA_SUB_INFO, true); // active on non-DDS SubscriptionInfo info = mock(SubscriptionInfo.class); doReturn(SUB_ID2).when(info).getSubscriptionId(); @@ -813,7 +814,7 @@ public class InternetDialogControllerTest extends SysuiTestCase { @Test public void getMobileNetworkSummary() { - when(mFlags.isEnabled(any(UnreleasedFlag.class))).thenReturn(true); + mFlags.set(Flags.QS_SECONDARY_DATA_SUB_INFO, true); InternetDialogController spyController = spy(mInternetDialogController); doReturn(SUB_ID2).when(spyController).getActiveAutoSwitchNonDdsSubId(); doReturn(true).when(spyController).isMobileDataEnabled(); @@ -837,7 +838,7 @@ public class InternetDialogControllerTest extends SysuiTestCase { @Test public void launchMobileNetworkSettings_validSubId() { - when(mFlags.isEnabled(any(UnreleasedFlag.class))).thenReturn(true); + mFlags.set(Flags.QS_SECONDARY_DATA_SUB_INFO, true); InternetDialogController spyController = spy(mInternetDialogController); doReturn(SUB_ID2).when(spyController).getActiveAutoSwitchNonDdsSubId(); spyController.launchMobileNetworkSettings(mDialogLaunchView); @@ -848,7 +849,7 @@ public class InternetDialogControllerTest extends SysuiTestCase { @Test public void launchMobileNetworkSettings_invalidSubId() { - when(mFlags.isEnabled(any(UnreleasedFlag.class))).thenReturn(true); + mFlags.set(Flags.QS_SECONDARY_DATA_SUB_INFO, true); InternetDialogController spyController = spy(mInternetDialogController); doReturn(SubscriptionManager.INVALID_SUBSCRIPTION_ID) .when(spyController).getActiveAutoSwitchNonDdsSubId(); @@ -860,7 +861,7 @@ public class InternetDialogControllerTest extends SysuiTestCase { @Test public void setAutoDataSwitchMobileDataPolicy() { - when(mFlags.isEnabled(any(UnreleasedFlag.class))).thenReturn(true); + mFlags.set(Flags.QS_SECONDARY_DATA_SUB_INFO, true); mInternetDialogController.setAutoDataSwitchMobileDataPolicy(SUB_ID, true); verify(mTelephonyManager).setMobileDataPolicyEnabled(eq( diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/CurrentUserTrackerTest.java b/packages/SystemUI/tests/src/com/android/systemui/settings/CurrentUserTrackerTest.java deleted file mode 100644 index 1b515c6bbdbd..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/settings/CurrentUserTrackerTest.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (C) 2017 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.settings; - -import android.content.Intent; - -import androidx.test.filters.SmallTest; - -import com.android.systemui.SysuiTestCase; -import com.android.systemui.broadcast.BroadcastDispatcher; - -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -/** - * Testing functionality of the current user tracker - */ -@SmallTest -public class CurrentUserTrackerTest extends SysuiTestCase { - - private CurrentUserTracker mTracker; - private CurrentUserTracker.UserReceiver mReceiver; - @Mock - private BroadcastDispatcher mBroadcastDispatcher; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - mReceiver = new CurrentUserTracker.UserReceiver(mBroadcastDispatcher); - mTracker = new CurrentUserTracker(mReceiver) { - @Override - public void onUserSwitched(int newUserId) { - stopTracking(); - } - }; - } - - @Test - public void testBroadCastDoesntCrashOnConcurrentModification() { - mTracker.startTracking(); - CurrentUserTracker secondTracker = new CurrentUserTracker(mReceiver) { - @Override - public void onUserSwitched(int newUserId) { - stopTracking(); - } - }; - secondTracker.startTracking(); - triggerUserSwitch(); - } - /** - * Simulates a user switch event. - */ - private void triggerUserSwitch() { - Intent intent = new Intent(Intent.ACTION_USER_SWITCHED); - intent.putExtra(Intent.EXTRA_USER_HANDLE, 1); - mReceiver.onReceive(getContext(), intent); - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt index 1130bda9b881..9d1802a686fa 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt @@ -28,9 +28,10 @@ import androidx.test.rule.ActivityTestRule import androidx.test.runner.intercepting.SingleActivityFactory import com.android.systemui.R import com.android.systemui.SysuiTestCase -import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.settings.UserTracker import com.android.systemui.util.mockito.any import com.google.common.truth.Truth.assertThat +import java.util.concurrent.Executor import org.junit.After import org.junit.Before import org.junit.Rule @@ -45,7 +46,9 @@ import org.mockito.MockitoAnnotations @TestableLooper.RunWithLooper class BrightnessDialogTest : SysuiTestCase() { + @Mock private lateinit var userTracker: UserTracker @Mock private lateinit var brightnessSliderControllerFactory: BrightnessSliderController.Factory + @Mock private lateinit var mainExecutor: Executor @Mock private lateinit var backgroundHandler: Handler @Mock private lateinit var brightnessSliderController: BrightnessSliderController @@ -56,8 +59,9 @@ class BrightnessDialogTest : SysuiTestCase() { object : SingleActivityFactory<TestDialog>(TestDialog::class.java) { override fun create(intent: Intent?): TestDialog { return TestDialog( - fakeBroadcastDispatcher, + userTracker, brightnessSliderControllerFactory, + mainExecutor, backgroundHandler ) } @@ -100,8 +104,15 @@ class BrightnessDialogTest : SysuiTestCase() { } class TestDialog( - broadcastDispatcher: BroadcastDispatcher, + userTracker: UserTracker, brightnessSliderControllerFactory: BrightnessSliderController.Factory, + mainExecutor: Executor, backgroundHandler: Handler - ) : BrightnessDialog(broadcastDispatcher, brightnessSliderControllerFactory, backgroundHandler) + ) : + BrightnessDialog( + userTracker, + brightnessSliderControllerFactory, + mainExecutor, + backgroundHandler + ) } 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 0ce9056dc1d1..bc17c19df8f5 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 androidx.test.filters.SmallTest 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,64 @@ class CombinedShadeHeaderConstraintsTest : SysuiTestCase() { 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) + } + } + + @Test + fun testEmptyCutoutDateIconsAreConstrainedWidth() { + CombinedShadeHeadersConstraintManagerImpl.emptyCutoutConstraints()() + + assertThat(qqsConstraint.getConstraint(R.id.date).layout.constrainedWidth).isTrue() + assertThat(qqsConstraint.getConstraint(R.id.statusIcons).layout.constrainedWidth).isTrue() + } + + @Test + fun testCenterCutoutDateIconsAreConstrainedWidth() { + CombinedShadeHeadersConstraintManagerImpl.centerCutoutConstraints(false, 10)() + + assertThat(qqsConstraint.getConstraint(R.id.date).layout.constrainedWidth).isTrue() + assertThat(qqsConstraint.getConstraint(R.id.statusIcons).layout.constrainedWidth).isTrue() + } + private operator fun ConstraintsChanges.invoke() { qqsConstraintsChanges?.invoke(qqsConstraint) qsConstraintsChanges?.invoke(qsConstraint) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt index 539a54b731ec..f5bed79b5e6f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt @@ -139,12 +139,19 @@ class DefaultClockProviderTest : SysuiTestCase() { } @Test - fun defaultClock_events_onFontSettingChanged() { + fun defaultSmallClock_events_onFontSettingChanged() { val clock = provider.createClock(DEFAULT_CLOCK_ID) - clock.events.onFontSettingChanged() + clock.smallClock.events.onFontSettingChanged(100f) - verify(mockSmallClockView).setTextSize(eq(TypedValue.COMPLEX_UNIT_PX), anyFloat()) - verify(mockLargeClockView).setTextSize(eq(TypedValue.COMPLEX_UNIT_PX), anyFloat()) + verify(mockSmallClockView).setTextSize(eq(TypedValue.COMPLEX_UNIT_PX), eq(100f)) + } + + @Test + fun defaultLargeClock_events_onFontSettingChanged() { + val clock = provider.createClock(DEFAULT_CLOCK_ID) + clock.largeClock.events.onFontSettingChanged(200f) + + verify(mockLargeClockView).setTextSize(eq(TypedValue.COMPLEX_UNIT_PX), eq(200f)) verify(mockLargeClockView).setLayoutParams(any()) } 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 77c690a924c3..e8a7ec82aab1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java @@ -87,6 +87,7 @@ import com.android.internal.app.IBatteryStats; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; +import com.android.keyguard.logging.KeyguardLogger; import com.android.settingslib.fuelgauge.BatteryStatus; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; @@ -271,7 +272,7 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { mUserManager, mExecutor, mExecutor, mFalsingManager, mAuthController, mLockPatternUtils, mScreenLifecycle, mKeyguardBypassController, mAccessibilityManager, - mFaceHelpMessageDeferral); + mFaceHelpMessageDeferral, mock(KeyguardLogger.class)); mController.init(); mController.setIndicationArea(mIndicationArea); verify(mStatusBarStateController).addCallback(mStatusBarStateListenerCaptor.capture()); @@ -1067,7 +1068,7 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { // 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 +1092,7 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { // 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 +1109,7 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { 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 +1126,7 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { 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/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java index bdafa4893c9e..15a687d2adc7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java @@ -53,6 +53,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dump.DumpManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.recents.OverviewProxyService; +import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.NotificationLockscreenUserManager.NotificationStateChangedListener; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; @@ -78,6 +79,8 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { private NotificationPresenter mPresenter; @Mock private UserManager mUserManager; + @Mock + private UserTracker mUserTracker; // Dependency mocks: @Mock @@ -115,6 +118,7 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { MockitoAnnotations.initMocks(this); int currentUserId = ActivityManager.getCurrentUser(); + when(mUserTracker.getUserId()).thenReturn(currentUserId); mSettings = new FakeSettings(); mSettings.setUserId(ActivityManager.getCurrentUser()); mCurrentUser = new UserInfo(currentUserId, "", 0); @@ -344,6 +348,7 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { mBroadcastDispatcher, mDevicePolicyManager, mUserManager, + mUserTracker, (() -> mVisibilityProvider), (() -> mNotifCollection), mClickNotifier, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java index 9c870b5aa363..faf4592d26e3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java @@ -71,6 +71,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.demomode.DemoModeController; import com.android.systemui.dump.DumpManager; import com.android.systemui.plugins.log.LogBuffer; +import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener; @@ -116,6 +117,7 @@ public class NetworkControllerBaseTest extends SysuiTestCase { protected TelephonyManager mMockTm; protected TelephonyListenerManager mTelephonyListenerManager; protected BroadcastDispatcher mMockBd; + protected UserTracker mUserTracker; protected Config mConfig; protected CallbackHandler mCallbackHandler; protected SubscriptionDefaults mMockSubDefaults; @@ -172,6 +174,7 @@ public class NetworkControllerBaseTest extends SysuiTestCase { mMockSm = mock(SubscriptionManager.class); mMockCm = mock(ConnectivityManager.class); mMockBd = mock(BroadcastDispatcher.class); + mUserTracker = mock(UserTracker.class); mMockNsm = mock(NetworkScoreManager.class); mMockSubDefaults = mock(SubscriptionDefaults.class); mCarrierConfigTracker = mock(CarrierConfigTracker.class); @@ -246,6 +249,7 @@ public class NetworkControllerBaseTest extends SysuiTestCase { mMockSubDefaults, mMockProvisionController, mMockBd, + mUserTracker, mDemoModeController, mCarrierConfigTracker, mWifiStatusTrackerFactory, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java index 1d112262765e..ca75a40300cb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java @@ -154,6 +154,7 @@ public class NetworkControllerDataTest extends NetworkControllerBaseTest { mMockSubDefaults, mock(DeviceProvisionedController.class), mMockBd, + mUserTracker, mDemoModeController, mock(CarrierConfigTracker.class), mWifiStatusTrackerFactory, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java index d5f5105036d3..84c242cda459 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java @@ -82,6 +82,7 @@ public class NetworkControllerSignalTest extends NetworkControllerBaseTest { mMockSubDefaults, mMockProvisionController, mMockBd, + mUserTracker, mDemoModeController, mCarrierConfigTracker, mWifiStatusTrackerFactory, @@ -118,6 +119,7 @@ public class NetworkControllerSignalTest extends NetworkControllerBaseTest { mMockSubDefaults, mMockProvisionController, mMockBd, + mUserTracker, mDemoModeController, mCarrierConfigTracker, mWifiStatusTrackerFactory, @@ -152,6 +154,7 @@ public class NetworkControllerSignalTest extends NetworkControllerBaseTest { mMockSubDefaults, mock(DeviceProvisionedController.class), mMockBd, + mUserTracker, mDemoModeController, mock(CarrierConfigTracker.class), mWifiStatusTrackerFactory, @@ -189,6 +192,7 @@ public class NetworkControllerSignalTest extends NetworkControllerBaseTest { mMockSubDefaults, mock(DeviceProvisionedController.class), mMockBd, + mUserTracker, mDemoModeController, mock(CarrierConfigTracker.class), mWifiStatusTrackerFactory, @@ -274,6 +278,7 @@ public class NetworkControllerSignalTest extends NetworkControllerBaseTest { mMockSubDefaults, mock(DeviceProvisionedController.class), mMockBd, + mUserTracker, mDemoModeController, mock(CarrierConfigTracker.class), mWifiStatusTrackerFactory, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java index 808abc8e9de5..de71e2c250c4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java @@ -16,7 +16,6 @@ package com.android.systemui.statusbar.phone; -import static com.android.systemui.statusbar.phone.ScrimController.KEYGUARD_SCRIM_ALPHA; import static com.android.systemui.statusbar.phone.ScrimController.OPAQUE; import static com.android.systemui.statusbar.phone.ScrimController.SEMI_TRANSPARENT; import static com.android.systemui.statusbar.phone.ScrimController.TRANSPARENT; @@ -59,7 +58,6 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.ShadeInterpolation; import com.android.systemui.dock.DockManager; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; -import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.scrim.ScrimView; import com.android.systemui.statusbar.policy.FakeConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; @@ -119,7 +117,6 @@ public class ScrimControllerTest extends SysuiTestCase { // TODO(b/204991468): Use a real PanelExpansionStateManager object once this bug is fixed. (The // event-dispatch-on-registration pattern caused some of these unit tests to fail.) @Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; - @Mock private KeyguardViewMediator mKeyguardViewMediator; private static class AnimatorListener implements Animator.AnimatorListener { private int mNumStarts; @@ -233,8 +230,7 @@ public class ScrimControllerTest extends SysuiTestCase { mDockManager, mConfigurationController, new FakeExecutor(new FakeSystemClock()), mScreenOffAnimationController, mKeyguardUnlockAnimationController, - mStatusBarKeyguardViewManager, - mKeyguardViewMediator); + mStatusBarKeyguardViewManager); mScrimController.setScrimVisibleListener(visible -> mScrimVisibility = visible); mScrimController.attachViews(mScrimBehind, mNotificationsScrim, mScrimInFront); mScrimController.setAnimatorListener(mAnimatorListener); @@ -243,8 +239,6 @@ public class ScrimControllerTest extends SysuiTestCase { mScrimController.setWallpaperSupportsAmbientMode(false); mScrimController.transitionTo(ScrimState.KEYGUARD); finishAnimationsImmediately(); - - mScrimController.setLaunchingAffordanceWithPreview(false); } @After @@ -858,8 +852,7 @@ public class ScrimControllerTest extends SysuiTestCase { mDockManager, mConfigurationController, new FakeExecutor(new FakeSystemClock()), mScreenOffAnimationController, mKeyguardUnlockAnimationController, - mStatusBarKeyguardViewManager, - mKeyguardViewMediator); + mStatusBarKeyguardViewManager); mScrimController.setScrimVisibleListener(visible -> mScrimVisibility = visible); mScrimController.attachViews(mScrimBehind, mNotificationsScrim, mScrimInFront); mScrimController.setAnimatorListener(mAnimatorListener); @@ -1638,30 +1631,6 @@ public class ScrimControllerTest extends SysuiTestCase { assertScrimAlpha(mScrimBehind, 0); } - @Test - public void keyguardAlpha_whenUnlockedForOcclusion_ifPlayingOcclusionAnimation() { - mScrimController.transitionTo(ScrimState.KEYGUARD); - - when(mKeyguardViewMediator.isOccludeAnimationPlaying()).thenReturn(true); - - mScrimController.transitionTo(ScrimState.UNLOCKED); - finishAnimationsImmediately(); - - assertScrimAlpha(mNotificationsScrim, (int) (KEYGUARD_SCRIM_ALPHA * 255f)); - } - - @Test - public void keyguardAlpha_whenUnlockedForLaunch_ifLaunchingAffordance() { - mScrimController.transitionTo(ScrimState.KEYGUARD); - when(mKeyguardViewMediator.isOccludeAnimationPlaying()).thenReturn(true); - mScrimController.setLaunchingAffordanceWithPreview(true); - - mScrimController.transitionTo(ScrimState.UNLOCKED); - finishAnimationsImmediately(); - - assertScrimAlpha(mNotificationsScrim, (int) (KEYGUARD_SCRIM_ALPHA * 255f)); - } - private void assertAlphaAfterExpansion(ScrimView scrim, float expectedAlpha, float expansion) { mScrimController.setRawPanelExpansionFraction(expansion); finishAnimationsImmediately(); 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 49c3a2128bd7..9f70565749df 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 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { } @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 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { } @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/BaseUserSwitcherAdapterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapterTest.kt index f3046477f4d1..0a3da0b5b029 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapterTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapterTest.kt @@ -237,7 +237,7 @@ class BaseUserSwitcherAdapterTest : SysuiTestCase() { fun refresh() { underTest.refresh() - verify(controller).refreshUsers(UserHandle.USER_NULL) + verify(controller).refreshUsers() } private fun createUserRecord( 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 43d0fe9559b6..1eee08c22187 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 @@ public class BatteryControllerTest extends SysuiTestCase { 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/statusbar/policy/BluetoothControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BluetoothControllerImplTest.java index d0391ac0795c..833cabbaecf4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BluetoothControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BluetoothControllerImplTest.java @@ -43,6 +43,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; import com.android.systemui.SysuiTestCase; import com.android.systemui.bluetooth.BluetoothLogger; import com.android.systemui.dump.DumpManager; +import com.android.systemui.settings.UserTracker; import org.junit.Before; import org.junit.Test; @@ -56,6 +57,7 @@ import java.util.List; @SmallTest public class BluetoothControllerImplTest extends SysuiTestCase { + private UserTracker mUserTracker; private LocalBluetoothManager mMockBluetoothManager; private CachedBluetoothDeviceManager mMockDeviceManager; private LocalBluetoothAdapter mMockAdapter; @@ -70,6 +72,7 @@ public class BluetoothControllerImplTest extends SysuiTestCase { mTestableLooper = TestableLooper.get(this); mMockBluetoothManager = mDependency.injectMockDependency(LocalBluetoothManager.class); mDevices = new ArrayList<>(); + mUserTracker = mock(UserTracker.class); mMockDeviceManager = mock(CachedBluetoothDeviceManager.class); when(mMockDeviceManager.getCachedDevicesCopy()).thenReturn(mDevices); when(mMockBluetoothManager.getCachedDeviceManager()).thenReturn(mMockDeviceManager); @@ -81,6 +84,7 @@ public class BluetoothControllerImplTest extends SysuiTestCase { mMockDumpManager = mock(DumpManager.class); mBluetoothControllerImpl = new BluetoothControllerImpl(mContext, + mUserTracker, mMockDumpManager, mock(BluetoothLogger.class), mTestableLooper.getLooper(), diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HotspotControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HotspotControllerImplTest.java index 26df03f31b9a..dc08aba7d5e5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HotspotControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HotspotControllerImplTest.java @@ -41,6 +41,7 @@ import androidx.test.filters.SmallTest; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.dump.DumpManager; +import com.android.systemui.settings.UserTracker; import org.junit.Before; import org.junit.Test; @@ -61,6 +62,8 @@ import java.util.concurrent.Executor; public class HotspotControllerImplTest extends SysuiTestCase { @Mock + private UserTracker mUserTracker; + @Mock private DumpManager mDumpManager; @Mock private TetheringManager mTetheringManager; @@ -104,7 +107,8 @@ public class HotspotControllerImplTest extends SysuiTestCase { Handler handler = new Handler(mLooper.getLooper()); - mController = new HotspotControllerImpl(mContext, handler, handler, mDumpManager); + mController = new HotspotControllerImpl(mContext, mUserTracker, handler, handler, + mDumpManager); verify(mTetheringManager) .registerTetheringEventCallback(any(), mTetheringCallbackCaptor.capture()); } @@ -191,7 +195,7 @@ public class HotspotControllerImplTest extends SysuiTestCase { Handler handler = new Handler(mLooper.getLooper()); HotspotController controller = - new HotspotControllerImpl(mContext, handler, handler, mDumpManager); + new HotspotControllerImpl(mContext, mUserTracker, handler, handler, mDumpManager); verifyNoMoreInteractions(mTetheringManager); assertFalse(controller.isHotspotSupported()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SecurityControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SecurityControllerTest.java index d44cdb24421a..15235b68c881 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SecurityControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SecurityControllerTest.java @@ -50,6 +50,7 @@ import androidx.test.runner.AndroidJUnit4; import com.android.systemui.SysuiTestCase; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dump.DumpManager; +import com.android.systemui.settings.UserTracker; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.time.FakeSystemClock; @@ -72,10 +73,12 @@ public class SecurityControllerTest extends SysuiTestCase { private final DevicePolicyManager mDevicePolicyManager = mock(DevicePolicyManager.class); private final IKeyChainService.Stub mKeyChainService = mock(IKeyChainService.Stub.class); private final UserManager mUserManager = mock(UserManager.class); + private final UserTracker mUserTracker = mock(UserTracker.class); private final BroadcastDispatcher mBroadcastDispatcher = mock(BroadcastDispatcher.class); private final Handler mHandler = mock(Handler.class); private SecurityControllerImpl mSecurityController; private ConnectivityManager mConnectivityManager = mock(ConnectivityManager.class); + private FakeExecutor mMainExecutor; private FakeExecutor mBgExecutor; private BroadcastReceiver mBroadcastReceiver; @@ -102,11 +105,14 @@ public class SecurityControllerTest extends SysuiTestCase { ArgumentCaptor<BroadcastReceiver> brCaptor = ArgumentCaptor.forClass(BroadcastReceiver.class); + mMainExecutor = new FakeExecutor(new FakeSystemClock()); mBgExecutor = new FakeExecutor(new FakeSystemClock()); mSecurityController = new SecurityControllerImpl( mContext, + mUserTracker, mHandler, mBroadcastDispatcher, + mMainExecutor, mBgExecutor, Mockito.mock(DumpManager.class)); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImplTest.kt deleted file mode 100644 index 169f4fb2715b..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImplTest.kt +++ /dev/null @@ -1,727 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.systemui.statusbar.policy - -import android.app.IActivityManager -import android.app.NotificationManager -import android.app.admin.DevicePolicyManager -import android.content.BroadcastReceiver -import android.content.Context -import android.content.DialogInterface -import android.content.Intent -import android.content.pm.UserInfo -import android.graphics.Bitmap -import android.hardware.face.FaceManager -import android.hardware.fingerprint.FingerprintManager -import android.os.Handler -import android.os.UserHandle -import android.os.UserManager -import android.provider.Settings -import android.testing.AndroidTestingRunner -import android.testing.TestableLooper -import android.view.ThreadedRenderer -import androidx.test.filters.SmallTest -import com.android.internal.jank.InteractionJankMonitor -import com.android.internal.logging.testing.UiEventLoggerFake -import com.android.internal.util.LatencyTracker -import com.android.internal.util.UserIcons -import com.android.systemui.GuestResetOrExitSessionReceiver -import com.android.systemui.GuestResumeSessionReceiver -import com.android.systemui.GuestSessionNotification -import com.android.systemui.R -import com.android.systemui.SysuiTestCase -import com.android.systemui.animation.DialogCuj -import com.android.systemui.animation.DialogLaunchAnimator -import com.android.systemui.broadcast.BroadcastDispatcher -import com.android.systemui.broadcast.BroadcastSender -import com.android.systemui.dump.DumpManager -import com.android.systemui.plugins.ActivityStarter -import com.android.systemui.plugins.FalsingManager -import com.android.systemui.qs.QSUserSwitcherEvent -import com.android.systemui.qs.user.UserSwitchDialogController -import com.android.systemui.settings.UserTracker -import com.android.systemui.shade.NotificationShadeWindowView -import com.android.systemui.telephony.TelephonyListenerManager -import com.android.systemui.user.data.source.UserRecord -import com.android.systemui.user.legacyhelper.data.LegacyUserDataHelper -import com.android.systemui.user.shared.model.UserActionModel -import com.android.systemui.util.concurrency.FakeExecutor -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.argumentCaptor -import com.android.systemui.util.mockito.capture -import com.android.systemui.util.mockito.kotlinArgumentCaptor -import com.android.systemui.util.mockito.nullable -import com.android.systemui.util.settings.GlobalSettings -import com.android.systemui.util.settings.SecureSettings -import com.android.systemui.util.time.FakeSystemClock -import com.google.common.truth.Truth -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.anyInt -import org.mockito.Mock -import org.mockito.Mockito.doNothing -import org.mockito.Mockito.doReturn -import org.mockito.Mockito.eq -import org.mockito.Mockito.mock -import org.mockito.Mockito.never -import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` -import org.mockito.MockitoAnnotations - -@RunWith(AndroidTestingRunner::class) -@TestableLooper.RunWithLooper(setAsMainLooper = true) -@SmallTest -class UserSwitcherControllerOldImplTest : SysuiTestCase() { - @Mock private lateinit var keyguardStateController: KeyguardStateController - @Mock private lateinit var activityManager: IActivityManager - @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController - @Mock private lateinit var devicePolicyManager: DevicePolicyManager - @Mock private lateinit var handler: Handler - @Mock private lateinit var userTracker: UserTracker - @Mock private lateinit var userManager: UserManager - @Mock private lateinit var activityStarter: ActivityStarter - @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher - @Mock private lateinit var broadcastSender: BroadcastSender - @Mock private lateinit var telephonyListenerManager: TelephonyListenerManager - @Mock private lateinit var secureSettings: SecureSettings - @Mock private lateinit var falsingManager: FalsingManager - @Mock private lateinit var dumpManager: DumpManager - @Mock private lateinit var interactionJankMonitor: InteractionJankMonitor - @Mock private lateinit var latencyTracker: LatencyTracker - @Mock private lateinit var dialogShower: UserSwitchDialogController.DialogShower - @Mock private lateinit var notificationShadeWindowView: NotificationShadeWindowView - @Mock private lateinit var threadedRenderer: ThreadedRenderer - @Mock private lateinit var dialogLaunchAnimator: DialogLaunchAnimator - @Mock private lateinit var globalSettings: GlobalSettings - @Mock private lateinit var guestSessionNotification: GuestSessionNotification - @Mock private lateinit var guestResetOrExitSessionReceiver: GuestResetOrExitSessionReceiver - private lateinit var resetSessionDialogFactory: - GuestResumeSessionReceiver.ResetSessionDialog.Factory - private lateinit var guestResumeSessionReceiver: GuestResumeSessionReceiver - private lateinit var testableLooper: TestableLooper - private lateinit var bgExecutor: FakeExecutor - private lateinit var longRunningExecutor: FakeExecutor - private lateinit var uiExecutor: FakeExecutor - private lateinit var uiEventLogger: UiEventLoggerFake - private lateinit var userSwitcherController: UserSwitcherControllerOldImpl - private lateinit var picture: Bitmap - private val ownerId = UserHandle.USER_SYSTEM - private val ownerInfo = UserInfo(ownerId, "Owner", null, - UserInfo.FLAG_ADMIN or UserInfo.FLAG_FULL or UserInfo.FLAG_INITIALIZED or - UserInfo.FLAG_PRIMARY or UserInfo.FLAG_SYSTEM or UserInfo.FLAG_ADMIN, - UserManager.USER_TYPE_FULL_SYSTEM) - private val guestId = 1234 - private val guestInfo = UserInfo(guestId, "Guest", null, - UserInfo.FLAG_FULL or UserInfo.FLAG_GUEST, UserManager.USER_TYPE_FULL_GUEST) - private val secondaryUser = - UserInfo(10, "Secondary", null, 0, UserManager.USER_TYPE_FULL_SECONDARY) - - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - testableLooper = TestableLooper.get(this) - bgExecutor = FakeExecutor(FakeSystemClock()) - longRunningExecutor = FakeExecutor(FakeSystemClock()) - uiExecutor = FakeExecutor(FakeSystemClock()) - uiEventLogger = UiEventLoggerFake() - - mContext.orCreateTestableResources.addOverride( - com.android.internal.R.bool.config_guestUserAutoCreated, false) - - mContext.addMockSystemService(Context.FACE_SERVICE, mock(FaceManager::class.java)) - mContext.addMockSystemService(Context.NOTIFICATION_SERVICE, - mock(NotificationManager::class.java)) - mContext.addMockSystemService(Context.FINGERPRINT_SERVICE, - mock(FingerprintManager::class.java)) - - resetSessionDialogFactory = object : GuestResumeSessionReceiver.ResetSessionDialog.Factory { - override fun create(userId: Int): GuestResumeSessionReceiver.ResetSessionDialog { - return GuestResumeSessionReceiver.ResetSessionDialog( - mContext, - mock(UserSwitcherController::class.java), - uiEventLogger, - userId - ) - } - } - - guestResumeSessionReceiver = GuestResumeSessionReceiver(userTracker, - secureSettings, - broadcastDispatcher, - guestSessionNotification, - resetSessionDialogFactory) - - `when`(userManager.canAddMoreUsers(eq(UserManager.USER_TYPE_FULL_SECONDARY))) - .thenReturn(true) - `when`(notificationShadeWindowView.context).thenReturn(context) - - // Since userSwitcherController involves InteractionJankMonitor. - // Let's fulfill the dependencies. - val mockedContext = mock(Context::class.java) - doReturn(mockedContext).`when`(notificationShadeWindowView).context - doReturn(true).`when`(notificationShadeWindowView).isAttachedToWindow - doNothing().`when`(threadedRenderer).addObserver(any()) - doNothing().`when`(threadedRenderer).removeObserver(any()) - doReturn(threadedRenderer).`when`(notificationShadeWindowView).threadedRenderer - - picture = UserIcons.convertToBitmap(context.getDrawable(R.drawable.ic_avatar_user)) - - // Create defaults for the current user - `when`(userTracker.userId).thenReturn(ownerId) - `when`(userTracker.userInfo).thenReturn(ownerInfo) - - `when`( - globalSettings.getIntForUser( - eq(Settings.Global.ADD_USERS_WHEN_LOCKED), - anyInt(), - eq(UserHandle.USER_SYSTEM) - ) - ).thenReturn(0) - - `when`( - globalSettings.getIntForUser( - eq(Settings.Global.USER_SWITCHER_ENABLED), - anyInt(), - eq(UserHandle.USER_SYSTEM) - ) - ).thenReturn(1) - - setupController() - } - - private fun setupController() { - userSwitcherController = - UserSwitcherControllerOldImpl( - mContext, - activityManager, - userManager, - userTracker, - keyguardStateController, - deviceProvisionedController, - devicePolicyManager, - handler, - activityStarter, - broadcastDispatcher, - broadcastSender, - uiEventLogger, - falsingManager, - telephonyListenerManager, - secureSettings, - globalSettings, - bgExecutor, - longRunningExecutor, - uiExecutor, - interactionJankMonitor, - latencyTracker, - dumpManager, - dialogLaunchAnimator, - guestResumeSessionReceiver, - guestResetOrExitSessionReceiver - ) - userSwitcherController.init(notificationShadeWindowView) - } - - @Test - fun testSwitchUser_parentDialogDismissed() { - val otherUserRecord = UserRecord( - secondaryUser, - picture, - false /* guest */, - false /* current */, - false /* isAddUser */, - false /* isRestricted */, - true /* isSwitchToEnabled */, - false /* isAddSupervisedUser */ - ) - `when`(userTracker.userId).thenReturn(ownerId) - `when`(userTracker.userInfo).thenReturn(ownerInfo) - - userSwitcherController.onUserListItemClicked(otherUserRecord, dialogShower) - testableLooper.processAllMessages() - - verify(dialogShower).dismiss() - } - - @Test - fun testAddGuest_okButtonPressed() { - val emptyGuestUserRecord = - UserRecord( - null, - null, - true /* guest */, - false /* current */, - false /* isAddUser */, - false /* isRestricted */, - true /* isSwitchToEnabled */, - false /* isAddSupervisedUser */ - ) - `when`(userTracker.userId).thenReturn(ownerId) - `when`(userTracker.userInfo).thenReturn(ownerInfo) - - `when`(userManager.createGuest(any())).thenReturn(guestInfo) - - userSwitcherController.onUserListItemClicked(emptyGuestUserRecord, null) - bgExecutor.runAllReady() - uiExecutor.runAllReady() - testableLooper.processAllMessages() - verify(interactionJankMonitor).begin(any()) - verify(latencyTracker).onActionStart(LatencyTracker.ACTION_USER_SWITCH) - verify(activityManager).switchUser(guestInfo.id) - assertEquals(1, uiEventLogger.numLogs()) - assertEquals(QSUserSwitcherEvent.QS_USER_GUEST_ADD.id, uiEventLogger.eventId(0)) - } - - @Test - fun testAddGuest_parentDialogDismissed() { - val emptyGuestUserRecord = - UserRecord( - null, - null, - true /* guest */, - false /* current */, - false /* isAddUser */, - false /* isRestricted */, - true /* isSwitchToEnabled */, - false /* isAddSupervisedUser */ - ) - `when`(userTracker.userId).thenReturn(ownerId) - `when`(userTracker.userInfo).thenReturn(ownerInfo) - - `when`(userManager.createGuest(any())).thenReturn(guestInfo) - - userSwitcherController.onUserListItemClicked(emptyGuestUserRecord, dialogShower) - bgExecutor.runAllReady() - uiExecutor.runAllReady() - testableLooper.processAllMessages() - verify(dialogShower).dismiss() - } - - @Test - fun testRemoveGuest_removeButtonPressed_isLogged() { - val currentGuestUserRecord = - UserRecord( - guestInfo, - picture, - true /* guest */, - true /* current */, - false /* isAddUser */, - false /* isRestricted */, - true /* isSwitchToEnabled */, - false /* isAddSupervisedUser */ - ) - `when`(userTracker.userId).thenReturn(guestInfo.id) - `when`(userTracker.userInfo).thenReturn(guestInfo) - - userSwitcherController.onUserListItemClicked(currentGuestUserRecord, null) - assertNotNull(userSwitcherController.mExitGuestDialog) - userSwitcherController.mExitGuestDialog - .getButton(DialogInterface.BUTTON_POSITIVE).performClick() - testableLooper.processAllMessages() - assertEquals(1, uiEventLogger.numLogs()) - assertTrue( - QSUserSwitcherEvent.QS_USER_GUEST_REMOVE.id == uiEventLogger.eventId(0) || - QSUserSwitcherEvent.QS_USER_SWITCH.id == uiEventLogger.eventId(0) - ) - } - - @Test - fun testRemoveGuest_removeButtonPressed_dialogDismissed() { - val currentGuestUserRecord = - UserRecord( - guestInfo, - picture, - true /* guest */, - true /* current */, - false /* isAddUser */, - false /* isRestricted */, - true /* isSwitchToEnabled */, - false /* isAddSupervisedUser */ - ) - `when`(userTracker.userId).thenReturn(guestInfo.id) - `when`(userTracker.userInfo).thenReturn(guestInfo) - - userSwitcherController.onUserListItemClicked(currentGuestUserRecord, null) - assertNotNull(userSwitcherController.mExitGuestDialog) - userSwitcherController.mExitGuestDialog - .getButton(DialogInterface.BUTTON_POSITIVE).performClick() - testableLooper.processAllMessages() - assertFalse(userSwitcherController.mExitGuestDialog.isShowing) - } - - @Test - fun testRemoveGuest_dialogShowerUsed() { - val currentGuestUserRecord = - UserRecord( - guestInfo, - picture, - true /* guest */, - true /* current */, - false /* isAddUser */, - false /* isRestricted */, - true /* isSwitchToEnabled */, - false /* isAddSupervisedUser */ - ) - `when`(userTracker.userId).thenReturn(guestInfo.id) - `when`(userTracker.userInfo).thenReturn(guestInfo) - - userSwitcherController.onUserListItemClicked(currentGuestUserRecord, dialogShower) - assertNotNull(userSwitcherController.mExitGuestDialog) - testableLooper.processAllMessages() - verify(dialogShower) - .showDialog( - userSwitcherController.mExitGuestDialog, - DialogCuj(InteractionJankMonitor.CUJ_USER_DIALOG_OPEN, "exit_guest_mode")) - } - - @Test - fun testRemoveGuest_cancelButtonPressed_isNotLogged() { - val currentGuestUserRecord = - UserRecord( - guestInfo, - picture, - true /* guest */, - true /* current */, - false /* isAddUser */, - false /* isRestricted */, - true /* isSwitchToEnabled */, - false /* isAddSupervisedUser */ - ) - `when`(userTracker.userId).thenReturn(guestId) - `when`(userTracker.userInfo).thenReturn(guestInfo) - - userSwitcherController.onUserListItemClicked(currentGuestUserRecord, null) - assertNotNull(userSwitcherController.mExitGuestDialog) - userSwitcherController.mExitGuestDialog - .getButton(DialogInterface.BUTTON_NEUTRAL).performClick() - testableLooper.processAllMessages() - assertEquals(0, uiEventLogger.numLogs()) - } - - @Test - fun testWipeGuest_startOverButtonPressed_isLogged() { - val currentGuestUserRecord = - UserRecord( - guestInfo, - picture, - true /* guest */, - false /* current */, - false /* isAddUser */, - false /* isRestricted */, - true /* isSwitchToEnabled */, - false /* isAddSupervisedUser */ - ) - `when`(userTracker.userId).thenReturn(guestId) - `when`(userTracker.userInfo).thenReturn(guestInfo) - - // Simulate that guest user has already logged in - `when`(secureSettings.getIntForUser( - eq(GuestResumeSessionReceiver.SETTING_GUEST_HAS_LOGGED_IN), anyInt(), anyInt())) - .thenReturn(1) - - userSwitcherController.onUserListItemClicked(currentGuestUserRecord, null) - - // Simulate a user switch event - val intent = Intent(Intent.ACTION_USER_SWITCHED).putExtra(Intent.EXTRA_USER_HANDLE, guestId) - - assertNotNull(userSwitcherController.mGuestResumeSessionReceiver) - userSwitcherController.mGuestResumeSessionReceiver.onReceive(context, intent) - - assertNotNull(userSwitcherController.mGuestResumeSessionReceiver.mNewSessionDialog) - userSwitcherController.mGuestResumeSessionReceiver.mNewSessionDialog - .getButton(GuestResumeSessionReceiver.ResetSessionDialog.BUTTON_WIPE).performClick() - testableLooper.processAllMessages() - assertEquals(1, uiEventLogger.numLogs()) - assertEquals(QSUserSwitcherEvent.QS_USER_GUEST_WIPE.id, uiEventLogger.eventId(0)) - } - - @Test - fun testWipeGuest_continueButtonPressed_isLogged() { - val currentGuestUserRecord = - UserRecord( - guestInfo, - picture, - true /* guest */, - false /* current */, - false /* isAddUser */, - false /* isRestricted */, - true /* isSwitchToEnabled */, - false /* isAddSupervisedUser */ - ) - `when`(userTracker.userId).thenReturn(guestId) - `when`(userTracker.userInfo).thenReturn(guestInfo) - - // Simulate that guest user has already logged in - `when`(secureSettings.getIntForUser( - eq(GuestResumeSessionReceiver.SETTING_GUEST_HAS_LOGGED_IN), anyInt(), anyInt())) - .thenReturn(1) - - userSwitcherController.onUserListItemClicked(currentGuestUserRecord, null) - - // Simulate a user switch event - val intent = Intent(Intent.ACTION_USER_SWITCHED).putExtra(Intent.EXTRA_USER_HANDLE, guestId) - - assertNotNull(userSwitcherController.mGuestResumeSessionReceiver) - userSwitcherController.mGuestResumeSessionReceiver.onReceive(context, intent) - - assertNotNull(userSwitcherController.mGuestResumeSessionReceiver.mNewSessionDialog) - userSwitcherController.mGuestResumeSessionReceiver.mNewSessionDialog - .getButton(GuestResumeSessionReceiver.ResetSessionDialog.BUTTON_DONTWIPE) - .performClick() - testableLooper.processAllMessages() - assertEquals(1, uiEventLogger.numLogs()) - assertEquals(QSUserSwitcherEvent.QS_USER_GUEST_CONTINUE.id, uiEventLogger.eventId(0)) - } - - @Test - fun test_getCurrentUserName_shouldReturnNameOfTheCurrentUser() { - fun addUser(id: Int, name: String, isCurrent: Boolean) { - userSwitcherController.users.add( - UserRecord( - UserInfo(id, name, 0), - null, false, isCurrent, false, - false, false, false - ) - ) - } - val bgUserName = "background_user" - val fgUserName = "foreground_user" - - addUser(1, bgUserName, false) - addUser(2, fgUserName, true) - - assertEquals(fgUserName, userSwitcherController.currentUserName) - } - - @Test - fun isSystemUser_currentUserIsSystemUser_shouldReturnTrue() { - `when`(userTracker.userId).thenReturn(UserHandle.USER_SYSTEM) - assertEquals(true, userSwitcherController.isSystemUser) - } - - @Test - fun isSystemUser_currentUserIsNotSystemUser_shouldReturnFalse() { - `when`(userTracker.userId).thenReturn(1) - assertEquals(false, userSwitcherController.isSystemUser) - } - - @Test - fun testCanCreateSupervisedUserWithConfiguredPackage() { - // GIVEN the supervised user creation package is configured - `when`(context.getString( - com.android.internal.R.string.config_supervisedUserCreationPackage)) - .thenReturn("some_pkg") - - // AND the current user is allowed to create new users - `when`(userTracker.userId).thenReturn(ownerId) - `when`(userTracker.userInfo).thenReturn(ownerInfo) - - // WHEN the controller is started with the above config - setupController() - testableLooper.processAllMessages() - - // THEN a supervised user can be constructed - assertTrue(userSwitcherController.canCreateSupervisedUser()) - } - - @Test - fun testCannotCreateSupervisedUserWithConfiguredPackage() { - // GIVEN the supervised user creation package is NOT configured - `when`(context.getString( - com.android.internal.R.string.config_supervisedUserCreationPackage)) - .thenReturn(null) - - // AND the current user is allowed to create new users - `when`(userTracker.userId).thenReturn(ownerId) - `when`(userTracker.userInfo).thenReturn(ownerInfo) - - // WHEN the controller is started with the above config - setupController() - testableLooper.processAllMessages() - - // THEN a supervised user can NOT be constructed - assertFalse(userSwitcherController.canCreateSupervisedUser()) - } - - @Test - fun testCannotCreateUserWhenUserSwitcherDisabled() { - `when`( - globalSettings.getIntForUser( - eq(Settings.Global.USER_SWITCHER_ENABLED), - anyInt(), - eq(UserHandle.USER_SYSTEM) - ) - ).thenReturn(0) - setupController() - assertFalse(userSwitcherController.canCreateUser()) - } - - @Test - fun testCannotCreateGuestUserWhenUserSwitcherDisabled() { - `when`( - globalSettings.getIntForUser( - eq(Settings.Global.USER_SWITCHER_ENABLED), - anyInt(), - eq(UserHandle.USER_SYSTEM) - ) - ).thenReturn(0) - setupController() - assertFalse(userSwitcherController.canCreateGuest(false)) - } - - @Test - fun testCannotCreateSupervisedUserWhenUserSwitcherDisabled() { - `when`( - globalSettings.getIntForUser( - eq(Settings.Global.USER_SWITCHER_ENABLED), - anyInt(), - eq(UserHandle.USER_SYSTEM) - ) - ).thenReturn(0) - setupController() - assertFalse(userSwitcherController.canCreateSupervisedUser()) - } - - @Test - fun testCanManageUser_userSwitcherEnabled_addUserWhenLocked() { - `when`( - globalSettings.getIntForUser( - eq(Settings.Global.USER_SWITCHER_ENABLED), - anyInt(), - eq(UserHandle.USER_SYSTEM) - ) - ).thenReturn(1) - - `when`( - globalSettings.getIntForUser( - eq(Settings.Global.ADD_USERS_WHEN_LOCKED), - anyInt(), - eq(UserHandle.USER_SYSTEM) - ) - ).thenReturn(1) - setupController() - assertTrue(userSwitcherController.canManageUsers()) - } - - @Test - fun testCanManageUser_userSwitcherDisabled_addUserWhenLocked() { - `when`( - globalSettings.getIntForUser( - eq(Settings.Global.USER_SWITCHER_ENABLED), - anyInt(), - eq(UserHandle.USER_SYSTEM) - ) - ).thenReturn(0) - - `when`( - globalSettings.getIntForUser( - eq(Settings.Global.ADD_USERS_WHEN_LOCKED), - anyInt(), - eq(UserHandle.USER_SYSTEM) - ) - ).thenReturn(1) - setupController() - assertFalse(userSwitcherController.canManageUsers()) - } - - @Test - fun testCanManageUser_userSwitcherEnabled_isAdmin() { - `when`( - globalSettings.getIntForUser( - eq(Settings.Global.USER_SWITCHER_ENABLED), - anyInt(), - eq(UserHandle.USER_SYSTEM) - ) - ).thenReturn(1) - - setupController() - assertTrue(userSwitcherController.canManageUsers()) - } - - @Test - fun testCanManageUser_userSwitcherDisabled_isAdmin() { - `when`( - globalSettings.getIntForUser( - eq(Settings.Global.USER_SWITCHER_ENABLED), - anyInt(), - eq(UserHandle.USER_SYSTEM) - ) - ).thenReturn(0) - - setupController() - assertFalse(userSwitcherController.canManageUsers()) - } - - @Test - fun addUserSwitchCallback() { - val broadcastReceiverCaptor = argumentCaptor<BroadcastReceiver>() - verify(broadcastDispatcher).registerReceiver( - capture(broadcastReceiverCaptor), - any(), - nullable(), nullable(), anyInt(), nullable()) - - val cb = mock(UserSwitcherController.UserSwitchCallback::class.java) - userSwitcherController.addUserSwitchCallback(cb) - - val intent = Intent(Intent.ACTION_USER_SWITCHED).putExtra(Intent.EXTRA_USER_HANDLE, guestId) - broadcastReceiverCaptor.value.onReceive(context, intent) - verify(cb).onUserSwitched() - } - - @Test - fun onUserItemClicked_guest_runsOnBgThread() { - val dialogShower = mock(UserSwitchDialogController.DialogShower::class.java) - val guestUserRecord = UserRecord( - null, - picture, - true /* guest */, - false /* current */, - false /* isAddUser */, - false /* isRestricted */, - true /* isSwitchToEnabled */, - false /* isAddSupervisedUser */ - ) - - userSwitcherController.onUserListItemClicked(guestUserRecord, dialogShower) - assertTrue(bgExecutor.numPending() > 0) - verify(userManager, never()).createGuest(context) - bgExecutor.runAllReady() - verify(userManager).createGuest(context) - } - - @Test - fun onUserItemClicked_manageUsers() { - val manageUserRecord = LegacyUserDataHelper.createRecord( - mContext, - ownerId, - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, - isRestricted = false, - isSwitchToEnabled = true - ) - - userSwitcherController.onUserListItemClicked(manageUserRecord, null) - val intentCaptor = kotlinArgumentCaptor<Intent>() - verify(activityStarter).startActivity(intentCaptor.capture(), - eq(true) - ) - Truth.assertThat(intentCaptor.value.action).isEqualTo(Settings.ACTION_USER_SETTINGS) - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ZenModeControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ZenModeControllerImplTest.java index 3fe1a9f6ed97..c06dbdcf3a1b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ZenModeControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ZenModeControllerImplTest.java @@ -35,6 +35,7 @@ import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dump.DumpManager; +import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.policy.ZenModeController.Callback; import com.android.systemui.util.settings.FakeSettings; @@ -58,6 +59,8 @@ public class ZenModeControllerImplTest extends SysuiTestCase { BroadcastDispatcher mBroadcastDispatcher; @Mock DumpManager mDumpManager; + @Mock + UserTracker mUserTracker; private ZenModeControllerImpl mController; @@ -72,7 +75,8 @@ public class ZenModeControllerImplTest extends SysuiTestCase { Handler.createAsync(Looper.myLooper()), mBroadcastDispatcher, mDumpManager, - new FakeSettings()); + new FakeSettings(), + mUserTracker); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/ripple/MultiRippleControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/MultiRippleControllerTest.kt index 05512e5bf1ce..0d19ab1db390 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/ripple/MultiRippleControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/MultiRippleControllerTest.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package com.android.systemui.ripple +package com.android.systemui.surfaceeffects.ripple import android.graphics.Color import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.ripple.MultiRippleController.Companion.MAX_RIPPLE_NUMBER +import com.android.systemui.surfaceeffects.ripple.MultiRippleController.Companion.MAX_RIPPLE_NUMBER import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat diff --git a/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/MultiRippleViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/MultiRippleViewTest.kt new file mode 100644 index 000000000000..2024d53b0212 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/MultiRippleViewTest.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.surfaceeffects.ripple + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class MultiRippleViewTest : SysuiTestCase() { + private val fakeSystemClock = FakeSystemClock() + // FakeExecutor is needed to run animator. + private val fakeExecutor = FakeExecutor(fakeSystemClock) + + @Test + fun onRippleFinishes_triggersRippleFinished() { + val multiRippleView = MultiRippleView(context, null) + val multiRippleController = MultiRippleController(multiRippleView) + val rippleAnimationConfig = RippleAnimationConfig(duration = 1000L) + + var isTriggered = false + val listener = + object : MultiRippleView.Companion.RipplesFinishedListener { + override fun onRipplesFinish() { + isTriggered = true + } + } + multiRippleView.addRipplesFinishedListener(listener) + + fakeExecutor.execute { + val rippleAnimation = RippleAnimation(rippleAnimationConfig) + multiRippleController.play(rippleAnimation) + + fakeSystemClock.advanceTime(rippleAnimationConfig.duration) + + assertThat(isTriggered).isTrue() + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/ripple/RippleAnimationTest.kt b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/RippleAnimationTest.kt index 7662282a04f4..756397a30e43 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/ripple/RippleAnimationTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/RippleAnimationTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.ripple +package com.android.systemui.surfaceeffects.ripple import android.graphics.Color import android.testing.AndroidTestingRunner diff --git a/packages/SystemUI/tests/src/com/android/systemui/ripple/RippleViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/RippleViewTest.kt index 2d2f4cc9edd2..1e5ab7e25599 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/ripple/RippleViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/RippleViewTest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.systemui.ripple +package com.android.systemui.surfaceeffects.ripple import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest @@ -21,12 +21,10 @@ import com.android.systemui.SysuiTestCase import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mock @SmallTest @RunWith(AndroidTestingRunner::class) class RippleViewTest : SysuiTestCase() { - @Mock private lateinit var rippleView: RippleView @Before diff --git a/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseControllerTest.kt new file mode 100644 index 000000000000..d25c8c1a5899 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseControllerTest.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.surfaceeffects.turbulencenoise + +import android.graphics.Color +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class TurbulenceNoiseControllerTest : SysuiTestCase() { + private val fakeSystemClock = FakeSystemClock() + // FakeExecutor is needed to run animator. + private val fakeExecutor = FakeExecutor(fakeSystemClock) + + @Test + fun play_playsTurbulenceNoise() { + val config = TurbulenceNoiseAnimationConfig(duration = 1000f) + val turbulenceNoiseView = TurbulenceNoiseView(context, null) + + val turbulenceNoiseController = TurbulenceNoiseController(turbulenceNoiseView) + + fakeExecutor.execute { + turbulenceNoiseController.play(config) + + assertThat(turbulenceNoiseView.isPlaying).isTrue() + + fakeSystemClock.advanceTime(config.duration.toLong()) + + assertThat(turbulenceNoiseView.isPlaying).isFalse() + } + } + + @Test + fun updateColor_updatesCorrectColor() { + val config = TurbulenceNoiseAnimationConfig(duration = 1000f, color = Color.WHITE) + val turbulenceNoiseView = TurbulenceNoiseView(context, null) + val expectedColor = Color.RED + + val turbulenceNoiseController = TurbulenceNoiseController(turbulenceNoiseView) + + fakeExecutor.execute { + turbulenceNoiseController.play(config) + + turbulenceNoiseView.updateColor(expectedColor) + + fakeSystemClock.advanceTime(config.duration.toLong()) + + assertThat(config.color).isEqualTo(expectedColor) + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseViewTest.kt new file mode 100644 index 000000000000..633aac076502 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseViewTest.kt @@ -0,0 +1,86 @@ +/* + * 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.surfaceeffects.turbulencenoise + +import android.testing.AndroidTestingRunner +import android.view.View +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class TurbulenceNoiseViewTest : SysuiTestCase() { + + private val fakeSystemClock = FakeSystemClock() + // FakeExecutor is needed to run animator. + private val fakeExecutor = FakeExecutor(fakeSystemClock) + + @Test + fun play_viewHasCorrectVisibility() { + val config = TurbulenceNoiseAnimationConfig(duration = 1000f) + val turbulenceNoiseView = TurbulenceNoiseView(context, null) + + assertThat(turbulenceNoiseView.visibility).isEqualTo(View.INVISIBLE) + + fakeExecutor.execute { + turbulenceNoiseView.play(config) + + assertThat(turbulenceNoiseView.visibility).isEqualTo(View.VISIBLE) + + fakeSystemClock.advanceTime(config.duration.toLong()) + + assertThat(turbulenceNoiseView.visibility).isEqualTo(View.INVISIBLE) + } + } + + @Test + fun play_playsAnimation() { + val config = TurbulenceNoiseAnimationConfig(duration = 1000f) + val turbulenceNoiseView = TurbulenceNoiseView(context, null) + + fakeExecutor.execute { + turbulenceNoiseView.play(config) + + assertThat(turbulenceNoiseView.isPlaying).isTrue() + } + } + + @Test + fun play_onEnd_triggersOnAnimationEnd() { + var animationEnd = false + val config = + TurbulenceNoiseAnimationConfig( + duration = 1000f, + onAnimationEnd = { animationEnd = true } + ) + val turbulenceNoiseView = TurbulenceNoiseView(context, null) + + fakeExecutor.execute { + turbulenceNoiseView.play(config) + + assertThat(turbulenceNoiseView.isPlaying).isTrue() + + fakeSystemClock.advanceTime(config.duration.toLong()) + + assertThat(animationEnd).isTrue() + } + } +} 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 8572478589fd..09f0d4a10410 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 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { ) ) - verify(logger).logViewAddition("Fake Window Title") + verify(logger).logViewAddition("id", "Fake Window Title") } @Test @@ -153,7 +153,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { underTest.displayView(getState()) assertThat(fakeWakeLock.isHeld).isTrue() - underTest.removeView("test reason") + underTest.removeView("id", "test reason") assertThat(fakeWakeLock.isHeld).isFalse() } @@ -263,21 +263,143 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { } @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 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { 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 d155050ce932..116b8fe62b37 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 @@ class TemporaryViewLoggerTest : SysuiTestCase() { @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 @@ class TemporaryViewLoggerTest : SysuiTestCase() { @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 @@ class TemporaryViewLoggerTest : SysuiTestCase() { 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 8e37aa292240..47c84ab48093 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 @@ class ChipbarCoordinatorTest : SysuiTestCase() { windowTitle = WINDOW_TITLE, wakeReason = WAKE_REASON, timeoutMs = TIMEOUT, + id = DEVICE_ID, ) } @@ -401,3 +402,4 @@ class ChipbarCoordinatorTest : SysuiTestCase() { 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/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt deleted file mode 100644 index 7c7f0e1e0e12..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt +++ /dev/null @@ -1,248 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package com.android.systemui.user.data.repository - -import android.content.pm.UserInfo -import android.os.UserHandle -import android.os.UserManager -import android.provider.Settings -import androidx.test.filters.SmallTest -import com.android.systemui.user.data.model.UserSwitcherSettingsModel -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -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.Mockito.`when` as whenever - -@SmallTest -@RunWith(JUnit4::class) -class UserRepositoryImplRefactoredTest : UserRepositoryImplTest() { - - @Before - fun setUp() { - super.setUp(isRefactored = true) - } - - @Test - fun userSwitcherSettings() = runSelfCancelingTest { - setUpGlobalSettings( - isSimpleUserSwitcher = true, - isAddUsersFromLockscreen = true, - isUserSwitcherEnabled = true, - ) - underTest = create(this) - - var value: UserSwitcherSettingsModel? = null - underTest.userSwitcherSettings.onEach { value = it }.launchIn(this) - - assertUserSwitcherSettings( - model = value, - expectedSimpleUserSwitcher = true, - expectedAddUsersFromLockscreen = true, - expectedUserSwitcherEnabled = true, - ) - - setUpGlobalSettings( - isSimpleUserSwitcher = false, - isAddUsersFromLockscreen = true, - isUserSwitcherEnabled = true, - ) - assertUserSwitcherSettings( - model = value, - expectedSimpleUserSwitcher = false, - expectedAddUsersFromLockscreen = true, - expectedUserSwitcherEnabled = true, - ) - } - - @Test - fun refreshUsers() = runSelfCancelingTest { - underTest = create(this) - val initialExpectedValue = - setUpUsers( - count = 3, - selectedIndex = 0, - ) - var userInfos: List<UserInfo>? = null - var selectedUserInfo: UserInfo? = null - underTest.userInfos.onEach { userInfos = it }.launchIn(this) - underTest.selectedUserInfo.onEach { selectedUserInfo = it }.launchIn(this) - - underTest.refreshUsers() - assertThat(userInfos).isEqualTo(initialExpectedValue) - assertThat(selectedUserInfo).isEqualTo(initialExpectedValue[0]) - assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedUserInfo?.id) - - val secondExpectedValue = - setUpUsers( - count = 4, - selectedIndex = 1, - ) - underTest.refreshUsers() - assertThat(userInfos).isEqualTo(secondExpectedValue) - assertThat(selectedUserInfo).isEqualTo(secondExpectedValue[1]) - assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedUserInfo?.id) - - val selectedNonGuestUserId = selectedUserInfo?.id - val thirdExpectedValue = - setUpUsers( - count = 2, - isLastGuestUser = true, - selectedIndex = 1, - ) - underTest.refreshUsers() - assertThat(userInfos).isEqualTo(thirdExpectedValue) - assertThat(selectedUserInfo).isEqualTo(thirdExpectedValue[1]) - assertThat(selectedUserInfo?.isGuest).isTrue() - assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedNonGuestUserId) - } - - @Test - fun `refreshUsers - sorts by creation time - guest user last`() = runSelfCancelingTest { - underTest = create(this) - val unsortedUsers = - setUpUsers( - count = 3, - selectedIndex = 0, - isLastGuestUser = true, - ) - unsortedUsers[0].creationTime = 999 - unsortedUsers[1].creationTime = 900 - unsortedUsers[2].creationTime = 950 - val expectedUsers = - listOf( - unsortedUsers[1], - unsortedUsers[0], - unsortedUsers[2], // last because this is the guest - ) - var userInfos: List<UserInfo>? = null - underTest.userInfos.onEach { userInfos = it }.launchIn(this) - - underTest.refreshUsers() - assertThat(userInfos).isEqualTo(expectedUsers) - } - - @Test - fun `userTrackerCallback - updates selectedUserInfo`() = runSelfCancelingTest { - underTest = create(this) - var selectedUserInfo: UserInfo? = null - underTest.selectedUserInfo.onEach { selectedUserInfo = it }.launchIn(this) - setUpUsers( - count = 2, - selectedIndex = 0, - ) - tracker.onProfileChanged() - assertThat(selectedUserInfo?.id == 0) - setUpUsers( - count = 2, - selectedIndex = 1, - ) - tracker.onProfileChanged() - assertThat(selectedUserInfo?.id == 1) - } - - private fun setUpUsers( - count: Int, - isLastGuestUser: Boolean = false, - selectedIndex: Int = 0, - ): List<UserInfo> { - val userInfos = - (0 until count).map { index -> - createUserInfo( - index, - isGuest = isLastGuestUser && index == count - 1, - ) - } - whenever(manager.aliveUsers).thenReturn(userInfos) - tracker.set(userInfos, selectedIndex) - return userInfos - } - - private fun createUserInfo( - id: Int, - isGuest: Boolean, - ): UserInfo { - val flags = 0 - return UserInfo( - id, - "user_$id", - /* iconPath= */ "", - flags, - if (isGuest) UserManager.USER_TYPE_FULL_GUEST else UserInfo.getDefaultUserType(flags), - ) - } - - private fun setUpGlobalSettings( - isSimpleUserSwitcher: Boolean = false, - isAddUsersFromLockscreen: Boolean = false, - isUserSwitcherEnabled: Boolean = true, - ) { - context.orCreateTestableResources.addOverride( - com.android.internal.R.bool.config_expandLockScreenUserSwitcher, - true, - ) - globalSettings.putIntForUser( - UserRepositoryImpl.SETTING_SIMPLE_USER_SWITCHER, - if (isSimpleUserSwitcher) 1 else 0, - UserHandle.USER_SYSTEM, - ) - globalSettings.putIntForUser( - Settings.Global.ADD_USERS_WHEN_LOCKED, - if (isAddUsersFromLockscreen) 1 else 0, - UserHandle.USER_SYSTEM, - ) - globalSettings.putIntForUser( - Settings.Global.USER_SWITCHER_ENABLED, - if (isUserSwitcherEnabled) 1 else 0, - UserHandle.USER_SYSTEM, - ) - } - - private fun assertUserSwitcherSettings( - model: UserSwitcherSettingsModel?, - expectedSimpleUserSwitcher: Boolean, - expectedAddUsersFromLockscreen: Boolean, - expectedUserSwitcherEnabled: Boolean, - ) { - checkNotNull(model) - assertThat(model.isSimpleUserSwitcher).isEqualTo(expectedSimpleUserSwitcher) - assertThat(model.isAddUsersFromLockscreen).isEqualTo(expectedAddUsersFromLockscreen) - assertThat(model.isUserSwitcherEnabled).isEqualTo(expectedUserSwitcherEnabled) - } - - /** - * Executes the given block of execution within the scope of a dedicated [CoroutineScope] which - * is then automatically canceled and cleaned-up. - */ - private fun runSelfCancelingTest( - block: suspend CoroutineScope.() -> Unit, - ) = - runBlocking(Dispatchers.Main.immediate) { - val scope = CoroutineScope(coroutineContext + Job()) - block(scope) - scope.cancel() - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt index dcea83a55a74..2e527be1af89 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt @@ -17,54 +17,263 @@ package com.android.systemui.user.data.repository +import android.content.pm.UserInfo +import android.os.UserHandle import android.os.UserManager +import android.provider.Settings +import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.flags.FakeFeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.settings.FakeUserTracker -import com.android.systemui.statusbar.policy.UserSwitcherController +import com.android.systemui.user.data.model.UserSwitcherSettingsModel import com.android.systemui.util.settings.FakeSettings +import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.TestCoroutineScope +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.`when` as whenever import org.mockito.MockitoAnnotations -abstract class UserRepositoryImplTest : SysuiTestCase() { +@SmallTest +@RunWith(JUnit4::class) +class UserRepositoryImplTest : SysuiTestCase() { - @Mock protected lateinit var manager: UserManager - @Mock protected lateinit var controller: UserSwitcherController + @Mock private lateinit var manager: UserManager - protected lateinit var underTest: UserRepositoryImpl + private lateinit var underTest: UserRepositoryImpl - protected lateinit var globalSettings: FakeSettings - protected lateinit var tracker: FakeUserTracker - protected lateinit var featureFlags: FakeFeatureFlags + private lateinit var globalSettings: FakeSettings + private lateinit var tracker: FakeUserTracker - protected fun setUp(isRefactored: Boolean) { + @Before + fun setUp() { MockitoAnnotations.initMocks(this) globalSettings = FakeSettings() tracker = FakeUserTracker() - featureFlags = FakeFeatureFlags() - featureFlags.set(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER, !isRefactored) } - protected fun create(scope: CoroutineScope = TestCoroutineScope()): UserRepositoryImpl { + @Test + fun userSwitcherSettings() = runSelfCancelingTest { + setUpGlobalSettings( + isSimpleUserSwitcher = true, + isAddUsersFromLockscreen = true, + isUserSwitcherEnabled = true, + ) + underTest = create(this) + + var value: UserSwitcherSettingsModel? = null + underTest.userSwitcherSettings.onEach { value = it }.launchIn(this) + + assertUserSwitcherSettings( + model = value, + expectedSimpleUserSwitcher = true, + expectedAddUsersFromLockscreen = true, + expectedUserSwitcherEnabled = true, + ) + + setUpGlobalSettings( + isSimpleUserSwitcher = false, + isAddUsersFromLockscreen = true, + isUserSwitcherEnabled = true, + ) + assertUserSwitcherSettings( + model = value, + expectedSimpleUserSwitcher = false, + expectedAddUsersFromLockscreen = true, + expectedUserSwitcherEnabled = true, + ) + } + + @Test + fun refreshUsers() = runSelfCancelingTest { + underTest = create(this) + val initialExpectedValue = + setUpUsers( + count = 3, + selectedIndex = 0, + ) + var userInfos: List<UserInfo>? = null + var selectedUserInfo: UserInfo? = null + underTest.userInfos.onEach { userInfos = it }.launchIn(this) + underTest.selectedUserInfo.onEach { selectedUserInfo = it }.launchIn(this) + + underTest.refreshUsers() + assertThat(userInfos).isEqualTo(initialExpectedValue) + assertThat(selectedUserInfo).isEqualTo(initialExpectedValue[0]) + assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedUserInfo?.id) + + val secondExpectedValue = + setUpUsers( + count = 4, + selectedIndex = 1, + ) + underTest.refreshUsers() + assertThat(userInfos).isEqualTo(secondExpectedValue) + assertThat(selectedUserInfo).isEqualTo(secondExpectedValue[1]) + assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedUserInfo?.id) + + val selectedNonGuestUserId = selectedUserInfo?.id + val thirdExpectedValue = + setUpUsers( + count = 2, + isLastGuestUser = true, + selectedIndex = 1, + ) + underTest.refreshUsers() + assertThat(userInfos).isEqualTo(thirdExpectedValue) + assertThat(selectedUserInfo).isEqualTo(thirdExpectedValue[1]) + assertThat(selectedUserInfo?.isGuest).isTrue() + assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedNonGuestUserId) + } + + @Test + fun `refreshUsers - sorts by creation time - guest user last`() = runSelfCancelingTest { + underTest = create(this) + val unsortedUsers = + setUpUsers( + count = 3, + selectedIndex = 0, + isLastGuestUser = true, + ) + unsortedUsers[0].creationTime = 999 + unsortedUsers[1].creationTime = 900 + unsortedUsers[2].creationTime = 950 + val expectedUsers = + listOf( + unsortedUsers[1], + unsortedUsers[0], + unsortedUsers[2], // last because this is the guest + ) + var userInfos: List<UserInfo>? = null + underTest.userInfos.onEach { userInfos = it }.launchIn(this) + + underTest.refreshUsers() + assertThat(userInfos).isEqualTo(expectedUsers) + } + + private fun setUpUsers( + count: Int, + isLastGuestUser: Boolean = false, + selectedIndex: Int = 0, + ): List<UserInfo> { + val userInfos = + (0 until count).map { index -> + createUserInfo( + index, + isGuest = isLastGuestUser && index == count - 1, + ) + } + whenever(manager.aliveUsers).thenReturn(userInfos) + tracker.set(userInfos, selectedIndex) + return userInfos + } + @Test + fun `userTrackerCallback - updates selectedUserInfo`() = runSelfCancelingTest { + underTest = create(this) + var selectedUserInfo: UserInfo? = null + underTest.selectedUserInfo.onEach { selectedUserInfo = it }.launchIn(this) + setUpUsers( + count = 2, + selectedIndex = 0, + ) + tracker.onProfileChanged() + assertThat(selectedUserInfo?.id).isEqualTo(0) + setUpUsers( + count = 2, + selectedIndex = 1, + ) + tracker.onProfileChanged() + assertThat(selectedUserInfo?.id).isEqualTo(1) + } + + private fun createUserInfo( + id: Int, + isGuest: Boolean, + ): UserInfo { + val flags = 0 + return UserInfo( + id, + "user_$id", + /* iconPath= */ "", + flags, + if (isGuest) UserManager.USER_TYPE_FULL_GUEST else UserInfo.getDefaultUserType(flags), + ) + } + + private fun setUpGlobalSettings( + isSimpleUserSwitcher: Boolean = false, + isAddUsersFromLockscreen: Boolean = false, + isUserSwitcherEnabled: Boolean = true, + ) { + context.orCreateTestableResources.addOverride( + com.android.internal.R.bool.config_expandLockScreenUserSwitcher, + true, + ) + globalSettings.putIntForUser( + UserRepositoryImpl.SETTING_SIMPLE_USER_SWITCHER, + if (isSimpleUserSwitcher) 1 else 0, + UserHandle.USER_SYSTEM, + ) + globalSettings.putIntForUser( + Settings.Global.ADD_USERS_WHEN_LOCKED, + if (isAddUsersFromLockscreen) 1 else 0, + UserHandle.USER_SYSTEM, + ) + globalSettings.putIntForUser( + Settings.Global.USER_SWITCHER_ENABLED, + if (isUserSwitcherEnabled) 1 else 0, + UserHandle.USER_SYSTEM, + ) + } + + private fun assertUserSwitcherSettings( + model: UserSwitcherSettingsModel?, + expectedSimpleUserSwitcher: Boolean, + expectedAddUsersFromLockscreen: Boolean, + expectedUserSwitcherEnabled: Boolean, + ) { + checkNotNull(model) + assertThat(model.isSimpleUserSwitcher).isEqualTo(expectedSimpleUserSwitcher) + assertThat(model.isAddUsersFromLockscreen).isEqualTo(expectedAddUsersFromLockscreen) + assertThat(model.isUserSwitcherEnabled).isEqualTo(expectedUserSwitcherEnabled) + } + + /** + * Executes the given block of execution within the scope of a dedicated [CoroutineScope] which + * is then automatically canceled and cleaned-up. + */ + private fun runSelfCancelingTest( + block: suspend CoroutineScope.() -> Unit, + ) = + runBlocking(Dispatchers.Main.immediate) { + val scope = CoroutineScope(coroutineContext + Job()) + block(scope) + scope.cancel() + } + + private fun create(scope: CoroutineScope = TestCoroutineScope()): UserRepositoryImpl { return UserRepositoryImpl( appContext = context, manager = manager, - controller = controller, applicationScope = scope, mainDispatcher = IMMEDIATE, backgroundDispatcher = IMMEDIATE, globalSettings = globalSettings, tracker = tracker, - featureFlags = featureFlags, ) } companion object { - @JvmStatic protected val IMMEDIATE = Dispatchers.Main.immediate + @JvmStatic private val IMMEDIATE = Dispatchers.Main.immediate } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt deleted file mode 100644 index a363a037c499..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package com.android.systemui.user.data.repository - -import android.content.pm.UserInfo -import androidx.test.filters.SmallTest -import com.android.systemui.statusbar.policy.UserSwitcherController -import com.android.systemui.user.data.source.UserRecord -import com.android.systemui.user.shared.model.UserActionModel -import com.android.systemui.user.shared.model.UserModel -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.capture -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -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.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` as whenever - -@SmallTest -@RunWith(JUnit4::class) -class UserRepositoryImplUnrefactoredTest : UserRepositoryImplTest() { - - companion object { - private val IMMEDIATE = Dispatchers.Main.immediate - } - - @Captor - private lateinit var userSwitchCallbackCaptor: - ArgumentCaptor<UserSwitcherController.UserSwitchCallback> - - @Before - fun setUp() { - super.setUp(isRefactored = false) - - whenever(controller.isAddUsersFromLockScreenEnabled).thenReturn(MutableStateFlow(false)) - whenever(controller.isGuestUserAutoCreated).thenReturn(false) - whenever(controller.isGuestUserResetting).thenReturn(false) - - underTest = create() - } - - @Test - fun `users - registers for updates`() = - runBlocking(IMMEDIATE) { - val job = underTest.users.onEach {}.launchIn(this) - - verify(controller).addUserSwitchCallback(any()) - - job.cancel() - } - - @Test - fun `users - unregisters from updates`() = - runBlocking(IMMEDIATE) { - val job = underTest.users.onEach {}.launchIn(this) - verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor)) - - job.cancel() - - verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value) - } - - @Test - fun `users - does not include actions`() = - runBlocking(IMMEDIATE) { - whenever(controller.users) - .thenReturn( - arrayListOf( - createUserRecord(0, isSelected = true), - createActionRecord(UserActionModel.ADD_USER), - createUserRecord(1), - createUserRecord(2), - createActionRecord(UserActionModel.ADD_SUPERVISED_USER), - createActionRecord(UserActionModel.ENTER_GUEST_MODE), - createActionRecord(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT), - ) - ) - var models: List<UserModel>? = null - val job = underTest.users.onEach { models = it }.launchIn(this) - - assertThat(models).hasSize(3) - assertThat(models?.get(0)?.id).isEqualTo(0) - assertThat(models?.get(0)?.isSelected).isTrue() - assertThat(models?.get(1)?.id).isEqualTo(1) - assertThat(models?.get(1)?.isSelected).isFalse() - assertThat(models?.get(2)?.id).isEqualTo(2) - assertThat(models?.get(2)?.isSelected).isFalse() - job.cancel() - } - - @Test - fun selectedUser() = - runBlocking(IMMEDIATE) { - whenever(controller.users) - .thenReturn( - arrayListOf( - createUserRecord(0, isSelected = true), - createUserRecord(1), - createUserRecord(2), - ) - ) - var id: Int? = null - val job = underTest.selectedUser.map { it.id }.onEach { id = it }.launchIn(this) - - assertThat(id).isEqualTo(0) - - whenever(controller.users) - .thenReturn( - arrayListOf( - createUserRecord(0), - createUserRecord(1), - createUserRecord(2, isSelected = true), - ) - ) - verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor)) - userSwitchCallbackCaptor.value.onUserSwitched() - assertThat(id).isEqualTo(2) - - job.cancel() - } - - @Test - fun `actions - unregisters from updates`() = - runBlocking(IMMEDIATE) { - val job = underTest.actions.onEach {}.launchIn(this) - verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor)) - - job.cancel() - - verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value) - } - - @Test - fun `actions - registers for updates`() = - runBlocking(IMMEDIATE) { - val job = underTest.actions.onEach {}.launchIn(this) - - verify(controller).addUserSwitchCallback(any()) - - job.cancel() - } - - @Test - fun `actions - does not include users`() = - runBlocking(IMMEDIATE) { - whenever(controller.users) - .thenReturn( - arrayListOf( - createUserRecord(0, isSelected = true), - createActionRecord(UserActionModel.ADD_USER), - createUserRecord(1), - createUserRecord(2), - createActionRecord(UserActionModel.ADD_SUPERVISED_USER), - createActionRecord(UserActionModel.ENTER_GUEST_MODE), - createActionRecord(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT), - ) - ) - var models: List<UserActionModel>? = null - val job = underTest.actions.onEach { models = it }.launchIn(this) - - assertThat(models).hasSize(4) - assertThat(models?.get(0)).isEqualTo(UserActionModel.ADD_USER) - assertThat(models?.get(1)).isEqualTo(UserActionModel.ADD_SUPERVISED_USER) - assertThat(models?.get(2)).isEqualTo(UserActionModel.ENTER_GUEST_MODE) - assertThat(models?.get(3)).isEqualTo(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) - job.cancel() - } - - private fun createUserRecord(id: Int, isSelected: Boolean = false): UserRecord { - return UserRecord( - info = UserInfo(id, "name$id", 0), - isCurrent = isSelected, - ) - } - - private fun createActionRecord(action: UserActionModel): UserRecord { - return UserRecord( - isAddUser = action == UserActionModel.ADD_USER, - isAddSupervisedUser = action == UserActionModel.ADD_SUPERVISED_USER, - isGuest = action == UserActionModel.ENTER_GUEST_MODE, - isManageUsers = action == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, - ) - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt deleted file mode 100644 index f682e31c0547..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt +++ /dev/null @@ -1,740 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package com.android.systemui.user.domain.interactor - -import android.content.Intent -import android.content.pm.UserInfo -import android.graphics.Bitmap -import android.graphics.drawable.Drawable -import android.os.UserHandle -import android.os.UserManager -import android.provider.Settings -import androidx.test.filters.SmallTest -import com.android.internal.R.drawable.ic_account_circle -import com.android.systemui.R -import com.android.systemui.common.shared.model.Text -import com.android.systemui.qs.user.UserSwitchDialogController -import com.android.systemui.user.data.model.UserSwitcherSettingsModel -import com.android.systemui.user.data.source.UserRecord -import com.android.systemui.user.domain.model.ShowDialogRequestModel -import com.android.systemui.user.shared.model.UserActionModel -import com.android.systemui.user.shared.model.UserModel -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.eq -import com.android.systemui.util.mockito.kotlinArgumentCaptor -import com.android.systemui.util.mockito.mock -import com.android.systemui.util.mockito.whenever -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.advanceUntilIdle -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 -import org.mockito.ArgumentMatchers.anyBoolean -import org.mockito.ArgumentMatchers.anyInt -import org.mockito.Mockito.never -import org.mockito.Mockito.verify - -@SmallTest -@RunWith(JUnit4::class) -class UserInteractorRefactoredTest : UserInteractorTest() { - - override fun isRefactored(): Boolean { - return true - } - - @Before - override fun setUp() { - super.setUp() - - overrideResource(R.drawable.ic_account_circle, GUEST_ICON) - overrideResource(R.dimen.max_avatar_size, 10) - overrideResource( - com.android.internal.R.string.config_supervisedUserCreationPackage, - SUPERVISED_USER_CREATION_APP_PACKAGE, - ) - whenever(manager.getUserIcon(anyInt())).thenReturn(ICON) - whenever(manager.canAddMoreUsers(any())).thenReturn(true) - } - - @Test - fun `onRecordSelected - user`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 3, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - - underTest.onRecordSelected(UserRecord(info = userInfos[1]), dialogShower) - - verify(dialogShower).dismiss() - verify(activityManager).switchUser(userInfos[1].id) - Unit - } - - @Test - fun `onRecordSelected - switch to guest user`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 3, includeGuest = true) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - - underTest.onRecordSelected(UserRecord(info = userInfos.last())) - - verify(activityManager).switchUser(userInfos.last().id) - Unit - } - - @Test - fun `onRecordSelected - enter guest mode`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 3, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - val guestUserInfo = createUserInfo(id = 1337, name = "guest", isGuest = true) - whenever(manager.createGuest(any())).thenReturn(guestUserInfo) - - underTest.onRecordSelected(UserRecord(isGuest = true), dialogShower) - - verify(dialogShower).dismiss() - verify(manager).createGuest(any()) - Unit - } - - @Test - fun `onRecordSelected - action`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 3, includeGuest = true) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - - underTest.onRecordSelected(UserRecord(isAddSupervisedUser = true), dialogShower) - - verify(dialogShower, never()).dismiss() - verify(activityStarter).startActivity(any(), anyBoolean()) - } - - @Test - fun `users - switcher enabled`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 3, includeGuest = true) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - - var value: List<UserModel>? = null - val job = underTest.users.onEach { value = it }.launchIn(this) - assertUsers(models = value, count = 3, includeGuest = true) - - job.cancel() - } - - @Test - fun `users - switches to second user`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 2, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - - var value: List<UserModel>? = null - val job = underTest.users.onEach { value = it }.launchIn(this) - userRepository.setSelectedUserInfo(userInfos[1]) - - assertUsers(models = value, count = 2, selectedIndex = 1) - job.cancel() - } - - @Test - fun `users - switcher not enabled`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 2, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = false)) - - var value: List<UserModel>? = null - val job = underTest.users.onEach { value = it }.launchIn(this) - assertUsers(models = value, count = 1) - - job.cancel() - } - - @Test - fun selectedUser() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 2, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - - var value: UserModel? = null - val job = underTest.selectedUser.onEach { value = it }.launchIn(this) - assertUser(value, id = 0, isSelected = true) - - userRepository.setSelectedUserInfo(userInfos[1]) - assertUser(value, id = 1, isSelected = true) - - job.cancel() - } - - @Test - fun `actions - device unlocked`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 2, includeGuest = false) - - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - keyguardRepository.setKeyguardShowing(false) - var value: List<UserActionModel>? = null - val job = underTest.actions.onEach { value = it }.launchIn(this) - - assertThat(value) - .isEqualTo( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, - ) - ) - - job.cancel() - } - - @Test - fun `actions - device unlocked user not primary - empty list`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 2, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[1]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - keyguardRepository.setKeyguardShowing(false) - var value: List<UserActionModel>? = null - val job = underTest.actions.onEach { value = it }.launchIn(this) - - assertThat(value).isEqualTo(emptyList<UserActionModel>()) - - job.cancel() - } - - @Test - fun `actions - device unlocked user is guest - empty list`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 2, includeGuest = true) - assertThat(userInfos[1].isGuest).isTrue() - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[1]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - keyguardRepository.setKeyguardShowing(false) - var value: List<UserActionModel>? = null - val job = underTest.actions.onEach { value = it }.launchIn(this) - - assertThat(value).isEqualTo(emptyList<UserActionModel>()) - - job.cancel() - } - - @Test - fun `actions - device locked add from lockscreen set - full list`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 2, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings( - UserSwitcherSettingsModel( - isUserSwitcherEnabled = true, - isAddUsersFromLockscreen = true, - ) - ) - keyguardRepository.setKeyguardShowing(false) - var value: List<UserActionModel>? = null - val job = underTest.actions.onEach { value = it }.launchIn(this) - - assertThat(value) - .isEqualTo( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, - ) - ) - - job.cancel() - } - - @Test - fun `actions - device locked - only guest action and manage user is shown`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 2, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - keyguardRepository.setKeyguardShowing(true) - var value: List<UserActionModel>? = null - val job = underTest.actions.onEach { value = it }.launchIn(this) - - assertThat(value) - .isEqualTo( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT - ) - ) - - job.cancel() - } - - @Test - fun `executeAction - add user - dialog shown`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 2, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - keyguardRepository.setKeyguardShowing(false) - var dialogRequest: ShowDialogRequestModel? = null - val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) - val dialogShower: UserSwitchDialogController.DialogShower = mock() - - underTest.executeAction(UserActionModel.ADD_USER, dialogShower) - assertThat(dialogRequest) - .isEqualTo( - ShowDialogRequestModel.ShowAddUserDialog( - userHandle = userInfos[0].userHandle, - isKeyguardShowing = false, - showEphemeralMessage = false, - dialogShower = dialogShower, - ) - ) - - underTest.onDialogShown() - assertThat(dialogRequest).isNull() - - job.cancel() - } - - @Test - fun `executeAction - add supervised user - starts activity`() = - runBlocking(IMMEDIATE) { - underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER) - - val intentCaptor = kotlinArgumentCaptor<Intent>() - verify(activityStarter).startActivity(intentCaptor.capture(), eq(true)) - assertThat(intentCaptor.value.action) - .isEqualTo(UserManager.ACTION_CREATE_SUPERVISED_USER) - assertThat(intentCaptor.value.`package`).isEqualTo(SUPERVISED_USER_CREATION_APP_PACKAGE) - } - - @Test - fun `executeAction - navigate to manage users`() = - runBlocking(IMMEDIATE) { - underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) - - val intentCaptor = kotlinArgumentCaptor<Intent>() - verify(activityStarter).startActivity(intentCaptor.capture(), eq(true)) - assertThat(intentCaptor.value.action).isEqualTo(Settings.ACTION_USER_SETTINGS) - } - - @Test - fun `executeAction - guest mode`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 2, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - val guestUserInfo = createUserInfo(id = 1337, name = "guest", isGuest = true) - whenever(manager.createGuest(any())).thenReturn(guestUserInfo) - val dialogRequests = mutableListOf<ShowDialogRequestModel?>() - val showDialogsJob = - underTest.dialogShowRequests - .onEach { - dialogRequests.add(it) - if (it != null) { - underTest.onDialogShown() - } - } - .launchIn(this) - val dismissDialogsJob = - underTest.dialogDismissRequests - .onEach { - if (it != null) { - underTest.onDialogDismissed() - } - } - .launchIn(this) - - underTest.executeAction(UserActionModel.ENTER_GUEST_MODE) - - assertThat(dialogRequests) - .contains( - ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true), - ) - verify(activityManager).switchUser(guestUserInfo.id) - - showDialogsJob.cancel() - dismissDialogsJob.cancel() - } - - @Test - fun `selectUser - already selected guest re-selected - exit guest dialog`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 2, includeGuest = true) - val guestUserInfo = userInfos[1] - assertThat(guestUserInfo.isGuest).isTrue() - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(guestUserInfo) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - var dialogRequest: ShowDialogRequestModel? = null - val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) - - underTest.selectUser( - newlySelectedUserId = guestUserInfo.id, - dialogShower = dialogShower, - ) - - assertThat(dialogRequest) - .isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java) - verify(dialogShower, never()).dismiss() - job.cancel() - } - - @Test - fun `selectUser - currently guest non-guest selected - exit guest dialog`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 2, includeGuest = true) - val guestUserInfo = userInfos[1] - assertThat(guestUserInfo.isGuest).isTrue() - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(guestUserInfo) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - var dialogRequest: ShowDialogRequestModel? = null - val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) - - underTest.selectUser(newlySelectedUserId = userInfos[0].id, dialogShower = dialogShower) - - assertThat(dialogRequest) - .isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java) - verify(dialogShower, never()).dismiss() - job.cancel() - } - - @Test - fun `selectUser - not currently guest - switches users`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 2, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - var dialogRequest: ShowDialogRequestModel? = null - val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) - - underTest.selectUser(newlySelectedUserId = userInfos[1].id, dialogShower = dialogShower) - - assertThat(dialogRequest).isNull() - verify(activityManager).switchUser(userInfos[1].id) - verify(dialogShower).dismiss() - job.cancel() - } - - @Test - fun `Telephony call state changes - refreshes users`() = - runBlocking(IMMEDIATE) { - val refreshUsersCallCount = userRepository.refreshUsersCallCount - - telephonyRepository.setCallState(1) - - assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) - } - - @Test - fun `User switched broadcast`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 2, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - val callback1: UserInteractor.UserCallback = mock() - val callback2: UserInteractor.UserCallback = mock() - underTest.addCallback(callback1) - underTest.addCallback(callback2) - val refreshUsersCallCount = userRepository.refreshUsersCallCount - - userRepository.setSelectedUserInfo(userInfos[1]) - fakeBroadcastDispatcher.registeredReceivers.forEach { - it.onReceive( - context, - Intent(Intent.ACTION_USER_SWITCHED) - .putExtra(Intent.EXTRA_USER_HANDLE, userInfos[1].id), - ) - } - - verify(callback1).onUserStateChanged() - verify(callback2).onUserStateChanged() - assertThat(userRepository.secondaryUserId).isEqualTo(userInfos[1].id) - assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) - } - - @Test - fun `User info changed broadcast`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 2, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - val refreshUsersCallCount = userRepository.refreshUsersCallCount - - fakeBroadcastDispatcher.registeredReceivers.forEach { - it.onReceive( - context, - Intent(Intent.ACTION_USER_INFO_CHANGED), - ) - } - - assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) - } - - @Test - fun `System user unlocked broadcast - refresh users`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 2, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - val refreshUsersCallCount = userRepository.refreshUsersCallCount - - fakeBroadcastDispatcher.registeredReceivers.forEach { - it.onReceive( - context, - Intent(Intent.ACTION_USER_UNLOCKED) - .putExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_SYSTEM), - ) - } - - assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) - } - - @Test - fun `Non-system user unlocked broadcast - do not refresh users`() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 2, includeGuest = false) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - val refreshUsersCallCount = userRepository.refreshUsersCallCount - - fakeBroadcastDispatcher.registeredReceivers.forEach { - it.onReceive( - context, - Intent(Intent.ACTION_USER_UNLOCKED).putExtra(Intent.EXTRA_USER_HANDLE, 1337), - ) - } - - assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount) - } - - @Test - fun userRecords() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 3, includeGuest = false) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - keyguardRepository.setKeyguardShowing(false) - - testCoroutineScope.advanceUntilIdle() - - assertRecords( - records = underTest.userRecords.value, - userIds = listOf(0, 1, 2), - selectedUserIndex = 0, - includeGuest = false, - expectedActions = - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, - ), - ) - } - - @Test - fun selectedUserRecord() = - runBlocking(IMMEDIATE) { - val userInfos = createUserInfos(count = 3, includeGuest = true) - userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) - userRepository.setUserInfos(userInfos) - userRepository.setSelectedUserInfo(userInfos[0]) - keyguardRepository.setKeyguardShowing(false) - - assertRecordForUser( - record = underTest.selectedUserRecord.value, - id = 0, - hasPicture = true, - isCurrent = true, - isSwitchToEnabled = true, - ) - } - - private fun assertUsers( - models: List<UserModel>?, - count: Int, - selectedIndex: Int = 0, - includeGuest: Boolean = false, - ) { - checkNotNull(models) - assertThat(models.size).isEqualTo(count) - models.forEachIndexed { index, model -> - assertUser( - model = model, - id = index, - isSelected = index == selectedIndex, - isGuest = includeGuest && index == count - 1 - ) - } - } - - private fun assertUser( - model: UserModel?, - id: Int, - isSelected: Boolean = false, - isGuest: Boolean = false, - ) { - checkNotNull(model) - assertThat(model.id).isEqualTo(id) - assertThat(model.name).isEqualTo(Text.Loaded(if (isGuest) "guest" else "user_$id")) - assertThat(model.isSelected).isEqualTo(isSelected) - assertThat(model.isSelectable).isTrue() - assertThat(model.isGuest).isEqualTo(isGuest) - } - - private fun assertRecords( - records: List<UserRecord>, - userIds: List<Int>, - selectedUserIndex: Int = 0, - includeGuest: Boolean = false, - expectedActions: List<UserActionModel> = emptyList(), - ) { - assertThat(records.size >= userIds.size).isTrue() - userIds.indices.forEach { userIndex -> - val record = records[userIndex] - assertThat(record.info).isNotNull() - val isGuest = includeGuest && userIndex == userIds.size - 1 - assertRecordForUser( - record = record, - id = userIds[userIndex], - hasPicture = !isGuest, - isCurrent = userIndex == selectedUserIndex, - isGuest = isGuest, - isSwitchToEnabled = true, - ) - } - - assertThat(records.size - userIds.size).isEqualTo(expectedActions.size) - (userIds.size until userIds.size + expectedActions.size).forEach { actionIndex -> - val record = records[actionIndex] - assertThat(record.info).isNull() - assertRecordForAction( - record = record, - type = expectedActions[actionIndex - userIds.size], - ) - } - } - - private fun assertRecordForUser( - record: UserRecord?, - id: Int? = null, - hasPicture: Boolean = false, - isCurrent: Boolean = false, - isGuest: Boolean = false, - isSwitchToEnabled: Boolean = false, - ) { - checkNotNull(record) - assertThat(record.info?.id).isEqualTo(id) - assertThat(record.picture != null).isEqualTo(hasPicture) - assertThat(record.isCurrent).isEqualTo(isCurrent) - assertThat(record.isGuest).isEqualTo(isGuest) - assertThat(record.isSwitchToEnabled).isEqualTo(isSwitchToEnabled) - } - - private fun assertRecordForAction( - record: UserRecord, - type: UserActionModel, - ) { - assertThat(record.isGuest).isEqualTo(type == UserActionModel.ENTER_GUEST_MODE) - assertThat(record.isAddUser).isEqualTo(type == UserActionModel.ADD_USER) - assertThat(record.isAddSupervisedUser) - .isEqualTo(type == UserActionModel.ADD_SUPERVISED_USER) - } - - private fun createUserInfos( - count: Int, - includeGuest: Boolean, - ): List<UserInfo> { - return (0 until count).map { index -> - val isGuest = includeGuest && index == count - 1 - createUserInfo( - id = index, - name = - if (isGuest) { - "guest" - } else { - "user_$index" - }, - isPrimary = !isGuest && index == 0, - isGuest = isGuest, - ) - } - } - - private fun createUserInfo( - id: Int, - name: String, - isPrimary: Boolean = false, - isGuest: Boolean = false, - ): UserInfo { - return UserInfo( - id, - name, - /* iconPath= */ "", - /* flags= */ if (isPrimary) { - UserInfo.FLAG_PRIMARY or UserInfo.FLAG_ADMIN - } else { - 0 - }, - if (isGuest) { - UserManager.USER_TYPE_FULL_GUEST - } else { - UserManager.USER_TYPE_FULL_SYSTEM - }, - ) - } - - companion object { - private val IMMEDIATE = Dispatchers.Main.immediate - private val ICON = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) - private val GUEST_ICON: Drawable = mock() - private const val SUPERVISED_USER_CREATION_APP_PACKAGE = "supervisedUserCreation" - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt index 58f55314c1b6..8fb98c12d6ff 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt @@ -19,51 +19,90 @@ package com.android.systemui.user.domain.interactor import android.app.ActivityManager import android.app.admin.DevicePolicyManager +import android.content.Intent +import android.content.pm.UserInfo +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.os.UserHandle import android.os.UserManager +import android.provider.Settings +import androidx.test.filters.SmallTest +import com.android.internal.R.drawable.ic_account_circle import com.android.internal.logging.UiEventLogger import com.android.systemui.GuestResetOrExitSessionReceiver import com.android.systemui.GuestResumeSessionReceiver +import com.android.systemui.R import com.android.systemui.SysuiTestCase -import com.android.systemui.flags.FakeFeatureFlags -import com.android.systemui.flags.Flags +import com.android.systemui.common.shared.model.Text import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.plugins.ActivityStarter import com.android.systemui.qs.user.UserSwitchDialogController import com.android.systemui.statusbar.policy.DeviceProvisionedController -import com.android.systemui.statusbar.policy.UserSwitcherController import com.android.systemui.telephony.data.repository.FakeTelephonyRepository import com.android.systemui.telephony.domain.interactor.TelephonyInteractor +import com.android.systemui.user.data.model.UserSwitcherSettingsModel import com.android.systemui.user.data.repository.FakeUserRepository +import com.android.systemui.user.data.source.UserRecord +import com.android.systemui.user.domain.model.ShowDialogRequestModel +import com.android.systemui.user.shared.model.UserActionModel +import com.android.systemui.user.shared.model.UserModel +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.kotlinArgumentCaptor +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.TestCoroutineScope +import kotlinx.coroutines.test.advanceUntilIdle +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations -abstract class UserInteractorTest : SysuiTestCase() { +@SmallTest +@RunWith(JUnit4::class) +class UserInteractorTest : SysuiTestCase() { - @Mock protected lateinit var controller: UserSwitcherController - @Mock protected lateinit var activityStarter: ActivityStarter - @Mock protected lateinit var manager: UserManager - @Mock protected lateinit var activityManager: ActivityManager - @Mock protected lateinit var deviceProvisionedController: DeviceProvisionedController - @Mock protected lateinit var devicePolicyManager: DevicePolicyManager - @Mock protected lateinit var uiEventLogger: UiEventLogger - @Mock protected lateinit var dialogShower: UserSwitchDialogController.DialogShower + @Mock private lateinit var activityStarter: ActivityStarter + @Mock private lateinit var manager: UserManager + @Mock private lateinit var activityManager: ActivityManager + @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController + @Mock private lateinit var devicePolicyManager: DevicePolicyManager + @Mock private lateinit var uiEventLogger: UiEventLogger + @Mock private lateinit var dialogShower: UserSwitchDialogController.DialogShower @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver - protected lateinit var underTest: UserInteractor + private lateinit var underTest: UserInteractor - protected lateinit var testCoroutineScope: TestCoroutineScope - protected lateinit var userRepository: FakeUserRepository - protected lateinit var keyguardRepository: FakeKeyguardRepository - protected lateinit var telephonyRepository: FakeTelephonyRepository + private lateinit var testCoroutineScope: TestCoroutineScope + private lateinit var userRepository: FakeUserRepository + private lateinit var keyguardRepository: FakeKeyguardRepository + private lateinit var telephonyRepository: FakeTelephonyRepository - abstract fun isRefactored(): Boolean - - open fun setUp() { + @Before + fun setUp() { MockitoAnnotations.initMocks(this) + whenever(manager.getUserIcon(anyInt())).thenReturn(ICON) + whenever(manager.canAddMoreUsers(any())).thenReturn(true) + + overrideResource(R.drawable.ic_account_circle, GUEST_ICON) + overrideResource(R.dimen.max_avatar_size, 10) + overrideResource( + com.android.internal.R.string.config_supervisedUserCreationPackage, + SUPERVISED_USER_CREATION_APP_PACKAGE, + ) userRepository = FakeUserRepository() keyguardRepository = FakeKeyguardRepository() @@ -79,16 +118,11 @@ abstract class UserInteractorTest : SysuiTestCase() { UserInteractor( applicationContext = context, repository = userRepository, - controller = controller, activityStarter = activityStarter, keyguardInteractor = KeyguardInteractor( repository = keyguardRepository, ), - featureFlags = - FakeFeatureFlags().apply { - set(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER, !isRefactored()) - }, manager = manager, applicationScope = testCoroutineScope, telephonyInteractor = @@ -117,7 +151,665 @@ abstract class UserInteractorTest : SysuiTestCase() { ) } + @Test + fun `onRecordSelected - user`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 3, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + underTest.onRecordSelected(UserRecord(info = userInfos[1]), dialogShower) + + verify(dialogShower).dismiss() + verify(activityManager).switchUser(userInfos[1].id) + Unit + } + + @Test + fun `onRecordSelected - switch to guest user`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 3, includeGuest = true) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + underTest.onRecordSelected(UserRecord(info = userInfos.last())) + + verify(activityManager).switchUser(userInfos.last().id) + Unit + } + + @Test + fun `onRecordSelected - enter guest mode`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 3, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + val guestUserInfo = createUserInfo(id = 1337, name = "guest", isGuest = true) + whenever(manager.createGuest(any())).thenReturn(guestUserInfo) + + underTest.onRecordSelected(UserRecord(isGuest = true), dialogShower) + + verify(dialogShower).dismiss() + verify(manager).createGuest(any()) + Unit + } + + @Test + fun `onRecordSelected - action`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 3, includeGuest = true) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + underTest.onRecordSelected(UserRecord(isAddSupervisedUser = true), dialogShower) + + verify(dialogShower, never()).dismiss() + verify(activityStarter).startActivity(any(), anyBoolean()) + } + + @Test + fun `users - switcher enabled`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 3, includeGuest = true) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + var value: List<UserModel>? = null + val job = underTest.users.onEach { value = it }.launchIn(this) + assertUsers(models = value, count = 3, includeGuest = true) + + job.cancel() + } + + @Test + fun `users - switches to second user`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + var value: List<UserModel>? = null + val job = underTest.users.onEach { value = it }.launchIn(this) + userRepository.setSelectedUserInfo(userInfos[1]) + + assertUsers(models = value, count = 2, selectedIndex = 1) + job.cancel() + } + + @Test + fun `users - switcher not enabled`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = false)) + + var value: List<UserModel>? = null + val job = underTest.users.onEach { value = it }.launchIn(this) + assertUsers(models = value, count = 1) + + job.cancel() + } + + @Test + fun selectedUser() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + var value: UserModel? = null + val job = underTest.selectedUser.onEach { value = it }.launchIn(this) + assertUser(value, id = 0, isSelected = true) + + userRepository.setSelectedUserInfo(userInfos[1]) + assertUser(value, id = 1, isSelected = true) + + job.cancel() + } + + @Test + fun `actions - device unlocked`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + keyguardRepository.setKeyguardShowing(false) + var value: List<UserActionModel>? = null + val job = underTest.actions.onEach { value = it }.launchIn(this) + + assertThat(value) + .isEqualTo( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, + ) + ) + + job.cancel() + } + + @Test + fun `actions - device unlocked user not primary - empty list`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[1]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + keyguardRepository.setKeyguardShowing(false) + var value: List<UserActionModel>? = null + val job = underTest.actions.onEach { value = it }.launchIn(this) + + assertThat(value).isEqualTo(emptyList<UserActionModel>()) + + job.cancel() + } + + @Test + fun `actions - device unlocked user is guest - empty list`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = true) + assertThat(userInfos[1].isGuest).isTrue() + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[1]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + keyguardRepository.setKeyguardShowing(false) + var value: List<UserActionModel>? = null + val job = underTest.actions.onEach { value = it }.launchIn(this) + + assertThat(value).isEqualTo(emptyList<UserActionModel>()) + + job.cancel() + } + + @Test + fun `actions - device locked add from lockscreen set - full list`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings( + UserSwitcherSettingsModel( + isUserSwitcherEnabled = true, + isAddUsersFromLockscreen = true, + ) + ) + keyguardRepository.setKeyguardShowing(false) + var value: List<UserActionModel>? = null + val job = underTest.actions.onEach { value = it }.launchIn(this) + + assertThat(value) + .isEqualTo( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, + ) + ) + + job.cancel() + } + + @Test + fun `actions - device locked - only guest action and manage user is shown`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + keyguardRepository.setKeyguardShowing(true) + var value: List<UserActionModel>? = null + val job = underTest.actions.onEach { value = it }.launchIn(this) + + assertThat(value) + .isEqualTo( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT + ) + ) + + job.cancel() + } + + @Test + fun `executeAction - add user - dialog shown`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + keyguardRepository.setKeyguardShowing(false) + var dialogRequest: ShowDialogRequestModel? = null + val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) + val dialogShower: UserSwitchDialogController.DialogShower = mock() + + underTest.executeAction(UserActionModel.ADD_USER, dialogShower) + assertThat(dialogRequest) + .isEqualTo( + ShowDialogRequestModel.ShowAddUserDialog( + userHandle = userInfos[0].userHandle, + isKeyguardShowing = false, + showEphemeralMessage = false, + dialogShower = dialogShower, + ) + ) + + underTest.onDialogShown() + assertThat(dialogRequest).isNull() + + job.cancel() + } + + @Test + fun `executeAction - add supervised user - starts activity`() = + runBlocking(IMMEDIATE) { + underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER) + + val intentCaptor = kotlinArgumentCaptor<Intent>() + verify(activityStarter).startActivity(intentCaptor.capture(), eq(true)) + assertThat(intentCaptor.value.action) + .isEqualTo(UserManager.ACTION_CREATE_SUPERVISED_USER) + assertThat(intentCaptor.value.`package`).isEqualTo(SUPERVISED_USER_CREATION_APP_PACKAGE) + } + + @Test + fun `executeAction - navigate to manage users`() = + runBlocking(IMMEDIATE) { + underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) + + val intentCaptor = kotlinArgumentCaptor<Intent>() + verify(activityStarter).startActivity(intentCaptor.capture(), eq(true)) + assertThat(intentCaptor.value.action).isEqualTo(Settings.ACTION_USER_SETTINGS) + } + + @Test + fun `executeAction - guest mode`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + val guestUserInfo = createUserInfo(id = 1337, name = "guest", isGuest = true) + whenever(manager.createGuest(any())).thenReturn(guestUserInfo) + val dialogRequests = mutableListOf<ShowDialogRequestModel?>() + val showDialogsJob = + underTest.dialogShowRequests + .onEach { + dialogRequests.add(it) + if (it != null) { + underTest.onDialogShown() + } + } + .launchIn(this) + val dismissDialogsJob = + underTest.dialogDismissRequests + .onEach { + if (it != null) { + underTest.onDialogDismissed() + } + } + .launchIn(this) + + underTest.executeAction(UserActionModel.ENTER_GUEST_MODE) + + assertThat(dialogRequests) + .contains( + ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true), + ) + verify(activityManager).switchUser(guestUserInfo.id) + + showDialogsJob.cancel() + dismissDialogsJob.cancel() + } + + @Test + fun `selectUser - already selected guest re-selected - exit guest dialog`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = true) + val guestUserInfo = userInfos[1] + assertThat(guestUserInfo.isGuest).isTrue() + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(guestUserInfo) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + var dialogRequest: ShowDialogRequestModel? = null + val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) + + underTest.selectUser( + newlySelectedUserId = guestUserInfo.id, + dialogShower = dialogShower, + ) + + assertThat(dialogRequest) + .isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java) + verify(dialogShower, never()).dismiss() + job.cancel() + } + + @Test + fun `selectUser - currently guest non-guest selected - exit guest dialog`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = true) + val guestUserInfo = userInfos[1] + assertThat(guestUserInfo.isGuest).isTrue() + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(guestUserInfo) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + var dialogRequest: ShowDialogRequestModel? = null + val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) + + underTest.selectUser(newlySelectedUserId = userInfos[0].id, dialogShower = dialogShower) + + assertThat(dialogRequest) + .isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java) + verify(dialogShower, never()).dismiss() + job.cancel() + } + + @Test + fun `selectUser - not currently guest - switches users`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + var dialogRequest: ShowDialogRequestModel? = null + val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) + + underTest.selectUser(newlySelectedUserId = userInfos[1].id, dialogShower = dialogShower) + + assertThat(dialogRequest).isNull() + verify(activityManager).switchUser(userInfos[1].id) + verify(dialogShower).dismiss() + job.cancel() + } + + @Test + fun `Telephony call state changes - refreshes users`() = + runBlocking(IMMEDIATE) { + val refreshUsersCallCount = userRepository.refreshUsersCallCount + + telephonyRepository.setCallState(1) + + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) + } + + @Test + fun `User switched broadcast`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + val callback1: UserInteractor.UserCallback = mock() + val callback2: UserInteractor.UserCallback = mock() + underTest.addCallback(callback1) + underTest.addCallback(callback2) + val refreshUsersCallCount = userRepository.refreshUsersCallCount + + userRepository.setSelectedUserInfo(userInfos[1]) + fakeBroadcastDispatcher.registeredReceivers.forEach { + it.onReceive( + context, + Intent(Intent.ACTION_USER_SWITCHED) + .putExtra(Intent.EXTRA_USER_HANDLE, userInfos[1].id), + ) + } + + verify(callback1).onUserStateChanged() + verify(callback2).onUserStateChanged() + assertThat(userRepository.secondaryUserId).isEqualTo(userInfos[1].id) + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) + } + + @Test + fun `User info changed broadcast`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + val refreshUsersCallCount = userRepository.refreshUsersCallCount + + fakeBroadcastDispatcher.registeredReceivers.forEach { + it.onReceive( + context, + Intent(Intent.ACTION_USER_INFO_CHANGED), + ) + } + + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) + } + + @Test + fun `System user unlocked broadcast - refresh users`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + val refreshUsersCallCount = userRepository.refreshUsersCallCount + + fakeBroadcastDispatcher.registeredReceivers.forEach { + it.onReceive( + context, + Intent(Intent.ACTION_USER_UNLOCKED) + .putExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_SYSTEM), + ) + } + + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) + } + + @Test + fun `Non-system user unlocked broadcast - do not refresh users`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + val refreshUsersCallCount = userRepository.refreshUsersCallCount + + fakeBroadcastDispatcher.registeredReceivers.forEach { + it.onReceive( + context, + Intent(Intent.ACTION_USER_UNLOCKED).putExtra(Intent.EXTRA_USER_HANDLE, 1337), + ) + } + + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount) + } + + @Test + fun userRecords() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 3, includeGuest = false) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + keyguardRepository.setKeyguardShowing(false) + + testCoroutineScope.advanceUntilIdle() + + assertRecords( + records = underTest.userRecords.value, + userIds = listOf(0, 1, 2), + selectedUserIndex = 0, + includeGuest = false, + expectedActions = + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, + ), + ) + } + + @Test + fun selectedUserRecord() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 3, includeGuest = true) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + keyguardRepository.setKeyguardShowing(false) + + assertRecordForUser( + record = underTest.selectedUserRecord.value, + id = 0, + hasPicture = true, + isCurrent = true, + isSwitchToEnabled = true, + ) + } + + private fun assertUsers( + models: List<UserModel>?, + count: Int, + selectedIndex: Int = 0, + includeGuest: Boolean = false, + ) { + checkNotNull(models) + assertThat(models.size).isEqualTo(count) + models.forEachIndexed { index, model -> + assertUser( + model = model, + id = index, + isSelected = index == selectedIndex, + isGuest = includeGuest && index == count - 1 + ) + } + } + + private fun assertUser( + model: UserModel?, + id: Int, + isSelected: Boolean = false, + isGuest: Boolean = false, + ) { + checkNotNull(model) + assertThat(model.id).isEqualTo(id) + assertThat(model.name).isEqualTo(Text.Loaded(if (isGuest) "guest" else "user_$id")) + assertThat(model.isSelected).isEqualTo(isSelected) + assertThat(model.isSelectable).isTrue() + assertThat(model.isGuest).isEqualTo(isGuest) + } + + private fun assertRecords( + records: List<UserRecord>, + userIds: List<Int>, + selectedUserIndex: Int = 0, + includeGuest: Boolean = false, + expectedActions: List<UserActionModel> = emptyList(), + ) { + assertThat(records.size >= userIds.size).isTrue() + userIds.indices.forEach { userIndex -> + val record = records[userIndex] + assertThat(record.info).isNotNull() + val isGuest = includeGuest && userIndex == userIds.size - 1 + assertRecordForUser( + record = record, + id = userIds[userIndex], + hasPicture = !isGuest, + isCurrent = userIndex == selectedUserIndex, + isGuest = isGuest, + isSwitchToEnabled = true, + ) + } + + assertThat(records.size - userIds.size).isEqualTo(expectedActions.size) + (userIds.size until userIds.size + expectedActions.size).forEach { actionIndex -> + val record = records[actionIndex] + assertThat(record.info).isNull() + assertRecordForAction( + record = record, + type = expectedActions[actionIndex - userIds.size], + ) + } + } + + private fun assertRecordForUser( + record: UserRecord?, + id: Int? = null, + hasPicture: Boolean = false, + isCurrent: Boolean = false, + isGuest: Boolean = false, + isSwitchToEnabled: Boolean = false, + ) { + checkNotNull(record) + assertThat(record.info?.id).isEqualTo(id) + assertThat(record.picture != null).isEqualTo(hasPicture) + assertThat(record.isCurrent).isEqualTo(isCurrent) + assertThat(record.isGuest).isEqualTo(isGuest) + assertThat(record.isSwitchToEnabled).isEqualTo(isSwitchToEnabled) + } + + private fun assertRecordForAction( + record: UserRecord, + type: UserActionModel, + ) { + assertThat(record.isGuest).isEqualTo(type == UserActionModel.ENTER_GUEST_MODE) + assertThat(record.isAddUser).isEqualTo(type == UserActionModel.ADD_USER) + assertThat(record.isAddSupervisedUser) + .isEqualTo(type == UserActionModel.ADD_SUPERVISED_USER) + } + + private fun createUserInfos( + count: Int, + includeGuest: Boolean, + ): List<UserInfo> { + return (0 until count).map { index -> + val isGuest = includeGuest && index == count - 1 + createUserInfo( + id = index, + name = + if (isGuest) { + "guest" + } else { + "user_$index" + }, + isPrimary = !isGuest && index == 0, + isGuest = isGuest, + ) + } + } + + private fun createUserInfo( + id: Int, + name: String, + isPrimary: Boolean = false, + isGuest: Boolean = false, + ): UserInfo { + return UserInfo( + id, + name, + /* iconPath= */ "", + /* flags= */ if (isPrimary) { + UserInfo.FLAG_PRIMARY or UserInfo.FLAG_ADMIN + } else { + 0 + }, + if (isGuest) { + UserManager.USER_TYPE_FULL_GUEST + } else { + UserManager.USER_TYPE_FULL_SYSTEM + }, + ) + } + companion object { private val IMMEDIATE = Dispatchers.Main.immediate + private val ICON = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) + private val GUEST_ICON: Drawable = mock() + private const val SUPERVISED_USER_CREATION_APP_PACKAGE = "supervisedUserCreation" } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt deleted file mode 100644 index 6a17c8ddc63d..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package com.android.systemui.user.domain.interactor - -import androidx.test.filters.SmallTest -import com.android.systemui.user.shared.model.UserActionModel -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.eq -import com.android.systemui.util.mockito.nullable -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -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.Mockito.anyBoolean -import org.mockito.Mockito.verify - -@SmallTest -@RunWith(JUnit4::class) -open class UserInteractorUnrefactoredTest : UserInteractorTest() { - - override fun isRefactored(): Boolean { - return false - } - - @Before - override fun setUp() { - super.setUp() - } - - @Test - fun `actions - not actionable when locked and locked - no actions`() = - runBlocking(IMMEDIATE) { - userRepository.setActions(UserActionModel.values().toList()) - userRepository.setActionableWhenLocked(false) - keyguardRepository.setKeyguardShowing(true) - - var actions: List<UserActionModel>? = null - val job = underTest.actions.onEach { actions = it }.launchIn(this) - - assertThat(actions).isEmpty() - job.cancel() - } - - @Test - fun `actions - not actionable when locked and not locked`() = - runBlocking(IMMEDIATE) { - setActions() - userRepository.setActionableWhenLocked(false) - keyguardRepository.setKeyguardShowing(false) - - var actions: List<UserActionModel>? = null - val job = underTest.actions.onEach { actions = it }.launchIn(this) - - assertThat(actions) - .isEqualTo( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, - ) - ) - job.cancel() - } - - @Test - fun `actions - actionable when locked and not locked`() = - runBlocking(IMMEDIATE) { - setActions() - userRepository.setActionableWhenLocked(true) - keyguardRepository.setKeyguardShowing(false) - - var actions: List<UserActionModel>? = null - val job = underTest.actions.onEach { actions = it }.launchIn(this) - - assertThat(actions) - .isEqualTo( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, - ) - ) - job.cancel() - } - - @Test - fun `actions - actionable when locked and locked`() = - runBlocking(IMMEDIATE) { - setActions() - userRepository.setActionableWhenLocked(true) - keyguardRepository.setKeyguardShowing(true) - - var actions: List<UserActionModel>? = null - val job = underTest.actions.onEach { actions = it }.launchIn(this) - - assertThat(actions) - .isEqualTo( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, - ) - ) - job.cancel() - } - - @Test - fun selectUser() { - val userId = 3 - - underTest.selectUser(userId) - - verify(controller).onUserSelected(eq(userId), nullable()) - } - - @Test - fun `executeAction - guest`() { - underTest.executeAction(UserActionModel.ENTER_GUEST_MODE) - - verify(controller).createAndSwitchToGuestUser(nullable()) - } - - @Test - fun `executeAction - add user`() { - underTest.executeAction(UserActionModel.ADD_USER) - - verify(controller).showAddUserDialog(nullable()) - } - - @Test - fun `executeAction - add supervised user`() { - underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER) - - verify(controller).startSupervisedUserActivity() - } - - @Test - fun `executeAction - manage users`() { - underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) - - verify(activityStarter).startActivity(any(), anyBoolean()) - } - - private fun setActions() { - userRepository.setActions(UserActionModel.values().toList()) - } - - companion object { - private val IMMEDIATE = Dispatchers.Main.immediate - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt index 116023aca655..db136800a3cc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt @@ -19,7 +19,7 @@ package com.android.systemui.user.ui.viewmodel import android.app.ActivityManager import android.app.admin.DevicePolicyManager -import android.graphics.drawable.Drawable +import android.content.pm.UserInfo import android.os.UserManager import androidx.test.filters.SmallTest import com.android.internal.logging.UiEventLogger @@ -27,32 +27,37 @@ import com.android.systemui.GuestResetOrExitSessionReceiver import com.android.systemui.GuestResumeSessionReceiver import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.Text -import com.android.systemui.flags.FakeFeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.plugins.ActivityStarter import com.android.systemui.power.data.repository.FakePowerRepository import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.statusbar.policy.DeviceProvisionedController -import com.android.systemui.statusbar.policy.UserSwitcherController import com.android.systemui.telephony.data.repository.FakeTelephonyRepository import com.android.systemui.telephony.domain.interactor.TelephonyInteractor +import com.android.systemui.user.data.model.UserSwitcherSettingsModel import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.user.domain.interactor.GuestUserInteractor import com.android.systemui.user.domain.interactor.RefreshUsersScheduler import com.android.systemui.user.domain.interactor.UserInteractor import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper import com.android.systemui.user.shared.model.UserActionModel -import com.android.systemui.user.shared.model.UserModel -import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.TestCoroutineScope -import kotlinx.coroutines.yield +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.TestResult +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -60,11 +65,11 @@ import org.junit.runners.JUnit4 import org.mockito.Mock import org.mockito.MockitoAnnotations +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(JUnit4::class) class UserSwitcherViewModelTest : SysuiTestCase() { - @Mock private lateinit var controller: UserSwitcherController @Mock private lateinit var activityStarter: ActivityStarter @Mock private lateinit var activityManager: ActivityManager @Mock private lateinit var manager: UserManager @@ -80,28 +85,47 @@ class UserSwitcherViewModelTest : SysuiTestCase() { private lateinit var keyguardRepository: FakeKeyguardRepository private lateinit var powerRepository: FakePowerRepository + private lateinit var testDispatcher: TestDispatcher + private lateinit var testScope: TestScope + private lateinit var injectedScope: CoroutineScope + @Before fun setUp() { MockitoAnnotations.initMocks(this) + whenever(manager.canAddMoreUsers(any())).thenReturn(true) + whenever(manager.getUserSwitchability(any())) + .thenReturn(UserManager.SWITCHABILITY_STATUS_OK) + overrideResource( + com.android.internal.R.string.config_supervisedUserCreationPackage, + SUPERVISED_USER_CREATION_PACKAGE, + ) + testDispatcher = UnconfinedTestDispatcher() + testScope = TestScope(testDispatcher) + injectedScope = CoroutineScope(testScope.coroutineContext + SupervisorJob()) userRepository = FakeUserRepository() + runBlocking { + userRepository.setSettings( + UserSwitcherSettingsModel( + isUserSwitcherEnabled = true, + ) + ) + } + keyguardRepository = FakeKeyguardRepository() powerRepository = FakePowerRepository() - val featureFlags = FakeFeatureFlags() - featureFlags.set(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER, true) - val scope = TestCoroutineScope() val refreshUsersScheduler = RefreshUsersScheduler( - applicationScope = scope, - mainDispatcher = IMMEDIATE, + applicationScope = injectedScope, + mainDispatcher = testDispatcher, repository = userRepository, ) val guestUserInteractor = GuestUserInteractor( applicationContext = context, - applicationScope = scope, - mainDispatcher = IMMEDIATE, - backgroundDispatcher = IMMEDIATE, + applicationScope = injectedScope, + mainDispatcher = testDispatcher, + backgroundDispatcher = testDispatcher, manager = manager, repository = userRepository, deviceProvisionedController = deviceProvisionedController, @@ -118,21 +142,19 @@ class UserSwitcherViewModelTest : SysuiTestCase() { UserInteractor( applicationContext = context, repository = userRepository, - controller = controller, activityStarter = activityStarter, keyguardInteractor = KeyguardInteractor( repository = keyguardRepository, ), - featureFlags = featureFlags, manager = manager, - applicationScope = scope, + applicationScope = injectedScope, telephonyInteractor = TelephonyInteractor( repository = FakeTelephonyRepository(), ), broadcastDispatcher = fakeBroadcastDispatcher, - backgroundDispatcher = IMMEDIATE, + backgroundDispatcher = testDispatcher, activityManager = activityManager, refreshUsersScheduler = refreshUsersScheduler, guestUserInteractor = guestUserInteractor, @@ -141,222 +163,216 @@ class UserSwitcherViewModelTest : SysuiTestCase() { PowerInteractor( repository = powerRepository, ), - featureFlags = featureFlags, guestUserInteractor = guestUserInteractor, ) .create(UserSwitcherViewModel::class.java) } @Test - fun users() = - runBlocking(IMMEDIATE) { - userRepository.setUsers( - listOf( - UserModel( - id = 0, - name = Text.Loaded("zero"), - image = USER_IMAGE, - isSelected = true, - isSelectable = true, - isGuest = false, - ), - UserModel( - id = 1, - name = Text.Loaded("one"), - image = USER_IMAGE, - isSelected = false, - isSelectable = true, - isGuest = false, - ), - UserModel( - id = 2, - name = Text.Loaded("two"), - image = USER_IMAGE, - isSelected = false, - isSelectable = false, - isGuest = false, - ), - ) + fun users() = selfCancelingTest { + val userInfos = + listOf( + UserInfo( + /* id= */ 0, + /* name= */ "zero", + /* iconPath= */ "", + /* flags= */ UserInfo.FLAG_PRIMARY or UserInfo.FLAG_ADMIN, + UserManager.USER_TYPE_FULL_SYSTEM, + ), + UserInfo( + /* id= */ 1, + /* name= */ "one", + /* iconPath= */ "", + /* flags= */ 0, + UserManager.USER_TYPE_FULL_SYSTEM, + ), + UserInfo( + /* id= */ 2, + /* name= */ "two", + /* iconPath= */ "", + /* flags= */ 0, + UserManager.USER_TYPE_FULL_SYSTEM, + ), ) - - var userViewModels: List<UserViewModel>? = null - val job = underTest.users.onEach { userViewModels = it }.launchIn(this) - - assertThat(userViewModels).hasSize(3) - assertUserViewModel( - viewModel = userViewModels?.get(0), - viewKey = 0, - name = "zero", - isSelectionMarkerVisible = true, - alpha = LegacyUserUiHelper.USER_SWITCHER_USER_VIEW_SELECTABLE_ALPHA, - isClickable = true, - ) - assertUserViewModel( - viewModel = userViewModels?.get(1), - viewKey = 1, - name = "one", - isSelectionMarkerVisible = false, - alpha = LegacyUserUiHelper.USER_SWITCHER_USER_VIEW_SELECTABLE_ALPHA, - isClickable = true, - ) - assertUserViewModel( - viewModel = userViewModels?.get(2), - viewKey = 2, - name = "two", - isSelectionMarkerVisible = false, - alpha = LegacyUserUiHelper.USER_SWITCHER_USER_VIEW_NOT_SELECTABLE_ALPHA, - isClickable = false, - ) - job.cancel() - } + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + + val userViewModels = mutableListOf<List<UserViewModel>>() + val job = launch(testDispatcher) { underTest.users.toList(userViewModels) } + + assertThat(userViewModels.last()).hasSize(3) + assertUserViewModel( + viewModel = userViewModels.last()[0], + viewKey = 0, + name = "zero", + isSelectionMarkerVisible = true, + ) + assertUserViewModel( + viewModel = userViewModels.last()[1], + viewKey = 1, + name = "one", + isSelectionMarkerVisible = false, + ) + assertUserViewModel( + viewModel = userViewModels.last()[2], + viewKey = 2, + name = "two", + isSelectionMarkerVisible = false, + ) + job.cancel() + } @Test - fun `maximumUserColumns - few users`() = - runBlocking(IMMEDIATE) { - setUsers(count = 2) - var value: Int? = null - val job = underTest.maximumUserColumns.onEach { value = it }.launchIn(this) - - assertThat(value).isEqualTo(4) - job.cancel() - } + fun `maximumUserColumns - few users`() = selfCancelingTest { + setUsers(count = 2) + val values = mutableListOf<Int>() + val job = launch(testDispatcher) { underTest.maximumUserColumns.toList(values) } + + assertThat(values.last()).isEqualTo(4) + + job.cancel() + } @Test - fun `maximumUserColumns - many users`() = - runBlocking(IMMEDIATE) { - setUsers(count = 5) - var value: Int? = null - val job = underTest.maximumUserColumns.onEach { value = it }.launchIn(this) - - assertThat(value).isEqualTo(3) - job.cancel() - } + fun `maximumUserColumns - many users`() = selfCancelingTest { + setUsers(count = 5) + val values = mutableListOf<Int>() + val job = launch(testDispatcher) { underTest.maximumUserColumns.toList(values) } + + assertThat(values.last()).isEqualTo(3) + job.cancel() + } @Test - fun `isOpenMenuButtonVisible - has actions - true`() = - runBlocking(IMMEDIATE) { - userRepository.setActions(UserActionModel.values().toList()) + fun `isOpenMenuButtonVisible - has actions - true`() = selfCancelingTest { + setUsers(2) - var isVisible: Boolean? = null - val job = underTest.isOpenMenuButtonVisible.onEach { isVisible = it }.launchIn(this) + val isVisible = mutableListOf<Boolean>() + val job = launch(testDispatcher) { underTest.isOpenMenuButtonVisible.toList(isVisible) } - assertThat(isVisible).isTrue() - job.cancel() - } + assertThat(isVisible.last()).isTrue() + job.cancel() + } @Test - fun `isOpenMenuButtonVisible - no actions - false`() = - runBlocking(IMMEDIATE) { - userRepository.setActions(emptyList()) + fun `isOpenMenuButtonVisible - no actions - false`() = selfCancelingTest { + val userInfos = setUsers(2) + userRepository.setSelectedUserInfo(userInfos[1]) + keyguardRepository.setKeyguardShowing(true) + whenever(manager.canAddMoreUsers(any())).thenReturn(false) - var isVisible: Boolean? = null - val job = underTest.isOpenMenuButtonVisible.onEach { isVisible = it }.launchIn(this) + val isVisible = mutableListOf<Boolean>() + val job = launch(testDispatcher) { underTest.isOpenMenuButtonVisible.toList(isVisible) } - assertThat(isVisible).isFalse() - job.cancel() - } + assertThat(isVisible.last()).isFalse() + job.cancel() + } @Test - fun menu() = - runBlocking(IMMEDIATE) { - userRepository.setActions(UserActionModel.values().toList()) - var isMenuVisible: Boolean? = null - val job = underTest.isMenuVisible.onEach { isMenuVisible = it }.launchIn(this) - assertThat(isMenuVisible).isFalse() + fun menu() = selfCancelingTest { + val isMenuVisible = mutableListOf<Boolean>() + val job = launch(testDispatcher) { underTest.isMenuVisible.toList(isMenuVisible) } + assertThat(isMenuVisible.last()).isFalse() - underTest.onOpenMenuButtonClicked() - assertThat(isMenuVisible).isTrue() + underTest.onOpenMenuButtonClicked() + assertThat(isMenuVisible.last()).isTrue() - underTest.onMenuClosed() - assertThat(isMenuVisible).isFalse() + underTest.onMenuClosed() + assertThat(isMenuVisible.last()).isFalse() - job.cancel() - } + job.cancel() + } @Test - fun `menu actions`() = - runBlocking(IMMEDIATE) { - userRepository.setActions(UserActionModel.values().toList()) - var actions: List<UserActionViewModel>? = null - val job = underTest.menu.onEach { actions = it }.launchIn(this) - - assertThat(actions?.map { it.viewKey }) - .isEqualTo( - listOf( - UserActionModel.ENTER_GUEST_MODE.ordinal.toLong(), - UserActionModel.ADD_USER.ordinal.toLong(), - UserActionModel.ADD_SUPERVISED_USER.ordinal.toLong(), - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT.ordinal.toLong(), - ) + fun `menu actions`() = selfCancelingTest { + setUsers(2) + val actions = mutableListOf<List<UserActionViewModel>>() + val job = launch(testDispatcher) { underTest.menu.toList(actions) } + + assertThat(actions.last().map { it.viewKey }) + .isEqualTo( + listOf( + UserActionModel.ENTER_GUEST_MODE.ordinal.toLong(), + UserActionModel.ADD_USER.ordinal.toLong(), + UserActionModel.ADD_SUPERVISED_USER.ordinal.toLong(), + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT.ordinal.toLong(), ) + ) - job.cancel() - } + job.cancel() + } @Test - fun `isFinishRequested - finishes when user is switched`() = - runBlocking(IMMEDIATE) { - setUsers(count = 2) - var isFinishRequested: Boolean? = null - val job = underTest.isFinishRequested.onEach { isFinishRequested = it }.launchIn(this) - assertThat(isFinishRequested).isFalse() - - userRepository.setSelectedUser(1) - yield() - assertThat(isFinishRequested).isTrue() - - job.cancel() - } + fun `isFinishRequested - finishes when user is switched`() = selfCancelingTest { + val userInfos = setUsers(count = 2) + val isFinishRequested = mutableListOf<Boolean>() + val job = launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) } + assertThat(isFinishRequested.last()).isFalse() + + userRepository.setSelectedUserInfo(userInfos[1]) + + assertThat(isFinishRequested.last()).isTrue() + + job.cancel() + } @Test - fun `isFinishRequested - finishes when the screen turns off`() = - runBlocking(IMMEDIATE) { - setUsers(count = 2) - powerRepository.setInteractive(true) - var isFinishRequested: Boolean? = null - val job = underTest.isFinishRequested.onEach { isFinishRequested = it }.launchIn(this) - assertThat(isFinishRequested).isFalse() - - powerRepository.setInteractive(false) - yield() - assertThat(isFinishRequested).isTrue() - - job.cancel() - } + fun `isFinishRequested - finishes when the screen turns off`() = selfCancelingTest { + setUsers(count = 2) + powerRepository.setInteractive(true) + val isFinishRequested = mutableListOf<Boolean>() + val job = launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) } + assertThat(isFinishRequested.last()).isFalse() + + powerRepository.setInteractive(false) + + assertThat(isFinishRequested.last()).isTrue() + + job.cancel() + } @Test - fun `isFinishRequested - finishes when cancel button is clicked`() = - runBlocking(IMMEDIATE) { - setUsers(count = 2) - powerRepository.setInteractive(true) - var isFinishRequested: Boolean? = null - val job = underTest.isFinishRequested.onEach { isFinishRequested = it }.launchIn(this) - assertThat(isFinishRequested).isFalse() - - underTest.onCancelButtonClicked() - yield() - assertThat(isFinishRequested).isTrue() - - underTest.onFinished() - yield() - assertThat(isFinishRequested).isFalse() - - job.cancel() - } + fun `isFinishRequested - finishes when cancel button is clicked`() = selfCancelingTest { + setUsers(count = 2) + powerRepository.setInteractive(true) + val isFinishRequested = mutableListOf<Boolean>() + val job = launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) } + assertThat(isFinishRequested.last()).isFalse() + + underTest.onCancelButtonClicked() + + assertThat(isFinishRequested.last()).isTrue() + + underTest.onFinished() + + assertThat(isFinishRequested.last()).isFalse() - private suspend fun setUsers(count: Int) { - userRepository.setUsers( + job.cancel() + } + + private suspend fun setUsers(count: Int): List<UserInfo> { + val userInfos = (0 until count).map { index -> - UserModel( - id = index, - name = Text.Loaded("$index"), - image = USER_IMAGE, - isSelected = index == 0, - isSelectable = true, - isGuest = false, + UserInfo( + /* id= */ index, + /* name= */ "$index", + /* iconPath= */ "", + /* flags= */ if (index == 0) { + // This is the primary user. + UserInfo.FLAG_PRIMARY or UserInfo.FLAG_ADMIN + } else { + // This isn't the primary user. + 0 + }, + UserManager.USER_TYPE_FULL_SYSTEM, ) } - ) + userRepository.setUserInfos(userInfos) + + if (userInfos.isNotEmpty()) { + userRepository.setSelectedUserInfo(userInfos[0]) + } + return userInfos } private fun assertUserViewModel( @@ -364,19 +380,25 @@ class UserSwitcherViewModelTest : SysuiTestCase() { viewKey: Int, name: String, isSelectionMarkerVisible: Boolean, - alpha: Float, - isClickable: Boolean, ) { checkNotNull(viewModel) assertThat(viewModel.viewKey).isEqualTo(viewKey) assertThat(viewModel.name).isEqualTo(Text.Loaded(name)) assertThat(viewModel.isSelectionMarkerVisible).isEqualTo(isSelectionMarkerVisible) - assertThat(viewModel.alpha).isEqualTo(alpha) - assertThat(viewModel.onClicked != null).isEqualTo(isClickable) + assertThat(viewModel.alpha) + .isEqualTo(LegacyUserUiHelper.USER_SWITCHER_USER_VIEW_SELECTABLE_ALPHA) + assertThat(viewModel.onClicked).isNotNull() } + private fun selfCancelingTest( + block: suspend TestScope.() -> Unit, + ): TestResult = + testScope.runTest { + block() + injectedScope.coroutineContext[Job.Key]?.cancelAndJoin() + } + companion object { - private val IMMEDIATE = Dispatchers.Main.immediate - private val USER_IMAGE = mock<Drawable>() + private const val SUPERVISED_USER_CREATION_PACKAGE = "com.some.package" } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java index d42f757e16bd..db3b9b5c8f4e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -1331,7 +1331,7 @@ public class BubblesTest extends SysuiTestCase { spyOn(mContext); mBubbleController.updateBubble(mBubbleEntry); verify(mContext).registerReceiver(mBroadcastReceiverArgumentCaptor.capture(), - mFilterArgumentCaptor.capture()); + mFilterArgumentCaptor.capture(), eq(Context.RECEIVER_EXPORTED)); assertThat(mFilterArgumentCaptor.getValue().getAction(0)).isEqualTo( Intent.ACTION_CLOSE_SYSTEM_DIALOGS); assertThat(mFilterArgumentCaptor.getValue().getAction(1)).isEqualTo( @@ -1351,7 +1351,7 @@ public class BubblesTest extends SysuiTestCase { mBubbleController.updateBubble(mBubbleEntry); mBubbleData.setExpanded(true); verify(mContext).registerReceiver(mBroadcastReceiverArgumentCaptor.capture(), - mFilterArgumentCaptor.capture()); + mFilterArgumentCaptor.capture(), eq(Context.RECEIVER_EXPORTED)); Intent i = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); mBroadcastReceiverArgumentCaptor.getValue().onReceive(mContext, i); @@ -1365,7 +1365,7 @@ public class BubblesTest extends SysuiTestCase { mBubbleData.setExpanded(true); verify(mContext).registerReceiver(mBroadcastReceiverArgumentCaptor.capture(), - mFilterArgumentCaptor.capture()); + mFilterArgumentCaptor.capture(), eq(Context.RECEIVER_EXPORTED)); Intent i = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); i.putExtra("reason", "gestureNav"); mBroadcastReceiverArgumentCaptor.getValue().onReceive(mContext, i); @@ -1379,7 +1379,7 @@ public class BubblesTest extends SysuiTestCase { mBubbleData.setExpanded(true); verify(mContext).registerReceiver(mBroadcastReceiverArgumentCaptor.capture(), - mFilterArgumentCaptor.capture()); + mFilterArgumentCaptor.capture(), eq(Context.RECEIVER_EXPORTED)); Intent i = new Intent(Intent.ACTION_SCREEN_OFF); mBroadcastReceiverArgumentCaptor.getValue().onReceive(mContext, i); diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt index 4df8aa42ea2f..b7c8cbf40bea 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt @@ -20,26 +20,15 @@ package com.android.systemui.user.data.repository import android.content.pm.UserInfo import android.os.UserHandle import com.android.systemui.user.data.model.UserSwitcherSettingsModel -import com.android.systemui.user.shared.model.UserActionModel -import com.android.systemui.user.shared.model.UserModel import java.util.concurrent.atomic.AtomicBoolean import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.map import kotlinx.coroutines.yield class FakeUserRepository : UserRepository { - private val _users = MutableStateFlow<List<UserModel>>(emptyList()) - override val users: Flow<List<UserModel>> = _users.asStateFlow() - override val selectedUser: Flow<UserModel> = - users.map { models -> models.first { model -> model.isSelected } } - - private val _actions = MutableStateFlow<List<UserActionModel>>(emptyList()) - override val actions: Flow<List<UserActionModel>> = _actions.asStateFlow() - private val _userSwitcherSettings = MutableStateFlow(UserSwitcherSettingsModel()) override val userSwitcherSettings: Flow<UserSwitcherSettingsModel> = _userSwitcherSettings.asStateFlow() @@ -52,9 +41,6 @@ class FakeUserRepository : UserRepository { override var lastSelectedNonGuestUserId: Int = UserHandle.USER_SYSTEM - private val _isActionableWhenLocked = MutableStateFlow(false) - override val isActionableWhenLocked: Flow<Boolean> = _isActionableWhenLocked.asStateFlow() - private var _isGuestUserAutoCreated: Boolean = false override val isGuestUserAutoCreated: Boolean get() = _isGuestUserAutoCreated @@ -100,35 +86,6 @@ class FakeUserRepository : UserRepository { yield() } - fun setUsers(models: List<UserModel>) { - _users.value = models - } - - suspend fun setSelectedUser(userId: Int) { - check(_users.value.find { it.id == userId } != null) { - "Cannot select a user with ID $userId - no user with that ID found!" - } - - setUsers( - _users.value.map { model -> - when { - model.isSelected && model.id != userId -> model.copy(isSelected = false) - !model.isSelected && model.id == userId -> model.copy(isSelected = true) - else -> model - } - } - ) - yield() - } - - fun setActions(models: List<UserActionModel>) { - _actions.value = models - } - - fun setActionableWhenLocked(value: Boolean) { - _isActionableWhenLocked.value = value - } - fun setGuestUserAutoCreated(value: Boolean) { _isGuestUserAutoCreated = value } diff --git a/packages/VpnDialogs/Android.bp b/packages/VpnDialogs/Android.bp index 05135b2bebf9..e4f80e22694b 100644 --- a/packages/VpnDialogs/Android.bp +++ b/packages/VpnDialogs/Android.bp @@ -23,10 +23,15 @@ package { 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 f971a0916837..28e7272853c4 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 fb2367843fc1..a98d6d8e0217 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.view.View; 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 @@ public class ConfirmDialog extends AlertActivity 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 @@ public class ConfirmDialog extends AlertActivity 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 @@ public class ConfirmDialog extends AlertActivity 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 @@ public class ConfirmDialog extends AlertActivity 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 000000000000..68639bd1c4fe --- /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 000000000000..f26c1fecb4e3 --- /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 000000000000..7cfa466ac961 --- /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/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java index f35de17088d1..c77b59717678 100644 --- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java +++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java @@ -16,6 +16,7 @@ package com.android.server.accessibility; +import static android.accessibilityservice.AccessibilityService.ACCESSIBILITY_TAKE_SCREENSHOT_REQUEST_INTERVAL_TIMES_MS; import static android.accessibilityservice.AccessibilityService.KEY_ACCESSIBILITY_SCREENSHOT_COLORSPACE; import static android.accessibilityservice.AccessibilityService.KEY_ACCESSIBILITY_SCREENSHOT_HARDWAREBUFFER; import static android.accessibilityservice.AccessibilityService.KEY_ACCESSIBILITY_SCREENSHOT_STATUS; @@ -83,6 +84,7 @@ import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityWindowInfo; import android.view.accessibility.IAccessibilityInteractionConnectionCallback; import android.view.inputmethod.EditorInfo; +import android.window.ScreenCapture; import android.window.ScreenCapture.ScreenshotHardwareBuffer; import com.android.internal.annotations.GuardedBy; @@ -211,6 +213,11 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ /** The timestamp of requesting to take screenshot in milliseconds */ private long mRequestTakeScreenshotTimestampMs; + /** + * The timestamps of requesting to take a window screenshot in milliseconds, + * mapping from accessibility window id -> timestamp. + */ + private SparseArray<Long> mRequestTakeScreenshotOfWindowTimestampMs = new SparseArray<>(); public interface SystemSupport { /** @@ -1252,6 +1259,51 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ } @Override + public void takeScreenshotOfWindow(int accessibilityWindowId, int interactionId, + ScreenCapture.ScreenCaptureListener listener, + IAccessibilityInteractionConnectionCallback callback) throws RemoteException { + final long currentTimestamp = SystemClock.uptimeMillis(); + if ((currentTimestamp + - mRequestTakeScreenshotOfWindowTimestampMs.get(accessibilityWindowId, 0L)) + <= ACCESSIBILITY_TAKE_SCREENSHOT_REQUEST_INTERVAL_TIMES_MS) { + callback.sendTakeScreenshotOfWindowError( + AccessibilityService.ERROR_TAKE_SCREENSHOT_INTERVAL_TIME_SHORT, interactionId); + return; + } + mRequestTakeScreenshotOfWindowTimestampMs.put(accessibilityWindowId, currentTimestamp); + + synchronized (mLock) { + if (!hasRightsToCurrentUserLocked()) { + callback.sendTakeScreenshotOfWindowError( + AccessibilityService.ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR, interactionId); + return; + } + if (!mSecurityPolicy.canTakeScreenshotLocked(this)) { + callback.sendTakeScreenshotOfWindowError( + AccessibilityService.ERROR_TAKE_SCREENSHOT_NO_ACCESSIBILITY_ACCESS, + interactionId); + return; + } + } + if (!mSecurityPolicy.checkAccessibilityAccess(this)) { + callback.sendTakeScreenshotOfWindowError( + AccessibilityService.ERROR_TAKE_SCREENSHOT_NO_ACCESSIBILITY_ACCESS, + interactionId); + return; + } + + RemoteAccessibilityConnection connection = mA11yWindowManager.getConnectionLocked( + mSystemSupport.getCurrentUserIdLocked(), + resolveAccessibilityWindowIdLocked(accessibilityWindowId)); + if (connection == null) { + callback.sendTakeScreenshotOfWindowError( + AccessibilityService.ERROR_TAKE_SCREENSHOT_INVALID_WINDOW, interactionId); + return; + } + connection.getRemote().takeScreenshotOfWindow(interactionId, listener, callback); + } + + @Override public void takeScreenshot(int displayId, RemoteCallback callback) { if (svcConnTracingEnabled()) { logTraceSvcConn("takeScreenshot", "displayId=" + displayId + ";callback=" + callback); diff --git a/services/accessibility/java/com/android/server/accessibility/ActionReplacingCallback.java b/services/accessibility/java/com/android/server/accessibility/ActionReplacingCallback.java index 6958b667da37..c08b6ab56f55 100644 --- a/services/accessibility/java/com/android/server/accessibility/ActionReplacingCallback.java +++ b/services/accessibility/java/com/android/server/accessibility/ActionReplacingCallback.java @@ -167,6 +167,12 @@ public class ActionReplacingCallback extends IAccessibilityInteractionConnection mServiceCallback.setPerformAccessibilityActionResult(succeeded, interactionId); } + @Override + public void sendTakeScreenshotOfWindowError(int errorCode, int interactionId) + throws RemoteException { + mServiceCallback.sendTakeScreenshotOfWindowError(errorCode, interactionId); + } + private void replaceInfoActionsAndCallService() { final AccessibilityNodeInfo nodeToReturn; boolean doCallback = false; diff --git a/services/backup/java/com/android/server/backup/BackupAndRestoreFeatureFlags.java b/services/backup/java/com/android/server/backup/BackupAndRestoreFeatureFlags.java new file mode 100644 index 000000000000..042bcbd0a0bb --- /dev/null +++ b/services/backup/java/com/android/server/backup/BackupAndRestoreFeatureFlags.java @@ -0,0 +1,55 @@ +/* + * 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.backup; + +import android.Manifest; +import android.annotation.RequiresPermission; +import android.provider.DeviceConfig; + +/** + * Retrieves values of feature flags. + * + * <p>These flags are intended to be configured server-side and their values should be set in {@link + * DeviceConfig} by a service that periodically syncs with the server. + * + * <p>This class must ensure that the namespace, flag name, and default value passed into {@link + * DeviceConfig} matches what's declared on the server. The namespace is shared for all backup and + * restore flags. + */ +public class BackupAndRestoreFeatureFlags { + private static final String NAMESPACE = "backup_and_restore"; + + private BackupAndRestoreFeatureFlags() {} + + /** Retrieves the value of the flag "backup_transport_future_timeout_millis". */ + @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG) + public static long getBackupTransportFutureTimeoutMillis() { + return DeviceConfig.getLong( + NAMESPACE, + /* name= */ "backup_transport_future_timeout_millis", + /* defaultValue= */ 600000); // 10 minutes + } + + /** Retrieves the value of the flag "backup_transport_callback_timeout_millis". */ + @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG) + public static long getBackupTransportCallbackTimeoutMillis() { + return DeviceConfig.getLong( + NAMESPACE, + /* name= */ "backup_transport_callback_timeout_millis", + /* defaultValue= */ 300000); // 5 minutes + } +} diff --git a/services/backup/java/com/android/server/backup/transport/BackupTransportClient.java b/services/backup/java/com/android/server/backup/transport/BackupTransportClient.java index 40d7cad9405a..21005bbf8af9 100644 --- a/services/backup/java/com/android/server/backup/transport/BackupTransportClient.java +++ b/services/backup/java/com/android/server/backup/transport/BackupTransportClient.java @@ -30,6 +30,7 @@ import android.util.Slog; import com.android.internal.backup.IBackupTransport; import com.android.internal.infra.AndroidFuture; +import com.android.server.backup.BackupAndRestoreFeatureFlags; import java.util.ArrayDeque; import java.util.HashSet; @@ -385,7 +386,8 @@ public class BackupTransportClient { private <T> T getFutureResult(AndroidFuture<T> future) { try { - return future.get(600, TimeUnit.SECONDS); + return future.get(BackupAndRestoreFeatureFlags.getBackupTransportFutureTimeoutMillis(), + TimeUnit.MILLISECONDS); } catch (InterruptedException | ExecutionException | TimeoutException | CancellationException e) { Slog.w(TAG, "Failed to get result from transport:", e); diff --git a/services/backup/java/com/android/server/backup/transport/TransportStatusCallback.java b/services/backup/java/com/android/server/backup/transport/TransportStatusCallback.java index fb98825f1343..deaa86c09b36 100644 --- a/services/backup/java/com/android/server/backup/transport/TransportStatusCallback.java +++ b/services/backup/java/com/android/server/backup/transport/TransportStatusCallback.java @@ -23,13 +23,13 @@ import android.util.Slog; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.backup.ITransportStatusCallback; +import com.android.server.backup.BackupAndRestoreFeatureFlags; public class TransportStatusCallback extends ITransportStatusCallback.Stub { private static final String TAG = "TransportStatusCallback"; - private static final int TIMEOUT_MILLIS = 300 * 1000; // 5 minutes. private static final int OPERATION_STATUS_DEFAULT = 0; - private final int mOperationTimeout; + private final long mOperationTimeout; @GuardedBy("this") private int mOperationStatus = OPERATION_STATUS_DEFAULT; @@ -37,7 +37,7 @@ public class TransportStatusCallback extends ITransportStatusCallback.Stub { private boolean mHasCompletedOperation = false; public TransportStatusCallback() { - mOperationTimeout = TIMEOUT_MILLIS; + mOperationTimeout = BackupAndRestoreFeatureFlags.getBackupTransportCallbackTimeoutMillis(); } @VisibleForTesting 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 000bafe1d650..ce7854d7368a 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 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController } /** + * 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 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController @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 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController @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 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController setInterestedWindowFlags(windowFlags, systemWindowFlags); mActivityListener = activityListener; mDeviceProfile = deviceProfile; + mPipBlockedCallback = pipBlockedCallback; mSecureWindowCallback = secureWindowCallback; } @@ -317,6 +329,17 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController } } + @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 5ebbf07526f1..be2107529f8b 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 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub mParams.getBlockedActivities(), mParams.getDefaultActivityPolicy(), createListenerAdapter(), + this::onEnteringPipBlocked, this::onActivityBlocked, this::onSecureWindowShown, mAssociationInfo.getDeviceProfile()); @@ -779,6 +780,11 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub 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 553146d0448d..84f2b63f9775 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -168,7 +168,7 @@ java_library_static { "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/BinaryTransparencyService.java b/services/core/java/com/android/server/BinaryTransparencyService.java index 8055afce8370..2662e03a5627 100644 --- a/services/core/java/com/android/server/BinaryTransparencyService.java +++ b/services/core/java/com/android/server/BinaryTransparencyService.java @@ -685,7 +685,9 @@ public class BinaryTransparencyService extends SystemService { FrameworkStatsLog.write(FrameworkStatsLog.APEX_INFO_GATHERED, packageInfo.packageName, packageInfo.getLongVersionCode(), - mBinaryHashes.get(packageInfo.packageName)); + mBinaryHashes.get(packageInfo.packageName), + 4, // indicating that the digest is SHA256 + null); // TODO: This is to comform to the extended schema. } } } catch (PackageManager.NameNotFoundException e) { diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java index 416de0af84a0..c7c2655a991d 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 @@ public class AccountManagerService } 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 @@ public class AccountManagerService @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/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index d0f245f91d5c..86bb699f07d2 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -23,18 +23,29 @@ import static android.Manifest.permission.START_FOREGROUND_SERVICES_FROM_BACKGRO import static android.app.ActivityManager.PROCESS_STATE_HEAVY_WEIGHT; import static android.app.ActivityManager.PROCESS_STATE_RECEIVER; import static android.app.ActivityManager.PROCESS_STATE_TOP; +import static android.app.ForegroundServiceTypePolicy.FGS_TYPE_POLICY_CHECK_DEPRECATED; +import static android.app.ForegroundServiceTypePolicy.FGS_TYPE_POLICY_CHECK_DISABLED; +import static android.app.ForegroundServiceTypePolicy.FGS_TYPE_POLICY_CHECK_OK; +import static android.app.ForegroundServiceTypePolicy.FGS_TYPE_POLICY_CHECK_PERMISSION_DENIED_ENFORCED; +import static android.app.ForegroundServiceTypePolicy.FGS_TYPE_POLICY_CHECK_PERMISSION_DENIED_PERMISSIVE; +import static android.app.ForegroundServiceTypePolicy.FGS_TYPE_POLICY_CHECK_UNKNOWN; +import static android.content.pm.PackageManager.PERMISSION_DENIED; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST; +import static android.os.PowerExemptionManager.REASON_ACTIVE_DEVICE_ADMIN; import static android.os.PowerExemptionManager.REASON_ACTIVITY_STARTER; import static android.os.PowerExemptionManager.REASON_ACTIVITY_VISIBILITY_GRACE_PERIOD; import static android.os.PowerExemptionManager.REASON_ALLOWLISTED_PACKAGE; import static android.os.PowerExemptionManager.REASON_BACKGROUND_ACTIVITY_PERMISSION; import static android.os.PowerExemptionManager.REASON_BACKGROUND_FGS_PERMISSION; +import static android.os.PowerExemptionManager.REASON_CARRIER_PRIVILEGED_APP; import static android.os.PowerExemptionManager.REASON_COMPANION_DEVICE_MANAGER; import static android.os.PowerExemptionManager.REASON_CURRENT_INPUT_METHOD; import static android.os.PowerExemptionManager.REASON_DENIED; import static android.os.PowerExemptionManager.REASON_DEVICE_DEMO_MODE; import static android.os.PowerExemptionManager.REASON_DEVICE_OWNER; +import static android.os.PowerExemptionManager.REASON_DISALLOW_APPS_CONTROL; +import static android.os.PowerExemptionManager.REASON_DPO_PROTECTED_APP; import static android.os.PowerExemptionManager.REASON_FGS_BINDING; import static android.os.PowerExemptionManager.REASON_INSTR_BACKGROUND_ACTIVITY_PERMISSION; import static android.os.PowerExemptionManager.REASON_INSTR_BACKGROUND_FGS_PERMISSION; @@ -45,10 +56,12 @@ import static android.os.PowerExemptionManager.REASON_PROC_STATE_PERSISTENT; import static android.os.PowerExemptionManager.REASON_PROC_STATE_PERSISTENT_UI; import static android.os.PowerExemptionManager.REASON_PROC_STATE_TOP; import static android.os.PowerExemptionManager.REASON_PROFILE_OWNER; +import static android.os.PowerExemptionManager.REASON_ROLE_EMERGENCY; import static android.os.PowerExemptionManager.REASON_SERVICE_LAUNCH; import static android.os.PowerExemptionManager.REASON_START_ACTIVITY_FLAG; import static android.os.PowerExemptionManager.REASON_SYSTEM_ALERT_WINDOW_PERMISSION; import static android.os.PowerExemptionManager.REASON_SYSTEM_ALLOW_LISTED; +import static android.os.PowerExemptionManager.REASON_SYSTEM_MODULE; import static android.os.PowerExemptionManager.REASON_SYSTEM_UID; import static android.os.PowerExemptionManager.REASON_TEMP_ALLOWED_WHILE_IN_USE; import static android.os.PowerExemptionManager.REASON_UID_VISIBLE; @@ -63,6 +76,9 @@ import static android.os.Process.SYSTEM_UID; import static android.os.Process.ZYGOTE_POLICY_FLAG_EMPTY; import static com.android.internal.messages.nano.SystemMessageProto.SystemMessage.NOTE_FOREGROUND_SERVICE_BG_LAUNCH; +import static com.android.internal.util.FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__DENIED; +import static com.android.internal.util.FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__ENTER; +import static com.android.internal.util.FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__EXIT; import static com.android.internal.util.FrameworkStatsLog.SERVICE_REQUEST_EVENT_REPORTED; import static com.android.internal.util.FrameworkStatsLog.SERVICE_REQUEST_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_COLD; import static com.android.internal.util.FrameworkStatsLog.SERVICE_REQUEST_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_HOT; @@ -94,6 +110,11 @@ import android.app.ActivityThread; import android.app.AppGlobals; import android.app.AppOpsManager; import android.app.ForegroundServiceStartNotAllowedException; +import android.app.ForegroundServiceTypeNotAllowedException; +import android.app.ForegroundServiceTypePolicy; +import android.app.ForegroundServiceTypePolicy.ForegroundServicePolicyCheckCode; +import android.app.ForegroundServiceTypePolicy.ForegroundServiceTypePermission; +import android.app.ForegroundServiceTypePolicy.ForegroundServiceTypePolicyInfo; import android.app.IApplicationThread; import android.app.IForegroundServiceObserver; import android.app.IServiceConnection; @@ -122,6 +143,7 @@ import android.content.pm.PackageManagerInternal; import android.content.pm.ParceledListSlice; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; +import android.content.pm.ServiceInfo.ForegroundServiceType; import android.net.Uri; import android.os.Binder; import android.os.Build; @@ -576,6 +598,7 @@ public final class ActiveServices { getAppStateTracker().addBackgroundRestrictedAppListener(new BackgroundRestrictedListener()); mAppWidgetManagerInternal = LocalServices.getService(AppWidgetManagerInternal.class); setAllowListWhileInUsePermissionInFgs(); + initSystemExemptedFgsTypePermission(); } private AppStateTracker getAppStateTracker() { @@ -724,7 +747,7 @@ public final class ActiveServices { ServiceRecord r = res.record; setFgsRestrictionLocked(callingPackage, callingPid, callingUid, service, r, userId, - allowBackgroundActivityStarts); + allowBackgroundActivityStarts, false /* isBindService */); if (!mAm.mUserController.exists(r.userId)) { Slog.w(TAG, "Trying to start service with non-existent user! " + r.userId); @@ -757,8 +780,8 @@ public final class ActiveServices { Slog.w(TAG, msg); showFgsBgRestrictedNotificationLocked(r); logFGSStateChangeLocked(r, - FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__DENIED, - 0, FGS_STOP_REASON_UNKNOWN); + FOREGROUND_SERVICE_STATE_CHANGED__STATE__DENIED, + 0, FGS_STOP_REASON_UNKNOWN, FGS_TYPE_POLICY_CHECK_UNKNOWN); if (CompatChanges.isChangeEnabled(FGS_START_EXCEPTION_CHANGE_ID, callingUid)) { throw new ForegroundServiceStartNotAllowedException(msg); } @@ -1911,6 +1934,7 @@ public final class ActiveServices { ignoreForeground = true; } + int fgsTypeCheckCode = FGS_TYPE_POLICY_CHECK_UNKNOWN; if (!ignoreForeground) { if (r.mStartForegroundCount == 0) { /* @@ -1931,7 +1955,9 @@ public final class ActiveServices { if (delayMs > mAm.mConstants.mFgsStartForegroundTimeoutMs) { resetFgsRestrictionLocked(r); setFgsRestrictionLocked(r.serviceInfo.packageName, r.app.getPid(), - r.appInfo.uid, r.intent.getIntent(), r, r.userId, false); + r.appInfo.uid, r.intent.getIntent(), r, r.userId, + false /* allowBackgroundActivityStarts */, + false /* isBindService */); final String temp = "startForegroundDelayMs:" + delayMs; if (r.mInfoAllowStartForeground != null) { r.mInfoAllowStartForeground += "; " + temp; @@ -1945,7 +1971,9 @@ public final class ActiveServices { // The second or later time startForeground() is called after service is // started. Check for app state again. setFgsRestrictionLocked(r.serviceInfo.packageName, r.app.getPid(), - r.appInfo.uid, r.intent.getIntent(), r, r.userId, false); + r.appInfo.uid, r.intent.getIntent(), r, r.userId, + false /* allowBackgroundActivityStarts */, + false /* isBindService */); } // If the foreground service is not started from TOP process, do not allow it to // have while-in-use location/camera/microphone access. @@ -1965,13 +1993,49 @@ public final class ActiveServices { updateServiceForegroundLocked(psr, true); ignoreForeground = true; logFGSStateChangeLocked(r, - FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__DENIED, - 0, FGS_STOP_REASON_UNKNOWN); + FOREGROUND_SERVICE_STATE_CHANGED__STATE__DENIED, + 0, FGS_STOP_REASON_UNKNOWN, FGS_TYPE_POLICY_CHECK_UNKNOWN); if (CompatChanges.isChangeEnabled(FGS_START_EXCEPTION_CHANGE_ID, r.appInfo.uid)) { throw new ForegroundServiceStartNotAllowedException(msg); } } + + if (!ignoreForeground) { + Pair<Integer, RuntimeException> fgsTypeResult = null; + if (foregroundServiceType == ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE) { + fgsTypeResult = validateForegroundServiceType(r, + foregroundServiceType, + ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE); + } else { + int fgsTypes = foregroundServiceType; + // If the service has declared some unknown types which might be coming + // from future releases, and if it also comes with the "specialUse", + // then it'll be deemed as the "specialUse" and we ignore this + // unknown type. Otherwise, it'll be treated as an invalid type. + int defaultFgsTypes = (foregroundServiceType + & ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE) != 0 + ? ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE + : ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE; + for (int serviceType = Integer.highestOneBit(fgsTypes); + serviceType != 0; + serviceType = Integer.highestOneBit(fgsTypes)) { + fgsTypeResult = validateForegroundServiceType(r, + serviceType, defaultFgsTypes); + fgsTypes &= ~serviceType; + if (fgsTypeResult.first != FGS_TYPE_POLICY_CHECK_OK) { + break; + } + } + } + fgsTypeCheckCode = fgsTypeResult.first; + if (fgsTypeResult.second != null) { + logFGSStateChangeLocked(r, + FOREGROUND_SERVICE_STATE_CHANGED__STATE__DENIED, + 0, FGS_STOP_REASON_UNKNOWN, fgsTypeResult.first); + throw fgsTypeResult.second; + } + } } // Apps under strict background restrictions simply don't get to have foreground @@ -2040,8 +2104,8 @@ public final class ActiveServices { registerAppOpCallbackLocked(r); mAm.updateForegroundServiceUsageStats(r.name, r.userId, true); logFGSStateChangeLocked(r, - FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__ENTER, - 0, FGS_STOP_REASON_UNKNOWN); + FOREGROUND_SERVICE_STATE_CHANGED__STATE__ENTER, + 0, FGS_STOP_REASON_UNKNOWN, fgsTypeCheckCode); updateNumForegroundServicesLocked(); } // Even if the service is already a FGS, we need to update the notification, @@ -2122,10 +2186,11 @@ public final class ActiveServices { AppOpsManager.OP_START_FOREGROUND, r.appInfo.uid, r.packageName, null); unregisterAppOpCallbackLocked(r); logFGSStateChangeLocked(r, - FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__EXIT, + FOREGROUND_SERVICE_STATE_CHANGED__STATE__EXIT, r.mFgsExitTime > r.mFgsEnterTime ? (int) (r.mFgsExitTime - r.mFgsEnterTime) : 0, - FGS_STOP_REASON_STOP_FOREGROUND); + FGS_STOP_REASON_STOP_FOREGROUND, + FGS_TYPE_POLICY_CHECK_UNKNOWN); r.mFgsNotificationWasDeferred = false; signalForegroundServiceObserversLocked(r); resetFgsRestrictionLocked(r); @@ -2161,6 +2226,118 @@ public final class ActiveServices { return now < eligible; } + /** + * Validate if the given service can start a foreground service with given type. + * + * @return A pair, where the first parameter is the result code and second is the exception + * object if it fails to start a foreground service with given type. + */ + @NonNull + private Pair<Integer, RuntimeException> validateForegroundServiceType(ServiceRecord r, + @ForegroundServiceType int type, + @ForegroundServiceType int defaultToType) { + final ForegroundServiceTypePolicy policy = ForegroundServiceTypePolicy.getDefaultPolicy(); + final ForegroundServiceTypePolicyInfo policyInfo = + policy.getForegroundServiceTypePolicyInfo(type, defaultToType); + final @ForegroundServicePolicyCheckCode int code = policy.checkForegroundServiceTypePolicy( + mAm.mContext, r.packageName, r.app.uid, r.app.getPid(), + r.mAllowWhileInUsePermissionInFgs, policyInfo); + RuntimeException exception = null; + switch (code) { + case FGS_TYPE_POLICY_CHECK_DEPRECATED: { + final String msg = "Starting FGS with type " + + ServiceInfo.foregroundServiceTypeToLabel(type) + + " code=" + code + + " callerApp=" + r.app + + " targetSDK=" + r.app.info.targetSdkVersion; + Slog.wtfQuiet(TAG, msg); + Slog.w(TAG, msg); + } break; + case FGS_TYPE_POLICY_CHECK_DISABLED: { + exception = new ForegroundServiceTypeNotAllowedException( + "Starting FGS with type " + + ServiceInfo.foregroundServiceTypeToLabel(type) + + " callerApp=" + r.app + + " targetSDK=" + r.app.info.targetSdkVersion + + " has been prohibited"); + } break; + case FGS_TYPE_POLICY_CHECK_PERMISSION_DENIED_PERMISSIVE: { + final String msg = "Starting FGS with type " + + ServiceInfo.foregroundServiceTypeToLabel(type) + + " code=" + code + + " callerApp=" + r.app + + " targetSDK=" + r.app.info.targetSdkVersion + + " requiredPermissions=" + policyInfo.toPermissionString(); + Slog.wtfQuiet(TAG, msg); + Slog.w(TAG, msg); + } break; + case FGS_TYPE_POLICY_CHECK_PERMISSION_DENIED_ENFORCED: { + exception = new SecurityException("Starting FGS with type " + + ServiceInfo.foregroundServiceTypeToLabel(type) + + " callerApp=" + r.app + + " targetSDK=" + r.app.info.targetSdkVersion + + " requires permissions: " + + policyInfo.toPermissionString()); + } break; + case FGS_TYPE_POLICY_CHECK_OK: + default: + break; + } + return Pair.create(code, exception); + } + + private class SystemExemptedFgsTypePermission extends ForegroundServiceTypePermission { + SystemExemptedFgsTypePermission() { + super("System exempted"); + } + + @Override + public int checkPermission(@NonNull Context context, int callerUid, int callerPid, + @NonNull String packageName, boolean allowWhileInUse) { + final AppRestrictionController appRestrictionController = mAm.mAppRestrictionController; + @ReasonCode int reason = appRestrictionController + .getPotentialSystemExemptionReason(callerUid); + if (reason == REASON_DENIED) { + reason = appRestrictionController + .getPotentialSystemExemptionReason(callerUid, packageName); + if (reason == REASON_DENIED) { + reason = appRestrictionController + .getPotentialUserAllowedExemptionReason(callerUid, packageName); + } + } + switch (reason) { + case REASON_SYSTEM_UID: + case REASON_SYSTEM_ALLOW_LISTED: + case REASON_DEVICE_DEMO_MODE: + case REASON_DISALLOW_APPS_CONTROL: + case REASON_DEVICE_OWNER: + case REASON_PROFILE_OWNER: + case REASON_PROC_STATE_PERSISTENT: + case REASON_PROC_STATE_PERSISTENT_UI: + case REASON_SYSTEM_MODULE: + case REASON_CARRIER_PRIVILEGED_APP: + case REASON_DPO_PROTECTED_APP: + case REASON_ACTIVE_DEVICE_ADMIN: + case REASON_ROLE_EMERGENCY: + case REASON_ALLOWLISTED_PACKAGE: + return PERMISSION_GRANTED; + default: + return PERMISSION_DENIED; + } + } + } + + private void initSystemExemptedFgsTypePermission() { + final ForegroundServiceTypePolicy policy = ForegroundServiceTypePolicy.getDefaultPolicy(); + final ForegroundServiceTypePolicyInfo policyInfo = + policy.getForegroundServiceTypePolicyInfo( + ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED, + ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE); + if (policyInfo != null) { + policyInfo.setCustomPermission(new SystemExemptedFgsTypePermission()); + } + } + ServiceNotificationPolicy applyForegroundServiceNotificationLocked(Notification notification, final String tag, final int id, final String pkg, final int userId) { // By nature of the FGS API, all FGS notifications have a null tag @@ -2985,7 +3162,7 @@ public final class ActiveServices { } } setFgsRestrictionLocked(callingPackage, callingPid, callingUid, service, s, userId, - false); + false /* allowBackgroundActivityStarts */, true /* isBindService */); if (s.app != null) { ProcessServiceRecord servicePsr = s.app.mServices; @@ -4773,10 +4950,11 @@ public final class ActiveServices { unregisterAppOpCallbackLocked(r); r.mFgsExitTime = SystemClock.uptimeMillis(); logFGSStateChangeLocked(r, - FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__EXIT, + FOREGROUND_SERVICE_STATE_CHANGED__STATE__EXIT, r.mFgsExitTime > r.mFgsEnterTime ? (int) (r.mFgsExitTime - r.mFgsEnterTime) : 0, - FGS_STOP_REASON_STOP_SERVICE); + FGS_STOP_REASON_STOP_SERVICE, + FGS_TYPE_POLICY_CHECK_UNKNOWN); mAm.updateForegroundServiceUsageStats(r.name, r.userId, false); } @@ -6500,7 +6678,7 @@ public final class ActiveServices { */ private void setFgsRestrictionLocked(String callingPackage, int callingPid, int callingUid, Intent intent, ServiceRecord r, int userId, - boolean allowBackgroundActivityStarts) { + boolean allowBackgroundActivityStarts, boolean isBindService) { r.mLastSetFgsRestrictionTime = SystemClock.elapsedRealtime(); // Check DeviceConfig flag. if (!mAm.mConstants.mFlagBackgroundFgsStartRestrictionEnabled) { @@ -6510,14 +6688,15 @@ public final class ActiveServices { if (!r.mAllowWhileInUsePermissionInFgs || (r.mAllowStartForeground == REASON_DENIED)) { final @ReasonCode int allowWhileInUse = shouldAllowFgsWhileInUsePermissionLocked( - callingPackage, callingPid, callingUid, r, allowBackgroundActivityStarts); + callingPackage, callingPid, callingUid, r, allowBackgroundActivityStarts, + isBindService); if (!r.mAllowWhileInUsePermissionInFgs) { r.mAllowWhileInUsePermissionInFgs = (allowWhileInUse != REASON_DENIED); } if (r.mAllowStartForeground == REASON_DENIED) { r.mAllowStartForeground = shouldAllowFgsStartForegroundWithBindingCheckLocked( allowWhileInUse, callingPackage, callingPid, callingUid, intent, r, - userId); + userId, isBindService); } } } @@ -6537,9 +6716,10 @@ public final class ActiveServices { } final @ReasonCode int allowWhileInUse = shouldAllowFgsWhileInUsePermissionLocked( callingPackage, callingPid, callingUid, null /* serviceRecord */, - false /* allowBackgroundActivityStarts */); + false /* allowBackgroundActivityStarts */, false); @ReasonCode int allowStartFgs = shouldAllowFgsStartForegroundNoBindingCheckLocked( - allowWhileInUse, callingPid, callingUid, callingPackage, null /* targetService */); + allowWhileInUse, callingPid, callingUid, callingPackage, null /* targetService */, + false /* isBindService */); if (allowStartFgs == REASON_DENIED) { if (canBindingClientStartFgsLocked(callingUid) != null) { @@ -6559,7 +6739,7 @@ public final class ActiveServices { */ private @ReasonCode int shouldAllowFgsWhileInUsePermissionLocked(String callingPackage, int callingPid, int callingUid, @Nullable ServiceRecord targetService, - boolean allowBackgroundActivityStarts) { + boolean allowBackgroundActivityStarts, boolean isBindService) { int ret = REASON_DENIED; final int uidState = mAm.getUidStateLocked(callingUid); @@ -6713,12 +6893,12 @@ public final class ActiveServices { shouldAllowFgsWhileInUsePermissionLocked( clientPackageName, clientPid, clientUid, null /* serviceRecord */, - false /* allowBackgroundActivityStarts */); + false /* allowBackgroundActivityStarts */, false); final @ReasonCode int allowStartFgs = shouldAllowFgsStartForegroundNoBindingCheckLocked( allowWhileInUse2, clientPid, clientUid, clientPackageName, - null /* targetService */); + null /* targetService */, false); if (allowStartFgs != REASON_DENIED) { return new Pair<>(allowStartFgs, clientPackageName); } else { @@ -6750,11 +6930,11 @@ public final class ActiveServices { */ private @ReasonCode int shouldAllowFgsStartForegroundWithBindingCheckLocked( @ReasonCode int allowWhileInUse, String callingPackage, int callingPid, - int callingUid, Intent intent, ServiceRecord r, int userId) { + int callingUid, Intent intent, ServiceRecord r, int userId, boolean isBindService) { ActivityManagerService.FgsTempAllowListItem tempAllowListReason = r.mInfoTempFgsAllowListReason = mAm.isAllowlistedForFgsStartLOSP(callingUid); int ret = shouldAllowFgsStartForegroundNoBindingCheckLocked(allowWhileInUse, callingPid, - callingUid, callingPackage, r); + callingUid, callingPackage, r, isBindService); String bindFromPackage = null; if (ret == REASON_DENIED) { @@ -6789,6 +6969,7 @@ public final class ActiveServices { + "; callerTargetSdkVersion:" + callerTargetSdkVersion + "; startForegroundCount:" + r.mStartForegroundCount + "; bindFromPackage:" + bindFromPackage + + ": isBindService:" + isBindService + "]"; if (!debugInfo.equals(r.mInfoAllowStartForeground)) { r.mLoggedInfoAllowStartForeground = false; @@ -6799,7 +6980,7 @@ public final class ActiveServices { private @ReasonCode int shouldAllowFgsStartForegroundNoBindingCheckLocked( @ReasonCode int allowWhileInUse, int callingPid, int callingUid, String callingPackage, - @Nullable ServiceRecord targetService) { + @Nullable ServiceRecord targetService, boolean isBindService) { int ret = allowWhileInUse; if (ret == REASON_DENIED) { @@ -6981,10 +7162,12 @@ public final class ActiveServices { } private void logFgsBackgroundStart(ServiceRecord r) { + /* // Only log if FGS is started from background. if (!isFgsBgStart(r.mAllowStartForeground)) { return; } + */ if (!r.mLoggedInfoAllowStartForeground) { final String msg = "Background started FGS: " + ((r.mAllowStartForeground != REASON_DENIED) ? "Allowed " : "Disallowed ") @@ -6996,10 +7179,10 @@ public final class ActiveServices { } Slog.i(TAG, msg); } else { - if (ActivityManagerUtils.shouldSamplePackageForAtom(r.packageName, - mAm.mConstants.mFgsStartDeniedLogSampleRate)) { + //if (ActivityManagerUtils.shouldSamplePackageForAtom(r.packageName, + // mAm.mConstants.mFgsStartDeniedLogSampleRate)) { Slog.wtfQuiet(TAG, msg); - } + //} Slog.w(TAG, msg); } r.mLoggedInfoAllowStartForeground = true; @@ -7011,17 +7194,20 @@ public final class ActiveServices { * @param r ServiceRecord * @param state one of ENTER/EXIT/DENIED event. * @param durationMs Only meaningful for EXIT event, the duration from ENTER and EXIT state. + * @param fgsStopReason why was this FGS stopped. + * @param fgsTypeCheckCode The FGS type policy check result. */ private void logFGSStateChangeLocked(ServiceRecord r, int state, int durationMs, - @FgsStopReason int fgsStopReason) { + @FgsStopReason int fgsStopReason, + @ForegroundServicePolicyCheckCode int fgsTypeCheckCode) { if (!ActivityManagerUtils.shouldSamplePackageForAtom( r.packageName, mAm.mConstants.mFgsAtomSampleRate)) { return; } boolean allowWhileInUsePermissionInFgs; @PowerExemptionManager.ReasonCode int fgsStartReasonCode; - if (state == FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__ENTER - || state == FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__EXIT) { + if (state == FOREGROUND_SERVICE_STATE_CHANGED__STATE__ENTER + || state == FOREGROUND_SERVICE_STATE_CHANGED__STATE__EXIT) { allowWhileInUsePermissionInFgs = r.mAllowWhileInUsePermissionInFgsAtEntering; fgsStartReasonCode = r.mAllowStartForegroundAtEntering; } else { @@ -7047,14 +7233,15 @@ public final class ActiveServices { r.mStartForegroundCount, ActivityManagerUtils.hashComponentNameForAtom(r.shortInstanceName), r.mFgsHasNotificationPermission, - r.foregroundServiceType); + r.foregroundServiceType, + fgsTypeCheckCode); int event = 0; - if (state == FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__ENTER) { + if (state == FOREGROUND_SERVICE_STATE_CHANGED__STATE__ENTER) { event = EventLogTags.AM_FOREGROUND_SERVICE_START; - } else if (state == FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__EXIT) { + } else if (state == FOREGROUND_SERVICE_STATE_CHANGED__STATE__EXIT) { event = EventLogTags.AM_FOREGROUND_SERVICE_STOP; - } else if (state == FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__DENIED) { + } else if (state == FOREGROUND_SERVICE_STATE_CHANGED__STATE__DENIED) { event = EventLogTags.AM_FOREGROUND_SERVICE_DENIED; } else { // Unknown event. @@ -7082,7 +7269,7 @@ public final class ActiveServices { String callingPackage) { return shouldAllowFgsWhileInUsePermissionLocked(callingPackage, callingPid, callingUid, /* targetService */ null, - /* allowBackgroundActivityStarts */ false) + /* allowBackgroundActivityStarts */ false, false) != REASON_DENIED; } diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index af6eaf4e94bd..d44b727d724b 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -378,12 +378,10 @@ import com.android.internal.util.FastPrintWriter; import com.android.internal.util.FrameworkStatsLog; import com.android.internal.util.MemInfoReader; import com.android.internal.util.Preconditions; -import com.android.internal.util.function.DecFunction; import com.android.internal.util.function.HeptFunction; import com.android.internal.util.function.HexFunction; import com.android.internal.util.function.QuadFunction; import com.android.internal.util.function.QuintFunction; -import com.android.internal.util.function.TriFunction; import com.android.internal.util.function.UndecFunction; import com.android.server.AlarmManagerInternal; import com.android.server.BootReceiver; @@ -11173,9 +11171,9 @@ public class ActivityManagerService extends IActivityManager.Stub 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); @@ -18904,19 +18902,20 @@ public class ActivityManagerService extends IActivityManager.Stub } @Override - public SyncNotedAppOp startProxyOperation(int code, + public SyncNotedAppOp startProxyOperation(@NonNull IBinder clientId, int code, @NonNull AttributionSource attributionSource, boolean startIfModeDefault, boolean shouldCollectAsyncNotedOp, String message, boolean shouldCollectMessage, boolean skipProxyOperation, @AttributionFlags int proxyAttributionFlags, @AttributionFlags int proxiedAttributionFlags, int attributionChainId, - @NonNull DecFunction<Integer, AttributionSource, Boolean, Boolean, String, Boolean, - Boolean, Integer, Integer, Integer, SyncNotedAppOp> superImpl) { + @NonNull UndecFunction<IBinder, Integer, AttributionSource, + Boolean, Boolean, String, Boolean, Boolean, Integer, Integer, Integer, + SyncNotedAppOp> superImpl) { if (attributionSource.getUid() == mTargetUid && isTargetOp(code)) { final int shellUid = UserHandle.getUid(UserHandle.getUserId( attributionSource.getUid()), Process.SHELL_UID); final long identity = Binder.clearCallingIdentity(); try { - return superImpl.apply(code, new AttributionSource(shellUid, + return superImpl.apply(clientId, code, new AttributionSource(shellUid, "com.android.shell", attributionSource.getAttributionTag(), attributionSource.getToken(), attributionSource.getNext()), startIfModeDefault, shouldCollectAsyncNotedOp, message, @@ -18926,21 +18925,22 @@ public class ActivityManagerService extends IActivityManager.Stub Binder.restoreCallingIdentity(identity); } } - return superImpl.apply(code, attributionSource, startIfModeDefault, + return superImpl.apply(clientId, code, attributionSource, startIfModeDefault, shouldCollectAsyncNotedOp, message, shouldCollectMessage, skipProxyOperation, proxyAttributionFlags, proxiedAttributionFlags, attributionChainId); } @Override - public void finishProxyOperation(int code, @NonNull AttributionSource attributionSource, - boolean skipProxyOperation, @NonNull TriFunction<Integer, AttributionSource, - Boolean, Void> superImpl) { + public void finishProxyOperation(@NonNull IBinder clientId, int code, + @NonNull AttributionSource attributionSource, boolean skipProxyOperation, + @NonNull QuadFunction<IBinder, Integer, AttributionSource, Boolean, + Void> superImpl) { if (attributionSource.getUid() == mTargetUid && isTargetOp(code)) { final int shellUid = UserHandle.getUid(UserHandle.getUserId( attributionSource.getUid()), Process.SHELL_UID); final long identity = Binder.clearCallingIdentity(); try { - superImpl.apply(code, new AttributionSource(shellUid, + superImpl.apply(clientId, code, new AttributionSource(shellUid, "com.android.shell", attributionSource.getAttributionTag(), attributionSource.getToken(), attributionSource.getNext()), skipProxyOperation); @@ -18948,7 +18948,7 @@ public class ActivityManagerService extends IActivityManager.Stub Binder.restoreCallingIdentity(identity); } } - superImpl.apply(code, attributionSource, skipProxyOperation); + superImpl.apply(clientId, code, attributionSource, skipProxyOperation); } private boolean isTargetOp(int code) { diff --git a/services/core/java/com/android/server/am/AppFGSTracker.java b/services/core/java/com/android/server/am/AppFGSTracker.java index 50515cd923d1..1f98aba5bbd7 100644 --- a/services/core/java/com/android/server/am/AppFGSTracker.java +++ b/services/core/java/com/android/server/am/AppFGSTracker.java @@ -16,10 +16,10 @@ package com.android.server.am; +import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPES_MAX_INDEX; import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION; import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK; import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE; -import static android.content.pm.ServiceInfo.NUM_OF_FOREGROUND_SERVICE_TYPES; import static android.content.pm.ServiceInfo.foregroundServiceTypeToLabel; import static android.os.PowerExemptionManager.REASON_DENIED; @@ -645,7 +645,7 @@ final class AppFGSTracker extends BaseAppStateDurationsTracker<AppFGSPolicy, Pac PackageDurations(int uid, String packageName, MaxTrackingDurationConfig maxTrackingDurationConfig, AppFGSTracker tracker) { - super(uid, packageName, NUM_OF_FOREGROUND_SERVICE_TYPES + 1, TAG, + super(uid, packageName, FOREGROUND_SERVICE_TYPES_MAX_INDEX + 1, TAG, maxTrackingDurationConfig); mEvents[DEFAULT_INDEX] = new LinkedList<>(); mTracker = tracker; diff --git a/services/core/java/com/android/server/am/AppRestrictionController.java b/services/core/java/com/android/server/am/AppRestrictionController.java index ba1c3b34d7a2..6abf6d8e9eea 100644 --- a/services/core/java/com/android/server/am/AppRestrictionController.java +++ b/services/core/java/com/android/server/am/AppRestrictionController.java @@ -2784,6 +2784,37 @@ public final class AppRestrictionController { */ @ReasonCode int getBackgroundRestrictionExemptionReason(int uid) { + @ReasonCode int reason = getPotentialSystemExemptionReason(uid); + if (reason != REASON_DENIED) { + return reason; + } + final String[] packages = mInjector.getPackageManager().getPackagesForUid(uid); + if (packages != null) { + // Check each packages to see if any of them is in the "fixed" exemption cases. + for (String pkg : packages) { + reason = getPotentialSystemExemptionReason(uid, pkg); + if (reason != REASON_DENIED) { + return reason; + } + } + // Loop the packages again, and check the user-configurable exemptions. + for (String pkg : packages) { + reason = getPotentialUserAllowedExemptionReason(uid, pkg); + if (reason != REASON_DENIED) { + return reason; + } + } + } + return REASON_DENIED; + } + + /** + * @param uid The uid to check. + * @return The potential exemption reason of the given uid. The caller must decide + * whether or not it should be exempted. + */ + @ReasonCode + int getPotentialSystemExemptionReason(int uid) { if (UserHandle.isCore(uid)) { return REASON_SYSTEM_UID; } @@ -2811,37 +2842,51 @@ public final class AppRestrictionController { } else if (uidProcState <= PROCESS_STATE_PERSISTENT_UI) { return REASON_PROC_STATE_PERSISTENT_UI; } - final String[] packages = mInjector.getPackageManager().getPackagesForUid(uid); - if (packages != null) { - final AppOpsManager appOpsManager = mInjector.getAppOpsManager(); - final PackageManagerInternal pm = mInjector.getPackageManagerInternal(); - final AppStandbyInternal appStandbyInternal = mInjector.getAppStandbyInternal(); - // Check each packages to see if any of them is in the "fixed" exemption cases. - for (String pkg : packages) { - if (isSystemModule(pkg)) { - return REASON_SYSTEM_MODULE; - } else if (isCarrierApp(pkg)) { - return REASON_CARRIER_PRIVILEGED_APP; - } else if (isExemptedFromSysConfig(pkg)) { - return REASON_SYSTEM_ALLOW_LISTED; - } else if (mConstantsObserver.mBgRestrictionExemptedPackages.contains(pkg)) { - return REASON_SYSTEM_ALLOW_LISTED; - } else if (pm.isPackageStateProtected(pkg, userId)) { - return REASON_DPO_PROTECTED_APP; - } else if (appStandbyInternal.isActiveDeviceAdmin(pkg, userId)) { - return REASON_ACTIVE_DEVICE_ADMIN; - } - } - // Loop the packages again, and check the user-configurable exemptions. - for (String pkg : packages) { - if (appOpsManager.checkOpNoThrow(AppOpsManager.OP_ACTIVATE_VPN, - uid, pkg) == AppOpsManager.MODE_ALLOWED) { - return REASON_OP_ACTIVATE_VPN; - } else if (appOpsManager.checkOpNoThrow(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, - uid, pkg) == AppOpsManager.MODE_ALLOWED) { - return REASON_OP_ACTIVATE_PLATFORM_VPN; - } - } + return REASON_DENIED; + } + + /** + * @param uid The uid to check. + * @param pkgName The package name to check. + * @return The potential system-fixed exemption reason of the given uid/package. The caller + * must decide whether or not it should be exempted. + */ + @ReasonCode + int getPotentialSystemExemptionReason(int uid, String pkg) { + final PackageManagerInternal pm = mInjector.getPackageManagerInternal(); + final AppStandbyInternal appStandbyInternal = mInjector.getAppStandbyInternal(); + final int userId = UserHandle.getUserId(uid); + if (isSystemModule(pkg)) { + return REASON_SYSTEM_MODULE; + } else if (isCarrierApp(pkg)) { + return REASON_CARRIER_PRIVILEGED_APP; + } else if (isExemptedFromSysConfig(pkg)) { + return REASON_SYSTEM_ALLOW_LISTED; + } else if (mConstantsObserver.mBgRestrictionExemptedPackages.contains(pkg)) { + return REASON_SYSTEM_ALLOW_LISTED; + } else if (pm.isPackageStateProtected(pkg, userId)) { + return REASON_DPO_PROTECTED_APP; + } else if (appStandbyInternal.isActiveDeviceAdmin(pkg, userId)) { + return REASON_ACTIVE_DEVICE_ADMIN; + } + return REASON_DENIED; + } + + /** + * @param uid The uid to check. + * @param pkgName The package name to check. + * @return The potential user-allowed exemption reason of the given uid/package. The caller + * must decide whether or not it should be exempted. + */ + @ReasonCode + int getPotentialUserAllowedExemptionReason(int uid, String pkg) { + final AppOpsManager appOpsManager = mInjector.getAppOpsManager(); + if (appOpsManager.checkOpNoThrow(AppOpsManager.OP_ACTIVATE_VPN, + uid, pkg) == AppOpsManager.MODE_ALLOWED) { + return REASON_OP_ACTIVATE_VPN; + } else if (appOpsManager.checkOpNoThrow(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, + uid, pkg) == AppOpsManager.MODE_ALLOWED) { + return REASON_OP_ACTIVATE_PLATFORM_VPN; } if (isRoleHeldByUid(RoleManager.ROLE_DIALER, uid)) { return REASON_ROLE_DIALER; @@ -2852,6 +2897,7 @@ public final class AppRestrictionController { if (isOnDeviceIdleAllowlist(uid)) { return REASON_ALLOWLISTED_PACKAGE; } + final ActivityManagerInternal am = mInjector.getActivityManagerInternal(); if (am.isAssociatedCompanionApp(UserHandle.getUserId(uid), uid)) { return REASON_COMPANION_DEVICE_MANAGER; } diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java index 47ca427be9ff..739d2777fe17 100644 --- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java +++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java @@ -164,6 +164,7 @@ class BroadcastProcessQueue { private boolean mProcessCached; private boolean mProcessInstrumented; + private boolean mProcessPersistent; private String mCachedToString; private String mCachedToShortString; @@ -323,8 +324,10 @@ class BroadcastProcessQueue { this.app = app; if (app != null) { setProcessInstrumented(app.getActiveInstrumentation() != null); + setProcessPersistent(app.isPersistent()); } else { setProcessInstrumented(false); + setProcessPersistent(false); } } @@ -352,6 +355,17 @@ class BroadcastProcessQueue { } /** + * Update if this process is in the "persistent" state, which signals broadcast dispatch should + * bypass all pauses or delays to prevent the system from becoming out of sync with itself. + */ + public void setProcessPersistent(boolean persistent) { + if (mProcessPersistent != persistent) { + mProcessPersistent = persistent; + invalidateRunnableAt(); + } + } + + /** * Return if we know of an actively running "warm" process for this queue. */ public boolean isProcessWarm() { @@ -636,6 +650,7 @@ class BroadcastProcessQueue { static final int REASON_MAX_PENDING = 3; static final int REASON_BLOCKED = 4; static final int REASON_INSTRUMENTED = 5; + static final int REASON_PERSISTENT = 6; static final int REASON_CONTAINS_FOREGROUND = 10; static final int REASON_CONTAINS_ORDERED = 11; static final int REASON_CONTAINS_ALARM = 12; @@ -652,6 +667,7 @@ class BroadcastProcessQueue { REASON_MAX_PENDING, REASON_BLOCKED, REASON_INSTRUMENTED, + REASON_PERSISTENT, REASON_CONTAINS_FOREGROUND, REASON_CONTAINS_ORDERED, REASON_CONTAINS_ALARM, @@ -672,6 +688,7 @@ class BroadcastProcessQueue { case REASON_MAX_PENDING: return "MAX_PENDING"; case REASON_BLOCKED: return "BLOCKED"; case REASON_INSTRUMENTED: return "INSTRUMENTED"; + case REASON_PERSISTENT: return "PERSISTENT"; case REASON_CONTAINS_FOREGROUND: return "CONTAINS_FOREGROUND"; case REASON_CONTAINS_ORDERED: return "CONTAINS_ORDERED"; case REASON_CONTAINS_ALARM: return "CONTAINS_ALARM"; @@ -731,6 +748,9 @@ class BroadcastProcessQueue { } else if (mCountManifest > 0) { mRunnableAt = runnableAt; mRunnableAtReason = REASON_CONTAINS_MANIFEST; + } else if (mProcessPersistent) { + mRunnableAt = runnableAt; + mRunnableAtReason = REASON_PERSISTENT; } else if (mProcessCached) { mRunnableAt = runnableAt + constants.DELAY_CACHED_MILLIS; mRunnableAtReason = REASON_CACHED; diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java index 3dee2627e45d..008d95a51cd9 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.BroadcastRecord.isDeliveryStateTerminal; 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 @@ class BroadcastQueueModernImpl extends BroadcastQueue { } 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, msg.arg1); } return true; } @@ -751,12 +746,10 @@ class BroadcastQueueModernImpl extends BroadcastQueue { if (mService.mProcessesReady && !r.timeoutExempt && !assumeDelivered) { 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); + final int softTimeoutMillis = (int) (r.isForeground() ? mFgConstants.TIMEOUT + : mBgConstants.TIMEOUT); + mLocalHandler.sendMessageDelayed(Message.obtain(mLocalHandler, + MSG_DELIVERY_TIMEOUT_SOFT, softTimeoutMillis, 0, queue), softTimeoutMillis); } if (r.allowBackgroundActivityStarts) { @@ -844,15 +837,16 @@ class BroadcastQueueModernImpl extends BroadcastQueue { } private void deliveryTimeoutSoftLocked(@NonNull BroadcastProcessQueue queue, - @DurationMillisLong long originalTimeout) { + int softTimeoutMillis) { 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 hardTimeoutMillis = MathUtils.constrain(cpuDelayTime, 0, softTimeoutMillis); mLocalHandler.sendMessageDelayed( - Message.obtain(mLocalHandler, MSG_DELIVERY_TIMEOUT_HARD, queue), hardTimeout); + Message.obtain(mLocalHandler, MSG_DELIVERY_TIMEOUT_HARD, queue), + hardTimeoutMillis); } else { deliveryTimeoutHardLocked(queue); } @@ -1462,7 +1456,7 @@ class BroadcastQueueModernImpl extends BroadcastQueue { } BroadcastProcessQueue created = new BroadcastProcessQueue(mConstants, processName, uid); - created.app = mService.getProcessRecordLocked(processName, uid); + created.setProcess(mService.getProcessRecordLocked(processName, uid)); if (leaf == null) { mProcessQueues.put(uid, created); diff --git a/services/core/java/com/android/server/am/ProcessStateRecord.java b/services/core/java/com/android/server/am/ProcessStateRecord.java index d2ef479ed524..2ad2077d01bc 100644 --- a/services/core/java/com/android/server/am/ProcessStateRecord.java +++ b/services/core/java/com/android/server/am/ProcessStateRecord.java @@ -30,6 +30,7 @@ import android.annotation.ElapsedRealtimeLong; import android.app.ActivityManager; import android.content.ComponentName; import android.os.SystemClock; +import android.os.Trace; import android.util.Slog; import android.util.TimeUtils; @@ -43,6 +44,9 @@ import java.io.PrintWriter; * The state info of the process, including proc state, oom adj score, et al. */ final class ProcessStateRecord { + // Enable this to trace all OomAdjuster state transitions + private static final boolean TRACE_OOM_ADJ = false; + private final ProcessRecord mApp; private final ActivityManagerService mService; private final ActivityManagerGlobalLock mProcLock; @@ -916,6 +920,12 @@ final class ProcessStateRecord { @GuardedBy("mService") void setAdjType(String adjType) { + if (TRACE_OOM_ADJ) { + Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER, + "oom:" + mApp.processName + "/u" + mApp.uid, 0); + Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, + "oom:" + mApp.processName + "/u" + mApp.uid, adjType, 0); + } mAdjType = adjType; } @@ -1153,6 +1163,10 @@ final class ProcessStateRecord { @GuardedBy({"mService", "mProcLock"}) void onCleanupApplicationRecordLSP() { + if (TRACE_OOM_ADJ) { + Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER, + "oom:" + mApp.processName + "/u" + mApp.uid, 0); + } setHasForegroundActivities(false); mHasShownUi = false; mForcingToImportant = null; diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index 92133274c0cc..4d86140816ea 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -1090,7 +1090,7 @@ class UserController implements Handler.Callback { // TODO(b/239982558): for now we're just updating the user's visibility, but most likely // we'll need to remove this call and handle that as part of the user state workflow // instead. - userManagerInternal.unassignUserFromDisplay(userId); + userManagerInternal.unassignUserFromDisplayOnStop(userId); final boolean visibilityChanged; boolean visibleBefore; @@ -1650,13 +1650,30 @@ class UserController implements Handler.Callback { return false; } - if (!userInfo.preCreated) { - // TODO(b/244644281): UMI should return whether the user is visible. And if fails, - // the user should not be in the mediator's started users structure - mInjector.getUserManagerInternal().assignUserToDisplay(userId, - userInfo.profileGroupId, foreground, displayId); + t.traceBegin("assignUserToDisplayOnStart"); + int result = mInjector.getUserManagerInternal().assignUserToDisplayOnStart(userId, + userInfo.profileGroupId, foreground, displayId); + t.traceEnd(); + + boolean visible; + switch (result) { + case UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE: + visible = true; + break; + case UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE: + visible = false; + break; + default: + Slogf.wtf(TAG, "Wrong result from assignUserToDisplayOnStart(): %d", result); + // Fall through + case UserManagerInternal.USER_ASSIGNMENT_RESULT_FAILURE: + Slogf.e(TAG, "%s user(%d) / display (%d) assignment failed: %s", + (foreground ? "fg" : "bg"), userId, displayId, + UserManagerInternal.userAssignmentResultToString(result)); + return false; } + // TODO(b/239982558): might need something similar for bg users on secondary display if (foreground && isUserSwitchUiEnabled()) { t.traceBegin("startFreezingScreen"); @@ -1751,19 +1768,6 @@ class UserController implements Handler.Callback { } t.traceEnd(); - // Need to call UM when user is on background, as there are some cases where the user - // cannot be started in background on a secondary display (for example, if user is a - // profile). - // TODO(b/253103846): it's also explicitly checking if the user is the USER_SYSTEM, as - // the UM call would return true during boot (when CarService / BootUserInitializer - // calls AM.startUserInBackground() because the system user is still the current user. - // TODO(b/244644281): another fragility of this check is that it must wait to call - // UMI.isUserVisible() until the user state is check, as that method checks if the - // profile of the current user is started. We should fix that dependency so the logic - // belongs to just one place (like UserDisplayAssigner) - boolean visible = foreground - || userId != UserHandle.USER_SYSTEM - && mInjector.getUserManagerInternal().isUserVisible(userId); if (visible) { synchronized (mLock) { addVisibleUserLocked(userId); @@ -1816,8 +1820,8 @@ class UserController implements Handler.Callback { // user that was started in the background before), so it's necessary to explicitly // notify the services (while when the user starts from BOOTING, USER_START_MSG // takes care of that. - mHandler.sendMessage(mHandler.obtainMessage(USER_VISIBILITY_CHANGED_MSG, userId, - visible ? 1 : 0)); + mHandler.sendMessage( + mHandler.obtainMessage(USER_VISIBILITY_CHANGED_MSG, userId, 1)); } t.traceBegin("sendMessages"); diff --git a/services/core/java/com/android/server/app/GameManagerService.java b/services/core/java/com/android/server/app/GameManagerService.java index 31d707da014f..64f2aa38e5b1 100644 --- a/services/core/java/com/android/server/app/GameManagerService.java +++ b/services/core/java/com/android/server/app/GameManagerService.java @@ -1314,16 +1314,9 @@ public final class GameManagerService extends IGameManagerService.Stub { void onUserSwitching(TargetUser from, TargetUser to) { final int toUserId = to.getUserIdentifier(); - if (from != null) { - synchronized (mLock) { - final int fromUserId = from.getUserIdentifier(); - if (mSettings.containsKey(fromUserId)) { - sendUserMessage(fromUserId, REMOVE_SETTINGS, "ON_USER_SWITCHING", - 0 /*delayMillis*/); - } - } - } - + // we want to re-populate the setting when switching user as the device config may have + // changed, which will only update for the previous user, see + // DeviceConfigListener#onPropertiesChanged. sendUserMessage(toUserId, POPULATE_GAME_MODE_SETTINGS, "ON_USER_SWITCHING", 0 /*delayMillis*/); @@ -1392,8 +1385,9 @@ public final class GameManagerService extends IGameManagerService.Stub { Slog.v(TAG, "Package configuration not found for " + packageName); return; } + } else { + updateFps(packageConfig, packageName, gameMode, userId); } - updateFps(packageConfig, packageName, gameMode, userId); updateUseAngle(packageName, gameMode); } diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index a58583c53321..2389b301d4bb 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -2920,18 +2920,18 @@ public class AppOpsService extends IAppOpsService.Stub implements PersistenceSch } @Override - public SyncNotedAppOp startProxyOperation(int code, + public SyncNotedAppOp startProxyOperation(@NonNull IBinder clientId, int code, @NonNull AttributionSource attributionSource, boolean startIfModeDefault, boolean shouldCollectAsyncNotedOp, String message, boolean shouldCollectMessage, boolean skipProxyOperation, @AttributionFlags int proxyAttributionFlags, @AttributionFlags int proxiedAttributionFlags, int attributionChainId) { - return mCheckOpsDelegateDispatcher.startProxyOperation(code, attributionSource, + return mCheckOpsDelegateDispatcher.startProxyOperation(clientId, code, attributionSource, startIfModeDefault, shouldCollectAsyncNotedOp, message, shouldCollectMessage, skipProxyOperation, proxyAttributionFlags, proxiedAttributionFlags, attributionChainId); } - private SyncNotedAppOp startProxyOperationImpl(int code, + private SyncNotedAppOp startProxyOperationImpl(@NonNull IBinder clientId, int code, @NonNull AttributionSource attributionSource, boolean startIfModeDefault, boolean shouldCollectAsyncNotedOp, String message, boolean shouldCollectMessage, boolean skipProxyOperation, @AttributionFlags @@ -2940,11 +2940,9 @@ public class AppOpsService extends IAppOpsService.Stub implements PersistenceSch final int proxyUid = attributionSource.getUid(); final String proxyPackageName = attributionSource.getPackageName(); final String proxyAttributionTag = attributionSource.getAttributionTag(); - final IBinder proxyToken = attributionSource.getToken(); final int proxiedUid = attributionSource.getNextUid(); final String proxiedPackageName = attributionSource.getNextPackageName(); final String proxiedAttributionTag = attributionSource.getNextAttributionTag(); - final IBinder proxiedToken = attributionSource.getNextToken(); verifyIncomingProxyUid(attributionSource); verifyIncomingOp(code); @@ -2986,7 +2984,7 @@ public class AppOpsService extends IAppOpsService.Stub implements PersistenceSch if (!skipProxyOperation) { // Test if the proxied operation will succeed before starting the proxy operation - final SyncNotedAppOp testProxiedOp = startOperationUnchecked(proxiedToken, code, + final SyncNotedAppOp testProxiedOp = startOperationUnchecked(clientId, code, proxiedUid, resolvedProxiedPackageName, proxiedAttributionTag, proxyUid, resolvedProxyPackageName, proxyAttributionTag, proxiedFlags, startIfModeDefault, shouldCollectAsyncNotedOp, message, shouldCollectMessage, @@ -2998,7 +2996,7 @@ public class AppOpsService extends IAppOpsService.Stub implements PersistenceSch final int proxyFlags = isProxyTrusted ? AppOpsManager.OP_FLAG_TRUSTED_PROXY : AppOpsManager.OP_FLAG_UNTRUSTED_PROXY; - final SyncNotedAppOp proxyAppOp = startOperationUnchecked(proxyToken, code, proxyUid, + final SyncNotedAppOp proxyAppOp = startOperationUnchecked(clientId, code, proxyUid, resolvedProxyPackageName, proxyAttributionTag, Process.INVALID_UID, null, null, proxyFlags, startIfModeDefault, !isProxyTrusted, "proxy " + message, shouldCollectMessage, proxyAttributionFlags, attributionChainId, @@ -3008,7 +3006,7 @@ public class AppOpsService extends IAppOpsService.Stub implements PersistenceSch } } - return startOperationUnchecked(proxiedToken, code, proxiedUid, resolvedProxiedPackageName, + return startOperationUnchecked(clientId, code, proxiedUid, resolvedProxiedPackageName, proxiedAttributionTag, proxyUid, resolvedProxyPackageName, proxyAttributionTag, proxiedFlags, startIfModeDefault, shouldCollectAsyncNotedOp, message, shouldCollectMessage, proxiedAttributionFlags, attributionChainId, @@ -3151,22 +3149,20 @@ public class AppOpsService extends IAppOpsService.Stub implements PersistenceSch } @Override - public void finishProxyOperation(int code, @NonNull AttributionSource attributionSource, - boolean skipProxyOperation) { - mCheckOpsDelegateDispatcher.finishProxyOperation(code, attributionSource, + public void finishProxyOperation(@NonNull IBinder clientId, int code, + @NonNull AttributionSource attributionSource, boolean skipProxyOperation) { + mCheckOpsDelegateDispatcher.finishProxyOperation(clientId, code, attributionSource, skipProxyOperation); } - private Void finishProxyOperationImpl(int code, @NonNull AttributionSource attributionSource, - boolean skipProxyOperation) { + private Void finishProxyOperationImpl(IBinder clientId, int code, + @NonNull AttributionSource attributionSource, boolean skipProxyOperation) { final int proxyUid = attributionSource.getUid(); final String proxyPackageName = attributionSource.getPackageName(); final String proxyAttributionTag = attributionSource.getAttributionTag(); - final IBinder proxyToken = attributionSource.getToken(); final int proxiedUid = attributionSource.getNextUid(); final String proxiedPackageName = attributionSource.getNextPackageName(); final String proxiedAttributionTag = attributionSource.getNextAttributionTag(); - final IBinder proxiedToken = attributionSource.getNextToken(); skipProxyOperation = skipProxyOperation && isCallerAndAttributionTrusted(attributionSource); @@ -3185,7 +3181,7 @@ public class AppOpsService extends IAppOpsService.Stub implements PersistenceSch } if (!skipProxyOperation) { - finishOperationUnchecked(proxyToken, code, proxyUid, resolvedProxyPackageName, + finishOperationUnchecked(clientId, code, proxyUid, resolvedProxyPackageName, proxyAttributionTag); } @@ -3195,7 +3191,7 @@ public class AppOpsService extends IAppOpsService.Stub implements PersistenceSch return null; } - finishOperationUnchecked(proxiedToken, code, proxiedUid, resolvedProxiedPackageName, + finishOperationUnchecked(clientId, code, proxiedUid, resolvedProxiedPackageName, proxiedAttributionTag); return null; @@ -6436,42 +6432,42 @@ public class AppOpsService extends IAppOpsService.Stub implements PersistenceSch attributionFlags, attributionChainId, AppOpsService.this::startOperationImpl); } - public SyncNotedAppOp startProxyOperation(int code, + public SyncNotedAppOp startProxyOperation(@NonNull IBinder clientId, int code, @NonNull AttributionSource attributionSource, boolean startIfModeDefault, boolean shouldCollectAsyncNotedOp, String message, boolean shouldCollectMessage, boolean skipProxyOperation, @AttributionFlags int proxyAttributionFlags, @AttributionFlags int proxiedAttributionFlags, int attributionChainId) { if (mPolicy != null) { if (mCheckOpsDelegate != null) { - return mPolicy.startProxyOperation(code, attributionSource, + return mPolicy.startProxyOperation(clientId, code, attributionSource, startIfModeDefault, shouldCollectAsyncNotedOp, message, shouldCollectMessage, skipProxyOperation, proxyAttributionFlags, proxiedAttributionFlags, attributionChainId, this::startDelegateProxyOperationImpl); } else { - return mPolicy.startProxyOperation(code, attributionSource, + return mPolicy.startProxyOperation(clientId, code, attributionSource, startIfModeDefault, shouldCollectAsyncNotedOp, message, shouldCollectMessage, skipProxyOperation, proxyAttributionFlags, proxiedAttributionFlags, attributionChainId, AppOpsService.this::startProxyOperationImpl); } } else if (mCheckOpsDelegate != null) { - return startDelegateProxyOperationImpl(code, attributionSource, + return startDelegateProxyOperationImpl(clientId, code, attributionSource, startIfModeDefault, shouldCollectAsyncNotedOp, message, shouldCollectMessage, skipProxyOperation, proxyAttributionFlags, proxiedAttributionFlags, attributionChainId); } - return startProxyOperationImpl(code, attributionSource, startIfModeDefault, + return startProxyOperationImpl(clientId, code, attributionSource, startIfModeDefault, shouldCollectAsyncNotedOp, message, shouldCollectMessage, skipProxyOperation, proxyAttributionFlags, proxiedAttributionFlags, attributionChainId); } - private SyncNotedAppOp startDelegateProxyOperationImpl(int code, + private SyncNotedAppOp startDelegateProxyOperationImpl(@NonNull IBinder clientId, int code, @NonNull AttributionSource attributionSource, boolean startIfModeDefault, boolean shouldCollectAsyncNotedOp, String message, boolean shouldCollectMessage, boolean skipProxyOperation, @AttributionFlags int proxyAttributionFlags, @AttributionFlags int proxiedAttributionFlsgs, int attributionChainId) { - return mCheckOpsDelegate.startProxyOperation(code, attributionSource, + return mCheckOpsDelegate.startProxyOperation(clientId, code, attributionSource, startIfModeDefault, shouldCollectAsyncNotedOp, message, shouldCollectMessage, skipProxyOperation, proxyAttributionFlags, proxiedAttributionFlsgs, attributionChainId, AppOpsService.this::startProxyOperationImpl); @@ -6500,27 +6496,28 @@ public class AppOpsService extends IAppOpsService.Stub implements PersistenceSch AppOpsService.this::finishOperationImpl); } - public void finishProxyOperation(int code, + public void finishProxyOperation(@NonNull IBinder clientId, int code, @NonNull AttributionSource attributionSource, boolean skipProxyOperation) { if (mPolicy != null) { if (mCheckOpsDelegate != null) { - mPolicy.finishProxyOperation(code, attributionSource, + mPolicy.finishProxyOperation(clientId, code, attributionSource, skipProxyOperation, this::finishDelegateProxyOperationImpl); } else { - mPolicy.finishProxyOperation(code, attributionSource, + mPolicy.finishProxyOperation(clientId, code, attributionSource, skipProxyOperation, AppOpsService.this::finishProxyOperationImpl); } } else if (mCheckOpsDelegate != null) { - finishDelegateProxyOperationImpl(code, attributionSource, skipProxyOperation); + finishDelegateProxyOperationImpl(clientId, code, attributionSource, + skipProxyOperation); } else { - finishProxyOperationImpl(code, attributionSource, skipProxyOperation); + finishProxyOperationImpl(clientId, code, attributionSource, skipProxyOperation); } } - private Void finishDelegateProxyOperationImpl(int code, + private Void finishDelegateProxyOperationImpl(@NonNull IBinder clientId, int code, @NonNull AttributionSource attributionSource, boolean skipProxyOperation) { - mCheckOpsDelegate.finishProxyOperation(code, attributionSource, skipProxyOperation, - AppOpsService.this::finishProxyOperationImpl); + mCheckOpsDelegate.finishProxyOperation(clientId, code, attributionSource, + skipProxyOperation, AppOpsService.this::finishProxyOperationImpl); return null; } } diff --git a/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java b/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java index 3c281d13c769..5114bd59f084 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.MIN_PRIORITY_UID_STATE; 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 @@ class AppOpsUidStateTrackerImpl implements AppOpsUidStateTracker { 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 fab7f1dfb812..d97195383778 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 @@ public class BiometricService extends SystemService { @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 48367b28bb40..dca9ef25a818 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 @@ public class FingerprintService extends SystemService { @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 560573799a3d..4c3760928df3 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.IndentingPrintWriter; 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 @@ public class BroadcastRadioService { } } + @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 class BroadcastRadioService { 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/Mutable.java b/services/core/java/com/android/server/broadcastradio/hal2/Mutable.java index a9d80549f963..a6cf72c548c4 100644 --- a/services/core/java/com/android/server/broadcastradio/hal2/Mutable.java +++ b/services/core/java/com/android/server/broadcastradio/hal2/Mutable.java @@ -34,13 +34,4 @@ final class Mutable<E> { public Mutable() { value = null; } - - /** - * Initialize value with specific value. - * - * @param value initial value. - */ - public Mutable(E value) { - this.value = value; - } } 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 0a23e385d67a..5913e0685eb4 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 @@ class RadioModule { 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 @@ class RadioModule { 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/cpu/OWNERS b/services/core/java/com/android/server/cpu/OWNERS new file mode 100644 index 000000000000..2f42363ee7e4 --- /dev/null +++ b/services/core/java/com/android/server/cpu/OWNERS @@ -0,0 +1,4 @@ +# Bug component: 608533 + +include platform/packages/services/Car:/OWNERS +lakshmana@google.com diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java index c06101f202e7..bf0b388fdb56 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 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal 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 16f1f235c2c1..cb97e2832854 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 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { 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 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { 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 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { // 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 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { 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 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { 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 facc6b2b2410..4a0ba2221ae7 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 @@ public final class DreamManagerService extends SystemService { 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 @@ public final class DreamManagerService extends SystemService { 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 @@ public final class DreamManagerService extends SystemService { 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 @@ public final class DreamManagerService extends SystemService { } 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 9d4f18113555..c83fa2d3942c 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.SystemClock; 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 @@ final class BatteryController { } } - 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 62344216c3db..298098a572b2 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 class InputManagerInternal { 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 d2282c3ee072..cb615a9729f6 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.Manifest; 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; 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.Surface; 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 @@ public class InputManagerService extends IInputManager.Stub 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 @@ public class InputManagerService extends IInputManager.Stub 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 class InputManagerService extends IInputManager.Stub 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,12 +2680,16 @@ public class InputManagerService extends IInputManager.Stub @EnforcePermission(Manifest.permission.BLUETOOTH) @Override public String getInputDeviceBluetoothAddress(int deviceId) { + super.getInputDeviceBluetoothAddress_enforcePermission(); + return mNative.getBluetoothAddress(deviceId); } @EnforcePermission(Manifest.permission.MONITOR_INPUT) @Override public void pilferPointers(IBinder inputChannelToken) { + super.pilferPointers_enforcePermission(); + Objects.requireNonNull(inputChannelToken); mNative.pilferPointers(inputChannelToken); } @@ -2689,74 +2697,78 @@ public class InputManagerService extends IInputManager.Stub @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 +3794,21 @@ public class InputManagerService extends IInputManager.Stub } @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 +3868,11 @@ public class InputManagerService extends IInputManager.Stub 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 e33f28c22998..b207e27b4005 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.hardware.lights.Light; 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 @@ final class KeyboardBacklightController implements InputManager.InputDeviceListe 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 1a0f6f7fff4a..015e5768d505 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.util.Slog; 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 @@ final class IInputMethodInvoker { // 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 @@ final class IInputMethodInvoker { // 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 4d1c5aede33d..8b083bd72722 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.DisplayImePolicy; 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.DirectBootAwareness; 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.InlineSuggestionsRequestInfo; 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub */ 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub * <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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub /** * 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub /** * 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } // 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mCurClient.mSessionRequestedForAccessibility = false; mCurClient = null; mCurVirtualDisplayToScreenMatrix = null; + ImeTracker.get().onFailed(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME); + mCurStatsToken = null; mMenuController.hideInputMethodMenuLocked(); } @@ -2379,8 +2387,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } @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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } @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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub "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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } @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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } @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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub imeDispatcher); didStart = true; } - showCurrentInputLocked( - windowToken, - InputMethodManager.SHOW_IMPLICIT, - null, + showCurrentInputImplicitLocked(windowToken, SoftInputShowHideReason.SHOW_AUTO_EDITOR_FORWARD_NAV); } break; @@ -3758,14 +3835,16 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub imeDispatcher); didStart = true; } - showCurrentInputLocked(windowToken, InputMethodManager.SHOW_IMPLICIT, null, + showCurrentInputImplicitLocked(windowToken, SoftInputShowHideReason.SHOW_STATE_ALWAYS_VISIBLE); } } else { @@ -3824,7 +3903,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } @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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } @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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mCurrentSubtype = getCurrentInputMethodSubtypeLocked(); } } + notifyInputMethodSubtypeChangedLocked(mSettings.getCurrentUserId(), imi, mCurrentSubtype); if (!setSubtypeOnly) { // Set InputMethod here @@ -6332,7 +6439,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub 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 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @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 1435016fc55a..b8abd98456e4 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 @@ public class GnssConfiguration { "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 @@ public class GnssConfiguration { } /** + * 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 6f637b83a694..6f6b1c910ff0 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.UserHandle; 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.CellInfoGsm; 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.app.IBatteryStats; 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 @@ public class GnssLocationProvider extends AbstractLocationProvider implements 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; - } - - switch (action) { - case CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED: - case TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED: - subscriptionOrCarrierConfigChanged(); - break; - } + mContext.registerReceiver(mIntentReceiver, intentFilter, null, mHandler); + + 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 @@ public class GnssLocationProvider extends AbstractLocationProvider implements 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 02bdfd5bcb56..a7fffe2ddf29 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 @@ class GnssNetworkConnectivityHandler { 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 631dbbf0f1fd..4e5e5f8e30f4 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 @@ class GnssVisibilityControl { 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 2d015a5d58c7..edb2e5bf3f97 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 @@ public class GnssNative { 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 @@ public class GnssNative { } @NativeEntryPoint - boolean isInEmergencySession() { + public boolean isInEmergencySession() { return Binder.withCleanCallingIdentity( () -> mEmergencyHelper.isInEmergency( TimeUnit.SECONDS.toMillis(mConfiguration.getEsExtensionSec()))); @@ -1507,6 +1515,10 @@ public class GnssNative { 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 @@ public class GnssNative { 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/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java index 92b685c78d98..d02faad1956e 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsService.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java @@ -248,14 +248,14 @@ public class LockSettingsService extends ILockSettings.Stub { // Locking order is mUserCreationAndRemovalLock -> mSpManager. private final Object mUserCreationAndRemovalLock = new Object(); - // These two arrays are only used at boot time. To save memory, they are set to null when - // PHASE_BOOT_COMPLETED is reached. + // These two arrays are only used at boot time. To save memory, they are set to null near the + // end of the boot, when onThirdPartyAppsStarted() is called. @GuardedBy("mUserCreationAndRemovalLock") private SparseIntArray mEarlyCreatedUsers = new SparseIntArray(); @GuardedBy("mUserCreationAndRemovalLock") private SparseIntArray mEarlyRemovedUsers = new SparseIntArray(); @GuardedBy("mUserCreationAndRemovalLock") - private boolean mBootComplete; + private boolean mThirdPartyAppsStarted; // Current password metrics for all secured users on the device. Updated when user unlocks the // device or changes password. Removed when user is stopped. @@ -297,16 +297,9 @@ public class LockSettingsService extends ILockSettings.Stub { @Override public void onBootPhase(int phase) { super.onBootPhase(phase); - switch (phase) { - case PHASE_ACTIVITY_MANAGER_READY: - mLockSettingsService.migrateOldDataAfterSystemReady(); - mLockSettingsService.loadEscrowData(); - break; - case PHASE_BOOT_COMPLETED: - mLockSettingsService.bootCompleted(); - break; - default: - break; + if (phase == PHASE_ACTIVITY_MANAGER_READY) { + mLockSettingsService.migrateOldDataAfterSystemReady(); + mLockSettingsService.loadEscrowData(); } } @@ -749,8 +742,8 @@ public class LockSettingsService extends ILockSettings.Stub { * <p> * This is primarily needed for users that were removed by Android 13 or earlier, which didn't * guarantee removal of LSS state as it relied on the {@code ACTION_USER_REMOVED} intent. It is - * also needed because {@link #removeUser()} delays requests to remove LSS state until the - * {@code PHASE_BOOT_COMPLETED} boot phase, so they can be lost. + * also needed because {@link #removeUser()} delays requests to remove LSS state until Weaver is + * guaranteed to be available, so they can be lost. * <p> * Stale state is detected by checking whether the user serial number changed. This works * because user serial numbers are never reused. @@ -931,7 +924,9 @@ public class LockSettingsService extends ILockSettings.Stub { return success; } - private void bootCompleted() { + // This is called when Weaver is guaranteed to be available (if the device supports Weaver). + // It does any synthetic password related work that was delayed from earlier in the boot. + private void onThirdPartyAppsStarted() { synchronized (mUserCreationAndRemovalLock) { // Handle delayed calls to LSS.removeUser() and LSS.createNewUser(). for (int i = 0; i < mEarlyRemovedUsers.size(); i++) { @@ -976,7 +971,7 @@ public class LockSettingsService extends ILockSettings.Stub { setString("migrated_all_users_to_sp_and_bound_ce", "true", 0); } - mBootComplete = true; + mThirdPartyAppsStarted = true; } } @@ -2304,14 +2299,14 @@ public class LockSettingsService extends ILockSettings.Stub { private void createNewUser(@UserIdInt int userId, int userSerialNumber) { synchronized (mUserCreationAndRemovalLock) { - // Before PHASE_BOOT_COMPLETED, don't actually create the synthetic password yet, but - // rather automatically delay it to later. We do this because protecting the synthetic + // During early boot, don't actually create the synthetic password yet, but rather + // automatically delay it to later. We do this because protecting the synthetic // password requires the Weaver HAL if the device supports it, and some devices don't // make Weaver available until fairly late in the boot process. This logic ensures a // consistent flow across all devices, regardless of their Weaver implementation. - if (!mBootComplete) { - Slogf.i(TAG, "Delaying locksettings state creation for user %d until boot complete", - userId); + if (!mThirdPartyAppsStarted) { + Slogf.i(TAG, "Delaying locksettings state creation for user %d until third-party " + + "apps are started", userId); mEarlyCreatedUsers.put(userId, userSerialNumber); mEarlyRemovedUsers.delete(userId); return; @@ -2325,14 +2320,14 @@ public class LockSettingsService extends ILockSettings.Stub { private void removeUser(@UserIdInt int userId) { synchronized (mUserCreationAndRemovalLock) { - // Before PHASE_BOOT_COMPLETED, don't actually remove the LSS state yet, but rather - // automatically delay it to later. We do this because deleting synthetic password - // protectors requires the Weaver HAL if the device supports it, and some devices don't - // make Weaver available until fairly late in the boot process. This logic ensures a - // consistent flow across all devices, regardless of their Weaver implementation. - if (!mBootComplete) { - Slogf.i(TAG, "Delaying locksettings state removal for user %d until boot complete", - userId); + // During early boot, don't actually remove the LSS state yet, but rather automatically + // delay it to later. We do this because deleting synthetic password protectors + // requires the Weaver HAL if the device supports it, and some devices don't make Weaver + // available until fairly late in the boot process. This logic ensures a consistent + // flow across all devices, regardless of their Weaver implementation. + if (!mThirdPartyAppsStarted) { + Slogf.i(TAG, "Delaying locksettings state removal for user %d until third-party " + + "apps are started", userId); if (mEarlyCreatedUsers.indexOfKey(userId) >= 0) { mEarlyCreatedUsers.delete(userId); } else { @@ -2634,9 +2629,8 @@ public class LockSettingsService extends ILockSettings.Stub { * protects the user's CE key with a key derived from the SP. * <p> * This is called just once in the lifetime of the user: at user creation time (possibly delayed - * until {@code PHASE_BOOT_COMPLETED} to ensure that the Weaver HAL is available if the device - * supports it), or when upgrading from Android 13 or earlier where users with no LSKF didn't - * necessarily have an SP. + * until the time when Weaver is guaranteed to be available), or when upgrading from Android 13 + * or earlier where users with no LSKF didn't necessarily have an SP. */ @GuardedBy("mSpManager") @VisibleForTesting @@ -3159,7 +3153,7 @@ public class LockSettingsService extends ILockSettings.Stub { pw.println("PasswordHandleCount: " + mGatekeeperPasswords.size()); synchronized (mUserCreationAndRemovalLock) { - pw.println("BootComplete: " + mBootComplete); + pw.println("ThirdPartyAppsStarted: " + mThirdPartyAppsStarted); } } @@ -3317,6 +3311,11 @@ public class LockSettingsService extends ILockSettings.Stub { private final class LocalService extends LockSettingsInternal { @Override + public void onThirdPartyAppsStarted() { + LockSettingsService.this.onThirdPartyAppsStarted(); + } + + @Override public void unlockUserKeyIfUnsecured(@UserIdInt int userId) { LockSettingsService.this.unlockUserKeyIfUnsecured(userId); } diff --git a/services/core/java/com/android/server/locksettings/LockSettingsStorage.java b/services/core/java/com/android/server/locksettings/LockSettingsStorage.java index d836df56ba1e..807ba3cf4463 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsStorage.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsStorage.java @@ -309,6 +309,10 @@ class LockSettingsStorage { } private void writeFile(File path, byte[] data) { + writeFile(path, data, /* syncParentDir= */ true); + } + + private void writeFile(File path, byte[] data, boolean syncParentDir) { synchronized (mFileWriteLock) { // Use AtomicFile to guarantee atomicity of the file write, including when an existing // file is replaced with a new one. This method is usually used to create new files, @@ -326,9 +330,11 @@ class LockSettingsStorage { file.failWrite(out); } // For performance reasons, AtomicFile only syncs the file itself, not also the parent - // directory. The latter must be done explicitly here, as some callers need a guarantee - // that the file really exists on-disk when this returns. - fsyncDirectory(path.getParentFile()); + // directory. The latter must be done explicitly when requested here, as some callers + // need a guarantee that the file really exists on-disk when this returns. + if (syncParentDir) { + fsyncDirectory(path.getParentFile()); + } mCache.putFile(path, data); } } @@ -378,10 +384,20 @@ class LockSettingsStorage { } } + /** + * Writes the synthetic password state file for the given user ID, protector ID, and state name. + * If the file already exists, then it is atomically replaced. + * <p> + * This doesn't sync the parent directory, and a result the new state file may be lost if the + * system crashes. The caller must call {@link syncSyntheticPasswordState()} afterwards to sync + * the parent directory if needed, preferably after batching up other state file creations for + * the same user. We do it this way because directory syncs are expensive on some filesystems. + */ public void writeSyntheticPasswordState(int userId, long protectorId, String name, byte[] data) { ensureSyntheticPasswordDirectoryForUser(userId); - writeFile(getSyntheticPasswordStateFileForUser(userId, protectorId, name), data); + writeFile(getSyntheticPasswordStateFileForUser(userId, protectorId, name), data, + /* syncParentDir= */ false); } public byte[] readSyntheticPasswordState(int userId, long protectorId, String name) { @@ -392,6 +408,13 @@ class LockSettingsStorage { deleteFile(getSyntheticPasswordStateFileForUser(userId, protectorId, name)); } + /** + * Ensures that all synthetic password state files for the user have really been saved to disk. + */ + public void syncSyntheticPasswordState(int userId) { + fsyncDirectory(getSyntheticPasswordDirectoryForUser(userId)); + } + public Map<Integer, List<Long>> listSyntheticPasswordProtectorsForAllUsers(String stateName) { Map<Integer, List<Long>> result = new ArrayMap<>(); final UserManager um = UserManager.get(mContext); diff --git a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java index 3fd488e6d357..73a16fdbc7c9 100644 --- a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java +++ b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java @@ -619,12 +619,16 @@ public class SyntheticPasswordManager { /** * Creates a new synthetic password (SP) for the given user. - * + * <p> * Any existing SID for the user is cleared. - * + * <p> * Also saves the escrow information necessary to re-generate the synthetic password under * an escrow scheme. This information can be removed with {@link #destroyEscrowData} if * password escrow should be disabled completely on the given user. + * <p> + * {@link syncState()} is not called yet; the caller should create a protector afterwards, which + * handles this. This makes it so that all the user's initial SP state files, including the + * initial LSKF-based protector, are efficiently created with only a single {@link syncState()}. */ SyntheticPassword newSyntheticPassword(int userId) { clearSidForUser(userId); @@ -668,6 +672,7 @@ public class SyntheticPasswordManager { private void saveSyntheticPasswordHandle(byte[] spHandle, int userId) { saveState(SP_HANDLE_NAME, spHandle, NULL_PROTECTOR_ID, userId); + syncState(userId); } private boolean loadEscrowData(SyntheticPassword sp, int userId) { @@ -677,6 +682,11 @@ public class SyntheticPasswordManager { return e0 != null && p1 != null; } + /** + * Saves the escrow data for the synthetic password. The caller is responsible for calling + * {@link syncState()} afterwards, once the user's other initial synthetic password state files + * have been created. + */ private void saveEscrowData(SyntheticPassword sp, int userId) { saveState(SP_E0_NAME, sp.mEncryptedEscrowSplit0, NULL_PROTECTOR_ID, userId); saveState(SP_P1_NAME, sp.mEscrowSplit1, NULL_PROTECTOR_ID, userId); @@ -708,6 +718,10 @@ public class SyntheticPasswordManager { return buffer.getInt(); } + /** + * Creates a file that stores the Weaver slot the protector is using. The caller is responsible + * for calling {@link syncState()} afterwards, once all the protector's files have been created. + */ private void saveWeaverSlot(int slot, long protectorId, int userId) { ByteBuffer buffer = ByteBuffer.allocate(Byte.BYTES + Integer.BYTES); buffer.put(WEAVER_VERSION); @@ -837,6 +851,7 @@ public class SyntheticPasswordManager { } createSyntheticPasswordBlob(protectorId, PROTECTOR_TYPE_LSKF_BASED, sp, protectorSecret, sid, userId); + syncState(userId); // ensure the new files are really saved to disk return protectorId; } @@ -996,6 +1011,7 @@ public class SyntheticPasswordManager { saveSecdiscardable(tokenHandle, tokenData.secdiscardableOnDisk, userId); createSyntheticPasswordBlob(tokenHandle, getTokenBasedProtectorType(tokenData.mType), sp, tokenData.aggregatedSecret, 0L, userId); + syncState(userId); // ensure the new files are really saved to disk tokenMap.get(userId).remove(tokenHandle); if (tokenData.mCallback != null) { tokenData.mCallback.onEscrowTokenActivated(tokenHandle, userId); @@ -1003,6 +1019,11 @@ public class SyntheticPasswordManager { return true; } + /** + * Creates a synthetic password blob, i.e. the file that stores the encrypted synthetic password + * (or encrypted escrow secret) for a protector. The caller is responsible for calling + * {@link syncState()} afterwards, once all the protector's files have been created. + */ private void createSyntheticPasswordBlob(long protectorId, byte protectorType, SyntheticPassword sp, byte[] protectorSecret, long sid, int userId) { final byte[] spSecret; @@ -1118,6 +1139,7 @@ public class SyntheticPasswordManager { // (getting rid of CREDENTIAL_TYPE_PASSWORD_OR_PIN) pwd.credentialType = credential.getType(); saveState(PASSWORD_DATA_NAME, pwd.toBytes(), protectorId, userId); + syncState(userId); synchronizeFrpPassword(pwd, 0, userId); } else { Slog.w(TAG, "Fail to re-enroll user password for user " + userId); @@ -1156,6 +1178,7 @@ public class SyntheticPasswordManager { if (result.syntheticPassword != null && !credential.isNone() && !hasPasswordMetrics(protectorId, userId)) { savePasswordMetrics(credential, result.syntheticPassword, protectorId, userId); + syncState(userId); // Not strictly needed as the upgrade can be re-done, but be safe. } return result; } @@ -1275,6 +1298,7 @@ public class SyntheticPasswordManager { + blob.mProtectorType); createSyntheticPasswordBlob(protectorId, blob.mProtectorType, result, protectorSecret, sid, userId); + syncState(userId); // Not strictly needed as the upgrade can be re-done, but be safe. } return result; } @@ -1396,12 +1420,21 @@ public class SyntheticPasswordManager { return ArrayUtils.concat(data, secdiscardable); } + /** + * Generates and writes the secdiscardable file for the given protector. The caller is + * responsible for calling {@link syncState()} afterwards, once all the protector's files have + * been created. + */ private byte[] createSecdiscardable(long protectorId, int userId) { byte[] data = secureRandom(SECDISCARDABLE_LENGTH); saveSecdiscardable(protectorId, data, userId); return data; } + /** + * Writes the secdiscardable file for the given protector. The caller is responsible for + * calling {@link syncState()} afterwards, once all the protector's files have been created. + */ private void saveSecdiscardable(long protectorId, byte[] secdiscardable, int userId) { saveState(SECDISCARDABLE_NAME, secdiscardable, protectorId, userId); } @@ -1445,6 +1478,11 @@ public class SyntheticPasswordManager { return VersionedPasswordMetrics.deserialize(decrypted).getMetrics(); } + /** + * Creates the password metrics file: the file associated with the LSKF-based protector that + * contains the encrypted metrics about the LSKF. The caller is responsible for calling + * {@link syncState()} afterwards if needed. + */ private void savePasswordMetrics(LockscreenCredential credential, SyntheticPassword sp, long protectorId, int userId) { final byte[] encrypted = SyntheticPasswordCrypto.encrypt(sp.deriveMetricsKey(), @@ -1466,10 +1504,21 @@ public class SyntheticPasswordManager { return mStorage.readSyntheticPasswordState(userId, protectorId, stateName); } + /** + * Persists the given synthetic password state for the given user ID and protector ID. + * <p> + * For performance reasons, this doesn't sync the user's synthetic password state directory. As + * a result, it doesn't guarantee that the file will really be present after a crash. If that + * is needed, call {@link syncState()} afterwards, preferably after batching up related updates. + */ private void saveState(String stateName, byte[] data, long protectorId, int userId) { mStorage.writeSyntheticPasswordState(userId, protectorId, stateName, data); } + private void syncState(int userId) { + mStorage.syncSyntheticPasswordState(userId); + } + private void destroyState(String stateName, long protectorId, int userId) { mStorage.deleteSyntheticPasswordState(userId, protectorId, stateName); } diff --git a/services/core/java/com/android/server/media/MediaButtonReceiverHolder.java b/services/core/java/com/android/server/media/MediaButtonReceiverHolder.java index dcdb881df12b..72ce38b72340 100644 --- a/services/core/java/com/android/server/media/MediaButtonReceiverHolder.java +++ b/services/core/java/com/android/server/media/MediaButtonReceiverHolder.java @@ -275,6 +275,10 @@ final class MediaButtonReceiverHolder { String.valueOf(mComponentType)); } + public ComponentName getComponentName() { + return mComponentName; + } + @ComponentType private static int getComponentType(PendingIntent pendingIntent) { if (pendingIntent.isBroadcast()) { diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index c3a555862d01..90135ad7684b 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -4967,7 +4967,16 @@ public class NotificationManagerService extends SystemService { } enforcePolicyAccess(Binder.getCallingUid(), "addAutomaticZenRule"); - return mZenModeHelper.addAutomaticZenRule(pkg, automaticZenRule, + // If the calling app is the system (from any user), take the package name from the + // rule's owner rather than from the caller's package. + String rulePkg = pkg; + if (isCallingAppIdSystem()) { + if (automaticZenRule.getOwner() != null) { + rulePkg = automaticZenRule.getOwner().getPackageName(); + } + } + + return mZenModeHelper.addAutomaticZenRule(rulePkg, automaticZenRule, "addAutomaticZenRule"); } @@ -9764,6 +9773,12 @@ public class NotificationManagerService extends SystemService { return uid == Process.SYSTEM_UID; } + protected boolean isCallingAppIdSystem() { + final int uid = Binder.getCallingUid(); + final int appid = UserHandle.getAppId(uid); + return appid == Process.SYSTEM_UID; + } + protected boolean isUidSystemOrPhone(int uid) { final int appid = UserHandle.getAppId(uid); return (appid == Process.SYSTEM_UID || appid == Process.PHONE_UID diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index f2c78ad1e82c..4b2c88c2bf06 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -326,7 +326,7 @@ public class ZenModeHelper { public String addAutomaticZenRule(String pkg, AutomaticZenRule automaticZenRule, String reason) { - if (!isSystemRule(automaticZenRule)) { + if (!ZenModeConfig.SYSTEM_AUTHORITY.equals(pkg)) { PackageItemInfo component = getServiceInfo(automaticZenRule.getOwner()); if (component == null) { component = getActivityInfo(automaticZenRule.getConfigurationActivity()); @@ -582,11 +582,6 @@ public class ZenModeHelper { } } - private boolean isSystemRule(AutomaticZenRule rule) { - return rule.getOwner() != null - && ZenModeConfig.SYSTEM_AUTHORITY.equals(rule.getOwner().getPackageName()); - } - private ServiceInfo getServiceInfo(ComponentName owner) { Intent queryIntent = new Intent(); queryIntent.setComponent(owner); diff --git a/services/core/java/com/android/server/om/OverlayManagerShellCommand.java b/services/core/java/com/android/server/om/OverlayManagerShellCommand.java index 5e98cc06d84b..978e43633b92 100644 --- a/services/core/java/com/android/server/om/OverlayManagerShellCommand.java +++ b/services/core/java/com/android/server/om/OverlayManagerShellCommand.java @@ -29,6 +29,7 @@ import android.content.res.AssetManager; import android.content.res.Resources; import android.content.res.TypedArray; import android.os.Binder; +import android.os.ParcelFileDescriptor; import android.os.Process; import android.os.RemoteException; import android.os.ShellCommand; @@ -64,7 +65,8 @@ final class OverlayManagerShellCommand extends ShellCommand { private final IOverlayManager mInterface; private static final Map<String, Integer> TYPE_MAP = Map.of( "color", TypedValue.TYPE_FIRST_COLOR_INT, - "string", TypedValue.TYPE_STRING); + "string", TypedValue.TYPE_STRING, + "drawable", -1); OverlayManagerShellCommand(@NonNull final Context ctx, @NonNull final IOverlayManager iom) { mContext = ctx; @@ -258,7 +260,7 @@ final class OverlayManagerShellCommand extends ShellCommand { String name = ""; String filename = null; String opt; - String configuration = null; + String config = null; while ((opt = getNextOption()) != null) { switch (opt) { case "--user": @@ -277,7 +279,7 @@ final class OverlayManagerShellCommand extends ShellCommand { filename = getNextArgRequired(); break; case "--config": - configuration = getNextArgRequired(); + config = getNextArgRequired(); break; default: err.println("Error: Unknown option: " + opt); @@ -312,7 +314,9 @@ final class OverlayManagerShellCommand extends ShellCommand { final String resourceName = getNextArgRequired(); final String typeStr = getNextArgRequired(); final String strData = String.join(" ", peekRemainingArgs()); - addOverlayValue(overlayBuilder, resourceName, typeStr, strData, configuration); + if (addOverlayValue(overlayBuilder, resourceName, typeStr, strData, config) != 0) { + return 1; + } } mInterface.commit(new OverlayManagerTransaction.Builder() @@ -369,8 +373,10 @@ final class OverlayManagerShellCommand extends ShellCommand { return 1; } String config = parser.getAttributeValue(null, "config"); - addOverlayValue(overlayBuilder, targetPackage + ':' + target, - overlayType, value, config); + if (addOverlayValue(overlayBuilder, targetPackage + ':' + target, + overlayType, value, config) != 0) { + return 1; + } } } } @@ -384,7 +390,7 @@ final class OverlayManagerShellCommand extends ShellCommand { return 0; } - private void addOverlayValue(FabricatedOverlay.Builder overlayBuilder, + private int addOverlayValue(FabricatedOverlay.Builder overlayBuilder, String resourceName, String typeString, String valueString, String configuration) { final int type; typeString = typeString.toLowerCase(Locale.getDefault()); @@ -399,6 +405,9 @@ final class OverlayManagerShellCommand extends ShellCommand { } if (type == TypedValue.TYPE_STRING) { overlayBuilder.setResourceValue(resourceName, type, valueString, configuration); + } else if (type < 0) { + ParcelFileDescriptor pfd = openFileForSystem(valueString, "r"); + overlayBuilder.setResourceValue(resourceName, pfd, configuration); } else { final int intData; if (valueString.startsWith("0x")) { @@ -408,6 +417,7 @@ final class OverlayManagerShellCommand extends ShellCommand { } overlayBuilder.setResourceValue(resourceName, type, intData, configuration); } + return 0; } private int runEnableExclusive() throws RemoteException { diff --git a/services/core/java/com/android/server/pm/AppsFilterBase.java b/services/core/java/com/android/server/pm/AppsFilterBase.java index 3b676c65db75..b4792c65bce5 100644 --- a/services/core/java/com/android/server/pm/AppsFilterBase.java +++ b/services/core/java/com/android/server/pm/AppsFilterBase.java @@ -44,7 +44,6 @@ import com.android.server.pm.pkg.PackageStateInternal; import com.android.server.pm.snapshot.PackageDataSnapshot; import com.android.server.utils.SnapshotCache; import com.android.server.utils.Watched; -import com.android.server.utils.WatchedArrayList; import com.android.server.utils.WatchedArrayMap; import com.android.server.utils.WatchedArraySet; import com.android.server.utils.WatchedSparseBooleanMatrix; @@ -179,9 +178,9 @@ public abstract class AppsFilterBase implements AppsFilterSnapshot { @NonNull @Watched - protected WatchedArrayList<String> mProtectedBroadcasts; + protected WatchedArraySet<String> mProtectedBroadcasts; @NonNull - protected SnapshotCache<WatchedArrayList<String>> mProtectedBroadcastsSnapshot; + protected SnapshotCache<WatchedArraySet<String>> mProtectedBroadcastsSnapshot; /** * This structure maps uid -> uid and indicates whether access from the first should be diff --git a/services/core/java/com/android/server/pm/AppsFilterImpl.java b/services/core/java/com/android/server/pm/AppsFilterImpl.java index 2e67bf2fcdf7..c97711b3aa80 100644 --- a/services/core/java/com/android/server/pm/AppsFilterImpl.java +++ b/services/core/java/com/android/server/pm/AppsFilterImpl.java @@ -73,7 +73,6 @@ import com.android.server.utils.Snappable; import com.android.server.utils.SnapshotCache; import com.android.server.utils.Watchable; import com.android.server.utils.WatchableImpl; -import com.android.server.utils.WatchedArrayList; import com.android.server.utils.WatchedArraySet; import com.android.server.utils.WatchedSparseBooleanMatrix; import com.android.server.utils.WatchedSparseSetArray; @@ -223,7 +222,7 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, mForceQueryable = new WatchedArraySet<>(); mForceQueryableSnapshot = new SnapshotCache.Auto<>( mForceQueryable, mForceQueryable, "AppsFilter.mForceQueryable"); - mProtectedBroadcasts = new WatchedArrayList<>(); + mProtectedBroadcasts = new WatchedArraySet<>(); mProtectedBroadcastsSnapshot = new SnapshotCache.Auto<>( mProtectedBroadcasts, mProtectedBroadcasts, "AppsFilter.mProtectedBroadcasts"); mPermissionToUids = new HashMap<>(); @@ -573,13 +572,17 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, return null; } - final boolean protectedBroadcastsChanged; - synchronized (mProtectedBroadcastsLock) { - protectedBroadcastsChanged = - mProtectedBroadcasts.addAll(newPkg.getProtectedBroadcasts()); - } - if (protectedBroadcastsChanged) { - mQueriesViaComponentRequireRecompute.set(true); + final List<String> newBroadcasts = newPkg.getProtectedBroadcasts(); + if (newBroadcasts.size() != 0) { + final boolean protectedBroadcastsChanged; + synchronized (mProtectedBroadcastsLock) { + final int oldSize = mProtectedBroadcasts.size(); + mProtectedBroadcasts.addAll(newBroadcasts); + protectedBroadcastsChanged = mProtectedBroadcasts.size() != oldSize; + } + if (protectedBroadcastsChanged) { + mQueriesViaComponentRequireRecompute.set(true); + } } final boolean newIsForceQueryable; @@ -1149,7 +1152,12 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, final ArrayList<String> protectedBroadcasts = new ArrayList<>( mProtectedBroadcasts.untrackedStorage()); collectProtectedBroadcasts(settings, removingPackageName); - protectedBroadcastsChanged = !mProtectedBroadcasts.containsAll(protectedBroadcasts); + for (int i = 0; i < protectedBroadcasts.size(); ++i) { + if (!mProtectedBroadcasts.contains(protectedBroadcasts.get(i))) { + protectedBroadcastsChanged = true; + break; + } + } } } diff --git a/services/core/java/com/android/server/pm/AppsFilterUtils.java b/services/core/java/com/android/server/pm/AppsFilterUtils.java index 7daa0b94000c..483fa8ade150 100644 --- a/services/core/java/com/android/server/pm/AppsFilterUtils.java +++ b/services/core/java/com/android/server/pm/AppsFilterUtils.java @@ -29,7 +29,7 @@ import com.android.server.pm.pkg.component.ParsedComponent; import com.android.server.pm.pkg.component.ParsedIntentInfo; import com.android.server.pm.pkg.component.ParsedMainComponent; import com.android.server.pm.pkg.component.ParsedProvider; -import com.android.server.utils.WatchedArrayList; +import com.android.server.utils.WatchedArraySet; import java.util.List; import java.util.Set; @@ -45,7 +45,7 @@ final class AppsFilterUtils { /** Returns true if the querying package may query for the potential target package */ public static boolean canQueryViaComponents(AndroidPackage querying, - AndroidPackage potentialTarget, WatchedArrayList<String> protectedBroadcasts) { + AndroidPackage potentialTarget, WatchedArraySet<String> protectedBroadcasts) { if (!querying.getQueriesIntents().isEmpty()) { for (Intent intent : querying.getQueriesIntents()) { if (matchesPackage(intent, potentialTarget, protectedBroadcasts)) { @@ -117,7 +117,7 @@ final class AppsFilterUtils { } private static boolean matchesPackage(Intent intent, AndroidPackage potentialTarget, - WatchedArrayList<String> protectedBroadcasts) { + WatchedArraySet<String> protectedBroadcasts) { if (matchesAnyComponents( intent, potentialTarget.getServices(), null /*protectedBroadcasts*/)) { return true; @@ -138,7 +138,7 @@ final class AppsFilterUtils { private static boolean matchesAnyComponents(Intent intent, List<? extends ParsedMainComponent> components, - WatchedArrayList<String> protectedBroadcasts) { + WatchedArraySet<String> protectedBroadcasts) { for (int i = ArrayUtils.size(components) - 1; i >= 0; i--) { ParsedMainComponent component = components.get(i); if (!component.isExported()) { @@ -152,7 +152,7 @@ final class AppsFilterUtils { } private static boolean matchesAnyFilter(Intent intent, ParsedComponent component, - WatchedArrayList<String> protectedBroadcasts) { + WatchedArraySet<String> protectedBroadcasts) { List<ParsedIntentInfo> intents = component.getIntents(); for (int i = ArrayUtils.size(intents) - 1; i >= 0; i--) { IntentFilter intentFilter = intents.get(i).getIntentFilter(); @@ -164,7 +164,7 @@ final class AppsFilterUtils { } private static boolean matchesIntentFilter(Intent intent, IntentFilter intentFilter, - @Nullable WatchedArrayList<String> protectedBroadcasts) { + @Nullable WatchedArraySet<String> protectedBroadcasts) { return intentFilter.match(intent.getAction(), intent.getType(), intent.getScheme(), intent.getData(), intent.getCategories(), "AppsFilter", true, protectedBroadcasts != null ? protectedBroadcasts.untrackedStorage() : null) > 0; diff --git a/services/core/java/com/android/server/pm/Computer.java b/services/core/java/com/android/server/pm/Computer.java index bf00a33d7d20..5b8ee2b085cc 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 @@ public interface Computer extends PackageDataSnapshot { 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 b2851369b56e..a8534b0b497c 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 @@ public class ComputerEngine implements Computer { } /** + * 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 @@ public class ComputerEngine implements Computer { 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 798217f226cf..04bd135cc77e 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 @@ class CrossProfileIntentFilter extends WatchedIntentFilter { //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 5ae4cab8e7a6..4362956b3c09 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.FeatureFlagUtils; 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 @@ public class CrossProfileIntentResolverEngine { 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<>(); - - 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); + while (!pendingUsers.isEmpty()) { + int currentUserId = pendingUsers.poll(); + List<CrossProfileIntentFilter> matchingFilters = + computer.getMatchingCrossProfileIntentFilters(intent, resolvedType, + currentUserId); + + 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++) { + + int targetUserId = crossProfileIntentFiltersByUser.keyAt(index); + + //if user is already visited then skip resolution for particular user. + if (visitedUserIds.contains(targetUserId)) { + continue; + } - UserInfo targetUserInfo = umInternal.getUserInfo(crossProfileIntentFiltersByUser - .keyAt(index)); + UserInfo targetUserInfo = umInternal.getUserInfo(targetUserId); - // Choosing strategy based on source and target user - CrossProfileResolver crossProfileResolver = - chooseCrossProfileResolver(computer, sourceUserInfo, targetUserInfo); + // 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 @@ public class CrossProfileIntentResolverEngine { /** * 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 @@ public class CrossProfileIntentResolverEngine { @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 @@ public class CrossProfileIntentResolverEngine { 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 cac93236f411..ceaaefd4085a 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 @@ public class DefaultCrossProfileIntentFiltersUtils { 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/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index 7dae4c64cfd8..2a0b44d43bd7 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -150,7 +150,6 @@ import android.util.SparseIntArray; import com.android.internal.annotations.GuardedBy; import com.android.internal.content.F2fsUtils; -import com.android.internal.content.InstallLocationUtils; import com.android.internal.security.VerityUtils; import com.android.internal.util.ArrayUtils; import com.android.internal.util.CollectionUtils; @@ -875,7 +874,7 @@ final class InstallPackageHelper { } } - Map<String, ReconciledPackage> reconciledPackages; + List<ReconciledPackage> reconciledPackages; synchronized (mPm.mLock) { try { Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "reconcilePackages"); @@ -1883,11 +1882,11 @@ final class InstallPackageHelper { } @GuardedBy("mPm.mLock") - private void commitPackagesLocked(Map<String, ReconciledPackage> reconciledPackages, + private void commitPackagesLocked(List<ReconciledPackage> reconciledPackages, @NonNull int[] allUsers) { // TODO: remove any expected failures from this method; this should only be able to fail due // to unavoidable errors (I/O, etc.) - for (ReconciledPackage reconciledPkg : reconciledPackages.values()) { + for (ReconciledPackage reconciledPkg : reconciledPackages) { final InstallRequest installRequest = reconciledPkg.mInstallRequest; final ParsedPackage parsedPackage = installRequest.getParsedPackage(); final String packageName = parsedPackage.getPackageName(); @@ -2205,9 +2204,9 @@ final class InstallPackageHelper { * locks on {@link com.android.server.pm.PackageManagerService.mLock}. */ @GuardedBy("mPm.mInstallLock") - private void executePostCommitStepsLIF(Map<String, ReconciledPackage> reconciledPackages) { + private void executePostCommitStepsLIF(List<ReconciledPackage> reconciledPackages) { final ArraySet<IncrementalStorage> incrementalStorages = new ArraySet<>(); - for (ReconciledPackage reconciledPkg : reconciledPackages.values()) { + for (ReconciledPackage reconciledPkg : reconciledPackages) { final InstallRequest installRequest = reconciledPkg.mInstallRequest; final boolean instantApp = ((installRequest.getScanFlags() & SCAN_AS_INSTANT_APP) != 0); final boolean isApex = ((installRequest.getScanFlags() & SCAN_AS_APEX) != 0); @@ -2338,45 +2337,6 @@ final class InstallPackageHelper { incrementalStorages); } - public int installLocationPolicy(PackageInfoLite pkgLite, int installFlags) { - String packageName = pkgLite.packageName; - int installLocation = pkgLite.installLocation; - // reader - synchronized (mPm.mLock) { - // Currently installed package which the new package is attempting to replace or - // null if no such package is installed. - AndroidPackage installedPkg = mPm.mPackages.get(packageName); - - if (installedPkg != null) { - if ((installFlags & PackageManager.INSTALL_REPLACE_EXISTING) != 0) { - // Check for updated system application. - if (installedPkg.isSystem()) { - return InstallLocationUtils.RECOMMEND_INSTALL_INTERNAL; - } else { - // If current upgrade specifies particular preference - if (installLocation == PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) { - // Application explicitly specified internal. - return InstallLocationUtils.RECOMMEND_INSTALL_INTERNAL; - } else if ( - installLocation == PackageInfo.INSTALL_LOCATION_PREFER_EXTERNAL) { - // App explicitly prefers external. Let policy decide - } else { - // Prefer previous location - if (installedPkg.isExternalStorage()) { - return InstallLocationUtils.RECOMMEND_INSTALL_EXTERNAL; - } - return InstallLocationUtils.RECOMMEND_INSTALL_INTERNAL; - } - } - } else { - // Invalid install. Return error code - return InstallLocationUtils.RECOMMEND_FAILED_ALREADY_EXISTS; - } - } - } - return pkgLite.recommendedInstallLocation; - } - Pair<Integer, String> verifyReplacingVersionCode(PackageInfoLite pkgLite, long requiredInstalledVersionCode, int installFlags) { if ((installFlags & PackageManager.INSTALL_APEX) != 0) { @@ -3645,7 +3605,7 @@ final class InstallPackageHelper { boolean appIdCreated = false; try { final String pkgName = scanResult.mPkgSetting.getPackageName(); - final Map<String, ReconciledPackage> reconcileResult = + final List<ReconciledPackage> reconcileResult = ReconcilePackageUtils.reconcilePackages( Collections.singletonList(installRequest), mPm.mPackages, Collections.singletonMap(pkgName, @@ -3657,7 +3617,7 @@ final class InstallPackageHelper { } else { installRequest.setScannedPackageSettingAppId(Process.INVALID_UID); } - commitReconciledScanResultLocked(reconcileResult.get(pkgName), + commitReconciledScanResultLocked(reconcileResult.get(0), mPm.mUserManager.getUserIds()); } catch (PackageManagerException e) { if (appIdCreated) { diff --git a/services/core/java/com/android/server/pm/InstallRequest.java b/services/core/java/com/android/server/pm/InstallRequest.java index 4443710dd08b..01a8bd0a4225 100644 --- a/services/core/java/com/android/server/pm/InstallRequest.java +++ b/services/core/java/com/android/server/pm/InstallRequest.java @@ -59,8 +59,10 @@ final class InstallRequest { @Nullable private PackageRemovedInfo mRemovedInfo; - private @PackageManagerService.ScanFlags int mScanFlags; - private @ParsingPackageUtils.ParseFlags int mParseFlags; + @PackageManagerService.ScanFlags + private int mScanFlags; + @ParsingPackageUtils.ParseFlags + private int mParseFlags; private boolean mReplace; @Nullable /* The original Package if it is being replaced, otherwise {@code null} */ @@ -155,7 +157,7 @@ final class InstallRequest { mParseFlags = parseFlags; mScanFlags = scanFlags; mScanResult = scanResult; - mPackageMetrics = null; // No real logging from this code path + mPackageMetrics = null; // No logging from this code path } @Nullable @@ -393,11 +395,13 @@ final class InstallRequest { return mParsedPackage; } - public @ParsingPackageUtils.ParseFlags int getParseFlags() { + @ParsingPackageUtils.ParseFlags + public int getParseFlags() { return mParseFlags; } - public @PackageManagerService.ScanFlags int getScanFlags() { + @PackageManagerService.ScanFlags + public int getScanFlags() { return mScanFlags; } @@ -435,6 +439,11 @@ final class InstallRequest { return mIsInstallForUsers; } + public boolean isInstallFromAdb() { + return mInstallArgs != null + && (mInstallArgs.mInstallFlags & PackageManager.INSTALL_FROM_ADB) != 0; + } + @Nullable public PackageSetting getOriginalPackageSetting() { return mOriginalPs; @@ -731,10 +740,10 @@ final class InstallRequest { } } - public void onInstallCompleted() { + public void onInstallCompleted(Computer snapshot) { if (getReturnCode() == INSTALL_SUCCEEDED) { if (mPackageMetrics != null) { - mPackageMetrics.onInstallSucceed(); + mPackageMetrics.onInstallSucceed(snapshot); } } } diff --git a/services/core/java/com/android/server/pm/InstallingSession.java b/services/core/java/com/android/server/pm/InstallingSession.java index d13822a4d98c..e4a0a3ab8dfa 100644 --- a/services/core/java/com/android/server/pm/InstallingSession.java +++ b/services/core/java/com/android/server/pm/InstallingSession.java @@ -510,7 +510,7 @@ class InstallingSession { mInstallPackageHelper.installPackagesTraced(installRequests); for (InstallRequest request : installRequests) { - request.onInstallCompleted(); + request.onInstallCompleted(mPm.snapshotComputer()); doPostInstall(request); } } diff --git a/services/core/java/com/android/server/pm/PackageMetrics.java b/services/core/java/com/android/server/pm/PackageMetrics.java index b725325eaaa9..0391163856b2 100644 --- a/services/core/java/com/android/server/pm/PackageMetrics.java +++ b/services/core/java/com/android/server/pm/PackageMetrics.java @@ -16,16 +16,26 @@ package com.android.server.pm; +import static android.os.Process.INVALID_UID; + import android.annotation.IntDef; +import android.content.pm.parsing.ApkLiteParseUtils; import android.os.UserManager; import android.util.Pair; import android.util.SparseArray; import com.android.internal.util.FrameworkStatsLog; +import com.android.server.pm.pkg.PackageStateInternal; +import java.io.File; +import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Stream; /** * Metrics class for reporting stats to logging infrastructures like Westworld @@ -43,7 +53,8 @@ final class PackageMetrics { STEP_COMMIT, }) @Retention(RetentionPolicy.SOURCE) - public @interface StepInt {} + public @interface StepInt { + } private final long mInstallStartTimestampMillis; private final SparseArray<InstallStep> mInstallSteps = new SparseArray<>(); @@ -56,16 +67,30 @@ final class PackageMetrics { mInstallRequest = installRequest; } - public void onInstallSucceed() { + public void onInstallSucceed(Computer snapshot) { + // TODO(b/239722919): report to SecurityLog if on work profile or managed device + reportInstallationStats(snapshot, true /* success */); + } + + private void reportInstallationStats(Computer snapshot, boolean success) { + // TODO(b/249294752): do not log if adb final long installDurationMillis = System.currentTimeMillis() - mInstallStartTimestampMillis; // write to stats final Pair<int[], long[]> stepDurations = getInstallStepDurations(); final int[] newUsers = mInstallRequest.getNewUsers(); final int[] originalUsers = mInstallRequest.getOriginUsers(); + final String packageName = mInstallRequest.getName(); + final String installerPackageName = mInstallRequest.getInstallerPackageName(); + final int installerUid = installerPackageName == null ? INVALID_UID + : snapshot.getPackageUid(installerPackageName, 0, 0); + final PackageStateInternal ps = snapshot.getPackageStateInternal(packageName); + final long versionCode = success ? 0 : ps.getVersionCode(); + final long apksSize = getApksSize(ps.getPath()); + FrameworkStatsLog.write(FrameworkStatsLog.PACKAGE_INSTALLATION_SESSION_REPORTED, 0 /* session_id */, - null /* package_name */, + success ? null : packageName /* not report package_name on success */, mInstallRequest.getUid() /* uid */, newUsers /* user_ids */, getUserTypes(newUsers) /* user_types */, @@ -73,13 +98,13 @@ final class PackageMetrics { getUserTypes(originalUsers) /* original_user_types */, mInstallRequest.getReturnCode() /* public_return_code */, 0 /* internal_error_code */, - 0 /* apks_size_bytes */, - 0 /* version_code */, + apksSize /* apks_size_bytes */, + versionCode /* version_code */, stepDurations.first /* install_steps */, stepDurations.second /* step_duration_millis */, installDurationMillis /* total_duration_millis */, mInstallRequest.getInstallFlags() /* install_flags */, - -1 /* installer_package_uid */, + installerUid /* installer_package_uid */, -1 /* original_installer_package_uid */, mInstallRequest.getDataLoaderType() /* data_loader_type */, 0 /* user_action_required_type */, @@ -93,6 +118,19 @@ final class PackageMetrics { ); } + private long getApksSize(File apkDir) { + // TODO(b/249294752): also count apk sizes for failed installs + final AtomicLong apksSize = new AtomicLong(); + try (Stream<Path> walkStream = Files.walk(apkDir.toPath())) { + walkStream.filter(p -> p.toFile().isFile() + && ApkLiteParseUtils.isApkFile(p.toFile())).forEach( + f -> apksSize.addAndGet(f.toFile().length())); + } catch (IOException e) { + // ignore + } + return apksSize.get(); + } + public void onStepStarted(@StepInt int step) { mInstallSteps.put(step, new InstallStep()); } @@ -140,12 +178,15 @@ final class PackageMetrics { private static class InstallStep { private final long mStartTimestampMillis; private long mDurationMillis = -1; + InstallStep() { mStartTimestampMillis = System.currentTimeMillis(); } + void finish() { mDurationMillis = System.currentTimeMillis() - mStartTimestampMillis; } + long getDurationMillis() { return mDurationMillis; } diff --git a/services/core/java/com/android/server/pm/ReconcilePackageUtils.java b/services/core/java/com/android/server/pm/ReconcilePackageUtils.java index 01d17f6444f5..99bcbc9f95e6 100644 --- a/services/core/java/com/android/server/pm/ReconcilePackageUtils.java +++ b/services/core/java/com/android/server/pm/ReconcilePackageUtils.java @@ -35,6 +35,7 @@ import com.android.server.pm.pkg.AndroidPackage; import com.android.server.pm.pkg.parsing.ParsingPackageUtils; import com.android.server.utils.WatchedLongSparseArray; +import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -48,14 +49,14 @@ import java.util.Map; * as install) led to the request. */ final class ReconcilePackageUtils { - public static Map<String, ReconciledPackage> reconcilePackages( + public static List<ReconciledPackage> reconcilePackages( List<InstallRequest> installRequests, Map<String, AndroidPackage> allPackages, Map<String, Settings.VersionInfo> versionInfos, SharedLibrariesImpl sharedLibraries, KeySetManagerService ksms, Settings settings) throws ReconcileFailure { - final Map<String, ReconciledPackage> result = new ArrayMap<>(installRequests.size()); + final List<ReconciledPackage> result = new ArrayList<>(installRequests.size()); // make a copy of the existing set of packages so we can combine them with incoming packages final ArrayMap<String, AndroidPackage> combinedPackages = @@ -88,7 +89,6 @@ final class ReconcilePackageUtils { } - final DeletePackageAction deletePackageAction; // we only want to try to delete for non system apps if (installRequest.isInstallReplace() && !installRequest.isInstallSystem()) { @@ -257,13 +257,11 @@ final class ReconcilePackageUtils { } } - result.put(installPackageName, + final ReconciledPackage reconciledPackage = new ReconciledPackage(installRequests, allPackages, installRequest, deletePackageAction, allowedSharedLibInfos, signingDetails, - sharedUserSignaturesChanged, removeAppKeySetData)); - } + sharedUserSignaturesChanged, removeAppKeySetData); - for (InstallRequest installRequest : installRequests) { // Check all shared libraries and map to their actual file path. // We only do this here for apps not on a system dir, because those // are the only ones that can fail an install due to this. We @@ -271,24 +269,21 @@ final class ReconcilePackageUtils { // library paths after the scan is done. Also during the initial // scan don't update any libs as we do this wholesale after all // apps are scanned to avoid dependency based scanning. - if ((installRequest.getScanFlags() & SCAN_BOOTING) != 0 - || (installRequest.getParseFlags() & ParsingPackageUtils.PARSE_IS_SYSTEM_DIR) - != 0) { - continue; - } - final String installPackageName = installRequest.getParsedPackage().getPackageName(); - try { - result.get(installPackageName).mCollectedSharedLibraryInfos = - sharedLibraries.collectSharedLibraryInfos( - installRequest.getParsedPackage(), combinedPackages, - incomingSharedLibraries); - } catch (PackageManagerException e) { - throw new ReconcileFailure(e.error, e.getMessage()); + if ((installRequest.getScanFlags() & SCAN_BOOTING) == 0 + && (installRequest.getParseFlags() & ParsingPackageUtils.PARSE_IS_SYSTEM_DIR) + == 0) { + try { + reconciledPackage.mCollectedSharedLibraryInfos = + sharedLibraries.collectSharedLibraryInfos( + installRequest.getParsedPackage(), combinedPackages, + incomingSharedLibraries); + } catch (PackageManagerException e) { + throw new ReconcileFailure(e.error, e.getMessage()); + } } - } - for (InstallRequest installRequest : installRequests) { installRequest.onReconcileFinished(); + result.add(reconciledPackage); } return result; diff --git a/services/core/java/com/android/server/pm/UserManagerInternal.java b/services/core/java/com/android/server/pm/UserManagerInternal.java index 91558308e305..9dafcceefdd0 100644 --- a/services/core/java/com/android/server/pm/UserManagerInternal.java +++ b/services/core/java/com/android/server/pm/UserManagerInternal.java @@ -25,6 +25,7 @@ import android.content.pm.UserProperties; import android.graphics.Bitmap; import android.os.Bundle; import android.os.UserManager; +import android.util.DebugUtils; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -46,6 +47,18 @@ public abstract class UserManagerInternal { public @interface OwnerType { } + public static final int USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE = 1; + public static final int USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE = 2; + public static final int USER_ASSIGNMENT_RESULT_FAILURE = -1; + + private static final String PREFIX_USER_ASSIGNMENT_RESULT = "USER_ASSIGNMENT_RESULT"; + @IntDef(flag = false, prefix = {PREFIX_USER_ASSIGNMENT_RESULT}, value = { + USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE, + USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE, + USER_ASSIGNMENT_RESULT_FAILURE + }) + public @interface UserAssignmentResult {} + public interface UserRestrictionsListener { /** * Called when a user restriction changes. @@ -343,34 +356,28 @@ public abstract class UserManagerInternal { public abstract @Nullable UserProperties getUserProperties(@UserIdInt int userId); /** - * Assigns a user to a display. - * - * <p>On most devices this call will be a no-op, but it will be used on devices that support - * multiple users on multiple displays (like automotives with passenger displays). + * Assigns a user to a display when it's starting, returning whether the assignment succeeded + * and the user is {@link UserManager#isUserVisible() visible}. * * <p><b>NOTE: </b>this method is meant to be used only by {@code UserController} (when a user - * is started) + * is started). If other clients (like {@code CarService} need to explicitly change the user / + * display assignment, we'll need to provide other APIs. * * <p><b>NOTE: </b>this method doesn't validate if the display exists, it's up to the caller to - * check it. In fact, one of the intended clients for this method is - * {@code DisplayManagerService}, which will call it when a virtual display is created (another - * client is {@code UserController}, which will call it when a user is started). + * pass a valid display id. */ - // TODO(b/244644281): rename to assignUserToDisplayOnStart() and make sure it's called on boot - // as well - public abstract void assignUserToDisplay(@UserIdInt int userId, @UserIdInt int profileGroupId, + public abstract @UserAssignmentResult int assignUserToDisplayOnStart(@UserIdInt int userId, + @UserIdInt int profileGroupId, boolean foreground, int displayId); /** - * Unassigns a user from its current display. - * - * <p>On most devices this call will be a no-op, but it will be used on devices that support - * multiple users on multiple displays (like automotives with passenger displays). + * Unassigns a user from its current display when it's stopping. * * <p><b>NOTE: </b>this method is meant to be used only by {@code UserController} (when a user - * is stopped). + * is stopped). If other clients (like {@code CarService} need to explicitly change the user / + * display assignment, we'll need to provide other APIs. */ - public abstract void unassignUserFromDisplay(@UserIdInt int userId); + public abstract void unassignUserFromDisplayOnStop(@UserIdInt int userId); /** * Returns {@code true} if the user is visible (as defined by @@ -413,6 +420,15 @@ public abstract class UserManagerInternal { */ public abstract @UserIdInt int getUserAssignedToDisplay(int displayId); + /** + * Gets the user-friendly representation of the {@code result} of a + * {@link #assignUserToDisplayOnStart(int, int, boolean, int)} call. + */ + public static String userAssignmentResultToString(@UserAssignmentResult int result) { + return DebugUtils.constantToString(UserManagerInternal.class, PREFIX_USER_ASSIGNMENT_RESULT, + result); + } + /** Adds a {@link UserVisibilityListener}. */ public abstract void addUserVisibilityListener(UserVisibilityListener listener); diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index d25566980fbb..4a4a231f4fba 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -18,6 +18,7 @@ package com.android.server.pm; import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; +import static android.os.UserManager.DEV_CREATE_OVERRIDE_PROPERTY; import static android.os.UserManager.DISALLOW_USER_SWITCH; import static android.os.UserManager.SYSTEM_USER_MODE_EMULATION_PROPERTY; @@ -264,7 +265,7 @@ public class UserManagerService extends IUserManager.Stub { @VisibleForTesting static final int MAX_RECENTLY_REMOVED_IDS_SIZE = 100; - private static final int USER_VERSION = 10; + private static final int USER_VERSION = 11; private static final long EPOCH_PLUS_30_YEARS = 30L * 365 * 24 * 60 * 60 * 1000L; // ms @@ -2814,7 +2815,8 @@ public class UserManagerService extends IUserManager.Stub { synchronized (mUsersLock) { count = getAliveUsersExcludingGuestsCountLU(); } - return count >= UserManager.getMaxSupportedUsers(); + return count >= UserManager.getMaxSupportedUsers() + && !isCreationOverrideEnabled(); } /** @@ -2824,15 +2826,16 @@ public class UserManagerService extends IUserManager.Stub { * <p>For checking whether more profiles can be added to a particular parent use * {@link #canAddMoreProfilesToUser}. */ - private boolean canAddMoreUsersOfType(UserTypeDetails userTypeDetails) { - if (!userTypeDetails.isEnabled()) { + private boolean canAddMoreUsersOfType(@NonNull UserTypeDetails userTypeDetails) { + if (!isUserTypeEnabled(userTypeDetails)) { return false; } final int max = userTypeDetails.getMaxAllowed(); if (max == UserTypeDetails.UNLIMITED_NUMBER_OF_USERS) { return true; // Indicates that there is no max. } - return getNumberOfUsersOfType(userTypeDetails.getName()) < max; + return getNumberOfUsersOfType(userTypeDetails.getName()) < max + || isCreationOverrideEnabled(); } /** @@ -2843,7 +2846,7 @@ public class UserManagerService extends IUserManager.Stub { public int getRemainingCreatableUserCount(String userType) { checkQueryOrCreateUsersPermission("get the remaining number of users that can be added."); final UserTypeDetails type = mUserTypes.get(userType); - if (type == null || !type.isEnabled()) { + if (type == null || !isUserTypeEnabled(type)) { return 0; } synchronized (mUsersLock) { @@ -2917,7 +2920,21 @@ public class UserManagerService extends IUserManager.Stub { public boolean isUserTypeEnabled(String userType) { checkCreateUsersPermission("check if user type is enabled."); final UserTypeDetails userTypeDetails = mUserTypes.get(userType); - return userTypeDetails != null && userTypeDetails.isEnabled(); + return userTypeDetails != null && isUserTypeEnabled(userTypeDetails); + } + + /** Returns whether the creation of users of the given user type is enabled on this device. */ + private boolean isUserTypeEnabled(@NonNull UserTypeDetails userTypeDetails) { + return userTypeDetails.isEnabled() || isCreationOverrideEnabled(); + } + + /** + * Returns whether to almost-always allow creating users even beyond their limit or if disabled. + * For Debug builds only. + */ + private boolean isCreationOverrideEnabled() { + return Build.isDebuggable() + && SystemProperties.getBoolean(DEV_CREATE_OVERRIDE_PROPERTY, false); } @Override @@ -2930,7 +2947,8 @@ public class UserManagerService extends IUserManager.Stub { @Override public boolean canAddMoreProfilesToUser(String userType, @UserIdInt int userId, boolean allowedToRemoveOne) { - return 0 < getRemainingCreatableProfileCount(userType, userId, allowedToRemoveOne); + return 0 < getRemainingCreatableProfileCount(userType, userId, allowedToRemoveOne) + || isCreationOverrideEnabled(); } @Override @@ -2948,7 +2966,7 @@ public class UserManagerService extends IUserManager.Stub { checkQueryOrCreateUsersPermission( "get the remaining number of profiles that can be added to the given user."); final UserTypeDetails type = mUserTypes.get(userType); - if (type == null || !type.isEnabled()) { + if (type == null || !isUserTypeEnabled(type)) { return 0; } // Managed profiles have their own specific rules. @@ -3337,6 +3355,7 @@ public class UserManagerService extends IUserManager.Stub { final int oldFlags = systemUserData.info.flags; final int newFlags; final String newUserType; + // TODO(b/256624031): Also handle FLAG_MAIN if (newHeadlessSystemUserMode) { newUserType = UserManager.USER_TYPE_SYSTEM_HEADLESS; newFlags = oldFlags & ~UserInfo.FLAG_FULL; @@ -3629,6 +3648,22 @@ public class UserManagerService extends IUserManager.Stub { userVersion = 10; } + if (userVersion < 11) { + // Add FLAG_MAIN + if (isHeadlessSystemUserMode()) { + final UserInfo earliestCreatedUser = getEarliestCreatedFullUser(); + earliestCreatedUser.flags |= UserInfo.FLAG_MAIN; + userIdsToWrite.add(earliestCreatedUser.id); + } else { + synchronized (mUsersLock) { + final UserData userData = mUsers.get(UserHandle.USER_SYSTEM); + userData.info.flags |= UserInfo.FLAG_MAIN; + userIdsToWrite.add(userData.info.id); + } + } + userVersion = 11; + } + // Reminder: If you add another upgrade, make sure to increment USER_VERSION too. // Done with userVersion changes, moving on to deal with userTypeVersion upgrades @@ -3758,6 +3793,21 @@ public class UserManagerService extends IUserManager.Stub { userInfo.profileBadge = getFreeProfileBadgeLU(userInfo.profileGroupId, userInfo.userType); } + private UserInfo getEarliestCreatedFullUser() { + final List<UserInfo> users = getUsersInternal(true, true, true); + UserInfo earliestUser = users.get(0); + long earliestCreationTime = earliestUser.creationTime; + for (int i = 0; i < users.size(); i++) { + final UserInfo info = users.get(i); + if (info.isFull() && info.isAdmin() && info.creationTime > 0 + && info.creationTime < earliestCreationTime) { + earliestCreationTime = info.creationTime; + earliestUser = info; + } + } + return earliestUser; + } + @GuardedBy({"mPackagesLock"}) private void fallbackToSingleUserLP() { int flags = UserInfo.FLAG_SYSTEM | UserInfo.FLAG_INITIALIZED | UserInfo.FLAG_ADMIN @@ -4407,7 +4457,7 @@ public class UserManagerService extends IUserManager.Stub { + ") indicated SYSTEM user, which cannot be created."); return null; } - if (!userTypeDetails.isEnabled()) { + if (!isUserTypeEnabled(userTypeDetails)) { throwCheckedUserOperationException( "Cannot add a user of disabled type " + userType + ".", UserManager.USER_OPERATION_ERROR_MAX_USERS); @@ -4479,27 +4529,12 @@ public class UserManagerService extends IUserManager.Stub { + " for user " + parentId, UserManager.USER_OPERATION_ERROR_MAX_USERS); } - // In legacy mode, restricted profile's parent can only be the owner user - if (isRestricted && !UserManager.isSplitSystemUser() - && (parentId != UserHandle.USER_SYSTEM)) { + if (isRestricted && (parentId != UserHandle.USER_SYSTEM) + && !isCreationOverrideEnabled()) { throwCheckedUserOperationException( - "Cannot add restricted profile - parent user must be owner", + "Cannot add restricted profile - parent user must be system", UserManager.USER_OPERATION_ERROR_UNKNOWN); } - if (isRestricted && UserManager.isSplitSystemUser()) { - if (parent == null) { - throwCheckedUserOperationException( - "Cannot add restricted profile - parent user must be specified", - UserManager.USER_OPERATION_ERROR_UNKNOWN); - } - if (!parent.info.canHaveProfile()) { - throwCheckedUserOperationException( - "Cannot add restricted profile - profiles cannot be created for " - + "the specified parent user id " - + parentId, - UserManager.USER_OPERATION_ERROR_UNKNOWN); - } - } userId = getNextAvailableId(); Slog.i(LOG_TAG, "Creating user " + userId + " of type " + userType); @@ -6799,15 +6834,14 @@ public class UserManagerService extends IUserManager.Stub { } @Override - public void assignUserToDisplay(@UserIdInt int userId, @UserIdInt int profileGroupId, + public int assignUserToDisplayOnStart(@UserIdInt int userId, @UserIdInt int profileGroupId, boolean foreground, int displayId) { - mUserVisibilityMediator.startUser(userId, profileGroupId, foreground, displayId); - mUserVisibilityMediator.assignUserToDisplay(userId, profileGroupId, displayId); + return mUserVisibilityMediator.startUser(userId, profileGroupId, foreground, + displayId); } @Override - public void unassignUserFromDisplay(@UserIdInt int userId) { - mUserVisibilityMediator.unassignUserFromDisplay(userId); + public void unassignUserFromDisplayOnStop(@UserIdInt int userId) { mUserVisibilityMediator.stopUser(userId); } diff --git a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java index dbd026efed8a..27d74d517fb9 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 @@ public class UserRestrictionsUtils { 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 @@ public class UserRestrictionsUtils { 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 @@ public class UserRestrictionsUtils { 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 c35fe174415d..8fb5773706c5 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 @@ public final class UserTypeFactory { .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)); } /** @@ -260,7 +262,8 @@ public final class UserTypeFactory { private static UserTypeDetails.Builder getDefaultTypeFullSystem() { return new UserTypeDetails.Builder() .setName(USER_TYPE_FULL_SYSTEM) - .setBaseType(FLAG_SYSTEM | FLAG_FULL); + .setBaseType(FLAG_SYSTEM | FLAG_FULL) + .setDefaultUserInfoPropertyFlags(UserInfo.FLAG_MAIN); } /** @@ -310,6 +313,10 @@ public final class UserTypeFactory { 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. @@ -391,6 +398,7 @@ public final class UserTypeFactory { } setIntAttribute(parser, "enabled", builder::setEnabled); + setIntAttribute(parser, "max-allowed", builder::setMaxAllowed); // Process child elements. final int depth = parser.getDepth(); diff --git a/services/core/java/com/android/server/pm/UserVisibilityMediator.java b/services/core/java/com/android/server/pm/UserVisibilityMediator.java index bd81062b0ff1..cbf7dfe77ca6 100644 --- a/services/core/java/com/android/server/pm/UserVisibilityMediator.java +++ b/services/core/java/com/android/server/pm/UserVisibilityMediator.java @@ -20,12 +20,15 @@ import static android.os.UserHandle.USER_NULL; import static android.os.UserHandle.USER_SYSTEM; import static android.view.Display.DEFAULT_DISPLAY; -import android.annotation.IntDef; +import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_FAILURE; +import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE; +import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE; +import static com.android.server.pm.UserManagerInternal.userAssignmentResultToString; + import android.annotation.Nullable; import android.annotation.UserIdInt; import android.os.UserHandle; import android.os.UserManager; -import android.util.DebugUtils; import android.util.Dumpable; import android.util.IndentingPrintWriter; import android.util.SparseIntArray; @@ -34,6 +37,7 @@ import android.view.Display; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; +import com.android.server.pm.UserManagerInternal.UserAssignmentResult; import com.android.server.utils.Slogf; import java.io.PrintWriter; @@ -52,23 +56,10 @@ public final class UserVisibilityMediator implements Dumpable { private static final String TAG = UserVisibilityMediator.class.getSimpleName(); - private static final String PREFIX_START_USER_RESULT = "START_USER_"; - // TODO(b/242195409): might need to change this if boot logic is refactored for HSUM devices @VisibleForTesting static final int INITIAL_CURRENT_USER_ID = USER_SYSTEM; - public static final int START_USER_RESULT_SUCCESS_VISIBLE = 1; - public static final int START_USER_RESULT_SUCCESS_INVISIBLE = 2; - public static final int START_USER_RESULT_FAILURE = -1; - - @IntDef(flag = false, prefix = {PREFIX_START_USER_RESULT}, value = { - START_USER_RESULT_SUCCESS_VISIBLE, - START_USER_RESULT_SUCCESS_INVISIBLE, - START_USER_RESULT_FAILURE - }) - public @interface StartUserResult {} - private final Object mLock = new Object(); private final boolean mUsersOnSecondaryDisplaysEnabled; @@ -97,10 +88,37 @@ public final class UserVisibilityMediator implements Dumpable { } /** - * TODO(b/244644281): merge with assignUserToDisplay() or add javadoc. + * See {@link UserManagerInternal#assignUserToDisplayOnStart(int, int, boolean, int)}. */ - public @StartUserResult int startUser(@UserIdInt int userId, @UserIdInt int profileGroupId, + public @UserAssignmentResult int startUser(@UserIdInt int userId, @UserIdInt int profileGroupId, boolean foreground, int displayId) { + // TODO(b/244644281): this method need to perform 4 actions: + // + // 1. Check if the user can be started given the provided arguments + // 2. If it can, decide whether it's visible or not (which is the return value) + // 3. Update the current user / profiles state + // 4. Update the users on secondary display state (if applicable) + // + // Ideally, they should be done "atomically" (i.e, only changing state while holding the + // mLock), but the initial implementation is just calling the existing methods, as the + // focus is to change the UserController startUser() workflow (so it relies on this class + // for the logic above). + // + // The next CL will refactor it (and the unit tests) to achieve that atomicity. + int result = startOnly(userId, profileGroupId, foreground, displayId); + if (result != USER_ASSIGNMENT_RESULT_FAILURE) { + assignUserToDisplay(userId, profileGroupId, displayId); + } + return result; + } + + /** + * @deprecated - see comment inside {@link #startUser(int, int, boolean, int)} + */ + @Deprecated + @VisibleForTesting + @UserAssignmentResult int startOnly(@UserIdInt int userId, + @UserIdInt int profileGroupId, boolean foreground, int displayId) { int actualProfileGroupId = profileGroupId == NO_PROFILE_GROUP_ID ? userId : profileGroupId; @@ -111,7 +129,7 @@ public final class UserVisibilityMediator implements Dumpable { if (foreground && displayId != DEFAULT_DISPLAY) { Slogf.w(TAG, "startUser(%d, %d, %b, %d) failed: cannot start foreground user on " + "secondary display", userId, actualProfileGroupId, foreground, displayId); - return START_USER_RESULT_FAILURE; + return USER_ASSIGNMENT_RESULT_FAILURE; } int visibility; @@ -121,13 +139,12 @@ public final class UserVisibilityMediator implements Dumpable { Slogf.w(TAG, "startUser(%d, %d, %b, %d) failed: cannot start profile user on " + "secondary display", userId, actualProfileGroupId, foreground, displayId); - return START_USER_RESULT_FAILURE; + return USER_ASSIGNMENT_RESULT_FAILURE; } if (foreground) { Slogf.w(TAG, "startUser(%d, %d, %b, %d) failed: cannot start profile user in " - + "foreground", userId, actualProfileGroupId, foreground, - displayId); - return START_USER_RESULT_FAILURE; + + "foreground", userId, actualProfileGroupId, foreground, displayId); + return USER_ASSIGNMENT_RESULT_FAILURE; } else { boolean isParentRunning = mStartedProfileGroupIds .get(actualProfileGroupId) == actualProfileGroupId; @@ -135,18 +152,18 @@ public final class UserVisibilityMediator implements Dumpable { Slogf.d(TAG, "profile parent running: %b", isParentRunning); } visibility = isParentRunning - ? START_USER_RESULT_SUCCESS_VISIBLE - : START_USER_RESULT_SUCCESS_INVISIBLE; + ? USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE + : USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE; } } else if (foreground) { mCurrentUserId = userId; - visibility = START_USER_RESULT_SUCCESS_VISIBLE; + visibility = USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE; } else { - visibility = START_USER_RESULT_SUCCESS_INVISIBLE; + visibility = USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE; } if (DBG) { Slogf.d(TAG, "adding user / profile mapping (%d -> %d) and returning %s", - userId, actualProfileGroupId, startUserResultToString(visibility)); + userId, actualProfileGroupId, userAssignmentResultToString(visibility)); } mStartedProfileGroupIds.put(userId, actualProfileGroupId); } @@ -154,21 +171,11 @@ public final class UserVisibilityMediator implements Dumpable { } /** - * TODO(b/244644281): merge with unassignUserFromDisplay() or add javadoc (and unit tests) - */ - public void stopUser(@UserIdInt int userId) { - if (DBG) { - Slogf.d(TAG, "stopUser(%d)", userId); - } - synchronized (mLock) { - mStartedProfileGroupIds.delete(userId); - } - } - - /** - * See {@link UserManagerInternal#assignUserToDisplay(int, int)}. + * @deprecated - see comment inside {@link #startUser(int, int, boolean, int)} */ - public void assignUserToDisplay(int userId, int profileGroupId, int displayId) { + @Deprecated + @VisibleForTesting + void assignUserToDisplay(int userId, int profileGroupId, int displayId) { if (DBG) { Slogf.d(TAG, "assignUserToDisplay(%d, %d): mUsersOnSecondaryDisplaysEnabled=%b", userId, displayId, mUsersOnSecondaryDisplaysEnabled); @@ -246,22 +253,25 @@ public final class UserVisibilityMediator implements Dumpable { } /** - * See {@link UserManagerInternal#unassignUserFromDisplay(int)}. + * See {@link UserManagerInternal#unassignUserFromDisplayOnStop(int)}. */ - public void unassignUserFromDisplay(int userId) { + public void stopUser(int userId) { if (DBG) { - Slogf.d(TAG, "unassignUserFromDisplay(%d)", userId); + Slogf.d(TAG, "stopUser(%d)", userId); } - if (!mUsersOnSecondaryDisplaysEnabled) { - // Don't need to do anything because methods (such as isUserVisible()) already know - // that the current user (and their profiles) is assigned to the default display. + synchronized (mLock) { if (DBG) { - Slogf.d(TAG, "ignoring when device doesn't support MUMD"); + Slogf.d(TAG, "Removing %d from mStartedProfileGroupIds (%s)", userId, + mStartedProfileGroupIds); } - return; - } + mStartedProfileGroupIds.delete(userId); - synchronized (mLock) { + if (!mUsersOnSecondaryDisplaysEnabled) { + // Don't need to do update mUsersOnSecondaryDisplays because methods (such as + // isUserVisible()) already know that the current user (and their profiles) is + // assigned to the default display. + return; + } if (DBG) { Slogf.d(TAG, "Removing %d from mUsersOnSecondaryDisplays (%s)", userId, mUsersOnSecondaryDisplays); @@ -395,33 +405,40 @@ public final class UserVisibilityMediator implements Dumpable { ipw.print("Current user id: "); ipw.println(mCurrentUserId); - ipw.print("Number of started user / profile group mappings: "); - ipw.println(mStartedProfileGroupIds.size()); - if (mStartedProfileGroupIds.size() > 0) { - ipw.increaseIndent(); - for (int i = 0; i < mStartedProfileGroupIds.size(); i++) { - ipw.print("User #"); - ipw.print(mStartedProfileGroupIds.keyAt(i)); - ipw.print(" -> profile #"); - ipw.println(mStartedProfileGroupIds.valueAt(i)); - } - ipw.decreaseIndent(); - } + dumpIntArray(ipw, mStartedProfileGroupIds, "started user / profile group", "u", "pg"); - ipw.print("Supports users on secondary displays: "); + ipw.print("Supports background users on secondary displays: "); ipw.println(mUsersOnSecondaryDisplaysEnabled); if (mUsersOnSecondaryDisplaysEnabled) { - ipw.print("Users on secondary displays: "); - synchronized (mLock) { - ipw.println(mUsersOnSecondaryDisplays); - } + dumpIntArray(ipw, mUsersOnSecondaryDisplays, "background user / secondary display", + "u", "d"); } } ipw.decreaseIndent(); } + private static void dumpIntArray(IndentingPrintWriter ipw, SparseIntArray array, + String arrayDescription, String keyName, String valueName) { + ipw.print("Number of "); + ipw.print(arrayDescription); + ipw.print(" mappings: "); + ipw.println(array.size()); + if (array.size() <= 0) { + return; + } + ipw.increaseIndent(); + for (int i = 0; i < array.size(); i++) { + ipw.print(keyName); ipw.print(':'); + ipw.print(array.keyAt(i)); + ipw.print(" -> "); + ipw.print(valueName); ipw.print(':'); + ipw.println(array.valueAt(i)); + } + ipw.decreaseIndent(); + } + @Override public void dump(PrintWriter pw, String[] args) { if (pw instanceof IndentingPrintWriter) { @@ -445,14 +462,6 @@ public final class UserVisibilityMediator implements Dumpable { return map; } - /** - * Gets the user-friendly representation of the {@code result}. - */ - public static String startUserResultToString(@StartUserResult int result) { - return DebugUtils.constantToString(UserVisibilityMediator.class, PREFIX_START_USER_RESULT, - result); - } - // TODO(b/244644281): methods below are needed because some APIs use the current users (full and // profiles) state to decide whether a user is visible or not. If we decide to always store that // info into intermediate maps, we should remove them. @@ -483,6 +492,14 @@ public final class UserVisibilityMediator implements Dumpable { } @VisibleForTesting + boolean isStartedUser(@UserIdInt int userId) { + synchronized (mLock) { + return mStartedProfileGroupIds.get(userId, + INITIAL_CURRENT_USER_ID) != INITIAL_CURRENT_USER_ID; + } + } + + @VisibleForTesting boolean isStartedProfile(@UserIdInt int userId) { int profileGroupId; synchronized (mLock) { 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 85882671a063..44081f7b2d2e 100644 --- a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java +++ b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java @@ -165,6 +165,11 @@ final class DefaultPermissionGrantPolicy { COARSE_BACKGROUND_LOCATION_PERMISSIONS.add(Manifest.permission.ACCESS_BACKGROUND_LOCATION); } + private static final Set<String> FINE_LOCATION_PERMISSIONS = new ArraySet<>(); + static { + FINE_LOCATION_PERMISSIONS.add(Manifest.permission.ACCESS_FINE_LOCATION); + } + private static final Set<String> ACTIVITY_RECOGNITION_PERMISSIONS = new ArraySet<>(); static { ACTIVITY_RECOGNITION_PERMISSIONS.add(Manifest.permission.ACTIVITY_RECOGNITION); @@ -616,6 +621,10 @@ final class DefaultPermissionGrantPolicy { grantPermissionsToSystemPackage(pm, getDefaultCaptivePortalLoginPackage(), userId, NOTIFICATION_PERMISSIONS); + // Dock Manager + grantPermissionsToSystemPackage(pm, getDefaultDockManagerPackage(), userId, + NOTIFICATION_PERMISSIONS); + // Camera grantPermissionsToSystemPackage(pm, getDefaultSystemHandlerActivityPackage(pm, MediaStore.ACTION_IMAGE_CAPTURE, userId), @@ -783,6 +792,8 @@ final class DefaultPermissionGrantPolicy { CONTACTS_PERMISSIONS, CALENDAR_PERMISSIONS, MICROPHONE_PERMISSIONS, PHONE_PERMISSIONS, SMS_PERMISSIONS, COARSE_BACKGROUND_LOCATION_PERMISSIONS, NEARBY_DEVICES_PERMISSIONS, NOTIFICATION_PERMISSIONS); + revokeRuntimePermissions(pm, voiceInteractPackageName, FINE_LOCATION_PERMISSIONS, + false, userId); } } @@ -933,6 +944,10 @@ final class DefaultPermissionGrantPolicy { 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/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java index 5f9ab959f80a..9ec63fcf7125 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java @@ -50,6 +50,7 @@ import android.content.pm.ParceledListSlice; import android.content.pm.PermissionGroupInfo; import android.content.pm.PermissionInfo; import android.content.pm.permission.SplitPermissionInfoParcelable; +import android.healthconnect.HealthConnectManager; import android.os.Binder; import android.os.IBinder; import android.os.Process; @@ -1100,7 +1101,7 @@ public class PermissionManagerService extends IPermissionManager.Stub { if (resolvedPackageName == null) { return; } - appOpsManager.finishOp(accessorSource.getToken(), op, + appOpsManager.finishOp(attributionSourceState.token, op, accessorSource.getUid(), resolvedPackageName, accessorSource.getAttributionTag()); } else { @@ -1109,8 +1110,9 @@ public class PermissionManagerService extends IPermissionManager.Stub { if (resolvedAttributionSource.getPackageName() == null) { return; } - appOpsManager.finishProxyOp(AppOpsManager.opToPublicName(op), - resolvedAttributionSource, skipCurrentFinish); + appOpsManager.finishProxyOp(attributionSourceState.token, + AppOpsManager.opToPublicName(op), resolvedAttributionSource, + skipCurrentFinish); } RegisteredAttribution registered = sRunningAttributionSources.remove(current.getToken()); @@ -1156,7 +1158,8 @@ public class PermissionManagerService extends IPermissionManager.Stub { if (permissionInfo == null) { try { permissionInfo = context.getPackageManager().getPermissionInfo(permission, 0); - if (PLATFORM_PACKAGE_NAME.equals(permissionInfo.packageName)) { + if (PLATFORM_PACKAGE_NAME.equals(permissionInfo.packageName) + || HealthConnectManager.isHealthPermission(context, permission)) { // Double addition due to concurrency is fine - the backing // store is concurrent. sPlatformPermissions.put(permission, permissionInfo); @@ -1225,10 +1228,11 @@ public class PermissionManagerService extends IPermissionManager.Stub { && next.getNext() == null); final boolean selfAccess = singleReceiverFromDatasource || next == null; - final int opMode = performOpTransaction(context, op, current, message, - forDataDelivery, /*startDataDelivery*/ false, skipCurrentChecks, - selfAccess, singleReceiverFromDatasource, AppOpsManager.OP_NONE, - AppOpsManager.ATTRIBUTION_FLAGS_NONE, AppOpsManager.ATTRIBUTION_FLAGS_NONE, + final int opMode = performOpTransaction(context, attributionSource.getToken(), op, + current, message, forDataDelivery, /*startDataDelivery*/ false, + skipCurrentChecks, selfAccess, singleReceiverFromDatasource, + AppOpsManager.OP_NONE, AppOpsManager.ATTRIBUTION_FLAGS_NONE, + AppOpsManager.ATTRIBUTION_FLAGS_NONE, AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE); switch (opMode) { @@ -1331,10 +1335,10 @@ public class PermissionManagerService extends IPermissionManager.Stub { attributionSource, next, fromDatasource, startDataDelivery, selfAccess, isLinkTrusted) : ATTRIBUTION_FLAGS_NONE; - final int opMode = performOpTransaction(context, op, current, message, - forDataDelivery, startDataDelivery, skipCurrentChecks, selfAccess, - singleReceiverFromDatasource, attributedOp, proxyAttributionFlags, - proxiedAttributionFlags, attributionChainId); + final int opMode = performOpTransaction(context, attributionSource.getToken(), op, + current, message, forDataDelivery, startDataDelivery, skipCurrentChecks, + selfAccess, singleReceiverFromDatasource, attributedOp, + proxyAttributionFlags, proxiedAttributionFlags, attributionChainId); switch (opMode) { case AppOpsManager.MODE_ERRORED: { @@ -1479,8 +1483,8 @@ public class PermissionManagerService extends IPermissionManager.Stub { attributionSource, next, /*fromDatasource*/ false, startDataDelivery, selfAccess, isLinkTrusted) : ATTRIBUTION_FLAGS_NONE; - final int opMode = performOpTransaction(context, op, current, message, - forDataDelivery, startDataDelivery, skipCurrentChecks, selfAccess, + final int opMode = performOpTransaction(context, current.getToken(), op, current, + message, forDataDelivery, startDataDelivery, skipCurrentChecks, selfAccess, /*fromDatasource*/ false, AppOpsManager.OP_NONE, proxyAttributionFlags, proxiedAttributionFlags, attributionChainId); @@ -1502,7 +1506,8 @@ public class PermissionManagerService extends IPermissionManager.Stub { } @SuppressWarnings("ConstantConditions") - private static int performOpTransaction(@NonNull Context context, int op, + private static int performOpTransaction(@NonNull Context context, + @NonNull IBinder chainStartToken, int op, @NonNull AttributionSource attributionSource, @Nullable String message, boolean forDataDelivery, boolean startDataDelivery, boolean skipProxyOperation, boolean selfAccess, boolean singleReceiverFromDatasource, int attributedOp, @@ -1564,7 +1569,7 @@ public class PermissionManagerService extends IPermissionManager.Stub { if (selfAccess) { try { startedOpResult = appOpsManager.startOpNoThrow( - resolvedAttributionSource.getToken(), startedOp, + chainStartToken, startedOp, resolvedAttributionSource.getUid(), resolvedAttributionSource.getPackageName(), /*startIfModeDefault*/ false, @@ -1575,14 +1580,14 @@ public class PermissionManagerService extends IPermissionManager.Stub { + " platform defined runtime permission " + AppOpsManager.opToPermission(op) + " while not having " + Manifest.permission.UPDATE_APP_OPS_STATS); - startedOpResult = appOpsManager.startProxyOpNoThrow(attributedOp, - attributionSource, message, skipProxyOperation, + startedOpResult = appOpsManager.startProxyOpNoThrow(chainStartToken, + attributedOp, attributionSource, message, skipProxyOperation, proxyAttributionFlags, proxiedAttributionFlags, attributionChainId); } } else { try { - startedOpResult = appOpsManager.startProxyOpNoThrow(startedOp, - resolvedAttributionSource, message, skipProxyOperation, + startedOpResult = appOpsManager.startProxyOpNoThrow(chainStartToken, + startedOp, resolvedAttributionSource, message, skipProxyOperation, proxyAttributionFlags, proxiedAttributionFlags, attributionChainId); } catch (SecurityException e) { //TODO 195339480: remove diff --git a/services/core/java/com/android/server/policy/AppOpsPolicy.java b/services/core/java/com/android/server/policy/AppOpsPolicy.java index a6d148c824c9..383249f41f39 100644 --- a/services/core/java/com/android/server/policy/AppOpsPolicy.java +++ b/services/core/java/com/android/server/policy/AppOpsPolicy.java @@ -45,13 +45,11 @@ import android.util.Log; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; -import com.android.internal.util.function.DecFunction; import com.android.internal.util.function.HeptFunction; import com.android.internal.util.function.HexFunction; import com.android.internal.util.function.QuadFunction; import com.android.internal.util.function.QuintConsumer; import com.android.internal.util.function.QuintFunction; -import com.android.internal.util.function.TriFunction; import com.android.internal.util.function.UndecFunction; import com.android.server.LocalServices; @@ -257,14 +255,14 @@ public final class AppOpsPolicy implements AppOpsManagerInternal.CheckOpsDelegat } @Override - public SyncNotedAppOp startProxyOperation(int code, + public SyncNotedAppOp startProxyOperation(@NonNull IBinder clientId, int code, @NonNull AttributionSource attributionSource, boolean startIfModeDefault, boolean shouldCollectAsyncNotedOp, String message, boolean shouldCollectMessage, boolean skipProxyOperation, @AttributionFlags int proxyAttributionFlags, @AttributionFlags int proxiedAttributionFlags, int attributionChainId, - @NonNull DecFunction<Integer, AttributionSource, Boolean, Boolean, String, Boolean, - Boolean, Integer, Integer, Integer, SyncNotedAppOp> superImpl) { - return superImpl.apply(resolveDatasourceOp(code, attributionSource.getUid(), + @NonNull UndecFunction<IBinder, Integer, AttributionSource, Boolean, Boolean, String, + Boolean, Boolean, Integer, Integer, Integer, SyncNotedAppOp> superImpl) { + return superImpl.apply(clientId, resolveDatasourceOp(code, attributionSource.getUid(), attributionSource.getPackageName(), attributionSource.getAttributionTag()), attributionSource, startIfModeDefault, shouldCollectAsyncNotedOp, message, shouldCollectMessage, skipProxyOperation, proxyAttributionFlags, @@ -280,10 +278,10 @@ public final class AppOpsPolicy implements AppOpsManagerInternal.CheckOpsDelegat } @Override - public void finishProxyOperation(int code, @NonNull AttributionSource attributionSource, - boolean skipProxyOperation, @NonNull TriFunction<Integer, AttributionSource, - Boolean, Void> superImpl) { - superImpl.apply(resolveDatasourceOp(code, attributionSource.getUid(), + public void finishProxyOperation(@NonNull IBinder clientId, int code, + @NonNull AttributionSource attributionSource, boolean skipProxyOperation, + @NonNull QuadFunction<IBinder, Integer, AttributionSource, Boolean, Void> superImpl) { + superImpl.apply(clientId, resolveDatasourceOp(code, attributionSource.getUid(), attributionSource.getPackageName(), attributionSource.getAttributionTag()), attributionSource, skipProxyOperation); } diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 98b5c1ba639b..3aa333aacdb6 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -3589,7 +3589,12 @@ public class PhoneWindowManager implements WindowManagerPolicy { @Override public void onKeyguardExitResult(boolean success) { if (success) { - startDockOrHome(displayId, true /*fromHomeKey*/, awakenFromDreams); + final long origId = Binder.clearCallingIdentity(); + try { + startDockOrHome(displayId, true /*fromHomeKey*/, awakenFromDreams); + } finally { + Binder.restoreCallingIdentity(origId); + } } } }); diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java index d8b1120c624d..cc84c85c7518 100644 --- a/services/core/java/com/android/server/power/PowerManagerService.java +++ b/services/core/java/com/android/server/power/PowerManagerService.java @@ -1053,7 +1053,7 @@ public final class PowerManagerService extends SystemService super(context); mContext = context; - mBinderService = new BinderService(); + mBinderService = new BinderService(mContext); mLocalService = new LocalService(); mNativeWrapper = injector.createNativeWrapper(); mSystemProperties = injector.createSystemPropertiesWrapper(); @@ -5485,12 +5485,17 @@ public final class PowerManagerService extends SystemService @VisibleForTesting final class BinderService extends IPowerManager.Stub { + private final PowerManagerShellCommand mShellCommand; + + BinderService(Context context) { + mShellCommand = new PowerManagerShellCommand(context, this); + } + @Override public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, String[] args, ShellCallback callback, ResultReceiver resultReceiver) { - (new PowerManagerShellCommand(this)).exec( - this, in, out, err, args, callback, resultReceiver); + mShellCommand.exec(this, in, out, err, args, callback, resultReceiver); } @Override // Binder call diff --git a/services/core/java/com/android/server/power/PowerManagerShellCommand.java b/services/core/java/com/android/server/power/PowerManagerShellCommand.java index a9b33ed58ef7..9439b762fde0 100644 --- a/services/core/java/com/android/server/power/PowerManagerShellCommand.java +++ b/services/core/java/com/android/server/power/PowerManagerShellCommand.java @@ -16,10 +16,15 @@ package com.android.server.power; +import android.content.Context; import android.content.Intent; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; import android.os.PowerManagerInternal; import android.os.RemoteException; import android.os.ShellCommand; +import android.util.SparseArray; +import android.view.Display; import java.io.PrintWriter; import java.util.List; @@ -27,9 +32,13 @@ import java.util.List; class PowerManagerShellCommand extends ShellCommand { private static final int LOW_POWER_MODE_ON = 1; - final PowerManagerService.BinderService mService; + private final Context mContext; + private final PowerManagerService.BinderService mService; - PowerManagerShellCommand(PowerManagerService.BinderService service) { + private SparseArray<WakeLock> mProxWakelocks = new SparseArray<>(); + + PowerManagerShellCommand(Context context, PowerManagerService.BinderService service) { + mContext = context; mService = service; } @@ -52,6 +61,8 @@ class PowerManagerShellCommand extends ShellCommand { return runSuppressAmbientDisplay(); case "list-ambient-display-suppression-tokens": return runListAmbientDisplaySuppressionTokens(); + case "set-prox": + return runSetProx(); default: return handleDefaultCommands(cmd); } @@ -117,6 +128,56 @@ class PowerManagerShellCommand extends ShellCommand { return 0; } + + /** TODO: Consider updating this code to support all wakelock types. */ + private int runSetProx() throws RemoteException { + PrintWriter pw = getOutPrintWriter(); + final boolean acquire; + switch (getNextArgRequired().toLowerCase()) { + case "list": + pw.println("Wakelocks:"); + pw.println(mProxWakelocks); + return 0; + case "acquire": + acquire = true; + break; + case "release": + acquire = false; + break; + default: + pw.println("Error: Allowed options are 'list' 'enable' and 'disable'."); + return -1; + } + + int displayId = Display.INVALID_DISPLAY; + String displayOption = getNextArg(); + if ("-d".equals(displayOption)) { + String idStr = getNextArg(); + displayId = Integer.parseInt(idStr); + if (displayId < 0) { + pw.println("Error: Specified displayId (" + idStr + ") must a non-negative int."); + return -1; + } + } + + int wakelockIndex = displayId + 1; // SparseArray doesn't support negative indexes + WakeLock wakelock = mProxWakelocks.get(wakelockIndex); + if (wakelock == null) { + PowerManager pm = mContext.getSystemService(PowerManager.class); + wakelock = pm.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, + "PowerManagerShellCommand[" + displayId + "]", displayId); + mProxWakelocks.put(wakelockIndex, wakelock); + } + + if (acquire) { + wakelock.acquire(); + } else { + wakelock.release(); + } + pw.println(wakelock); + return 0; + } + @Override public void onHelp() { final PrintWriter pw = getOutPrintWriter(); @@ -138,6 +199,11 @@ class PowerManagerShellCommand extends ShellCommand { pw.println(" ambient display"); pw.println(" list-ambient-display-suppression-tokens"); pw.println(" prints the tokens used to suppress ambient display"); + pw.println(" set-prox [list|acquire|release] (-d <display_id>)"); + pw.println(" Acquires the proximity sensor wakelock. Wakelock is associated with"); + pw.println(" a specific display if specified. 'list' lists wakelocks previously"); + pw.println(" created by set-prox including their held status."); + pw.println(); Intent.printIntentArgsHelp(pw , ""); } 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 dfa12814a138..0d13831abe52 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.Binder; 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 @@ public final class HintManagerService extends SystemService { 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 @@ public final class HintManagerService extends SystemService { 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 @@ public final class HintManagerService extends SystemService { } } + @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/servicewatcher/ServiceWatcherImpl.java b/services/core/java/com/android/server/servicewatcher/ServiceWatcherImpl.java index d9f504e5f270..ac970389b8b1 100644 --- a/services/core/java/com/android/server/servicewatcher/ServiceWatcherImpl.java +++ b/services/core/java/com/android/server/servicewatcher/ServiceWatcherImpl.java @@ -206,16 +206,21 @@ class ServiceWatcherImpl<TBoundServiceInfo extends BoundServiceInfo> implements Log.d(TAG, "[" + mTag + "] binding to " + mBoundServiceInfo); } + mRebinder = null; + Intent bindIntent = new Intent(mBoundServiceInfo.getAction()).setComponent( mBoundServiceInfo.getComponentName()); - if (!mContext.bindServiceAsUser(bindIntent, this, - BIND_AUTO_CREATE | BIND_NOT_FOREGROUND | BIND_NOT_VISIBLE, - mHandler, UserHandle.of(mBoundServiceInfo.getUserId()))) { - Log.e(TAG, "[" + mTag + "] unexpected bind failure - retrying later"); - mRebinder = this::bind; - mHandler.postDelayed(mRebinder, RETRY_DELAY_MS); - } else { - mRebinder = null; + try { + if (!mContext.bindServiceAsUser(bindIntent, this, + BIND_AUTO_CREATE | BIND_NOT_FOREGROUND | BIND_NOT_VISIBLE, + mHandler, UserHandle.of(mBoundServiceInfo.getUserId()))) { + Log.e(TAG, "[" + mTag + "] unexpected bind failure - retrying later"); + mRebinder = this::bind; + mHandler.postDelayed(mRebinder, RETRY_DELAY_MS); + } + } catch (SecurityException e) { + // if anything goes wrong it shouldn't crash the system server + Log.e(TAG, "[" + mTag + "] " + mBoundServiceInfo + " bind failed", e); } } diff --git a/services/core/java/com/android/server/timedetector/EnvironmentImpl.java b/services/core/java/com/android/server/timedetector/EnvironmentImpl.java index 8d106f7d7fa4..5801920864b4 100644 --- a/services/core/java/com/android/server/timedetector/EnvironmentImpl.java +++ b/services/core/java/com/android/server/timedetector/EnvironmentImpl.java @@ -128,4 +128,5 @@ final class EnvironmentImpl implements TimeDetectorStrategyImpl.Environment { @Override public void dumpDebugLog(@NonNull PrintWriter printWriter) { SystemClockTime.dump(printWriter); - }} + } +} diff --git a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java index 6984df9b0fc3..2c8fd967ae8a 100644 --- a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java +++ b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java @@ -2274,6 +2274,23 @@ public class TvInteractiveAppManagerService extends SystemService { } @Override + public void onRequestStopRecording(String recordingId) { + synchronized (mLock) { + if (DEBUG) { + Slogf.d(TAG, "onRequestStopRecording"); + } + if (mSessionState.mSession == null || mSessionState.mClient == null) { + return; + } + try { + mSessionState.mClient.onRequestStopRecording(recordingId, mSessionState.mSeq); + } catch (RemoteException e) { + Slogf.e(TAG, "error in onRequestStopRecording", e); + } + } + } + + @Override public void onRequestSigning(String id, String algorithm, String alias, byte[] data) { synchronized (mLock) { if (DEBUG) { diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java index 59f37c27f637..3b7bf4880faa 100644 --- a/services/core/java/com/android/server/wm/ActivityClientController.java +++ b/services/core/java/com/android/server/wm/ActivityClientController.java @@ -86,6 +86,7 @@ import android.window.TransitionInfo; import com.android.internal.app.AssistUtils; import com.android.internal.policy.IKeyguardDismissCallback; import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.util.FrameworkStatsLog; import com.android.server.LocalServices; import com.android.server.Watchdog; import com.android.server.pm.KnownPackages; @@ -454,6 +455,39 @@ class ActivityClientController extends IActivityClientController.Stub { finishTask == Activity.FINISH_TASK_WITH_ROOT_ACTIVITY; if (finishTask == Activity.FINISH_TASK_WITH_ACTIVITY || (finishWithRootActivity && r == rootR)) { + ActivityRecord topActivity = + r.getTask().getTopNonFinishingActivity(); + boolean passesAsmChecks = topActivity != null + && topActivity.getUid() == r.getUid(); + if (!passesAsmChecks) { + Slog.i(TAG, "Finishing task from background. r: " + r); + FrameworkStatsLog.write(FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED, + /* caller_uid */ + r.getUid(), + /* caller_activity_class_name */ + r.info.name, + /* target_task_top_activity_uid */ + topActivity == null ? -1 : topActivity.getUid(), + /* target_task_top_activity_class_name */ + topActivity == null ? null : topActivity.info.name, + /* target_task_is_different */ + false, + /* target_activity_uid */ + -1, + /* target_activity_class_name */ + null, + /* target_intent_action */ + null, + /* target_intent_flags */ + 0, + /* action */ + FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED__ACTION__FINISH_TASK, + /* version */ + 1, + /* multi_window */ + false + ); + } // If requested, remove the task that is associated to this activity only if it // was the root activity in the task. The result code and data is ignored // because we don't support returning them across task boundaries. Also, to @@ -1177,7 +1211,8 @@ class ActivityClientController extends IActivityClientController.Stub { synchronized (mGlobalLock) { final ActivityRecord r = ActivityRecord.isInRootTaskLocked(token); if (r != null) { - r.reportFullyDrawnLocked(restoredFromBundle); + mTaskSupervisor.getActivityMetricsLogger().notifyFullyDrawn(r, + restoredFromBundle); } } } finally { diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java index f0de1d35f790..e1ab291cc00a 100644 --- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java +++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java @@ -454,6 +454,7 @@ class ActivityMetricsLogger { final int windowsFullyDrawnDelayMs; final int activityRecordIdHashCode; final boolean relaunched; + final long timestampNs; private TransitionInfoSnapshot(TransitionInfo info) { this(info, info.mLastLaunchedActivity, INVALID_DELAY); @@ -483,6 +484,7 @@ class ActivityMetricsLogger { activityRecordIdHashCode = System.identityHashCode(launchedActivity); this.windowsFullyDrawnDelayMs = windowsFullyDrawnDelayMs; relaunched = info.mRelaunched; + timestampNs = info.mLaunchingState.mStartRealtimeNs; } @WaitResult.LaunchState int getLaunchState() { @@ -498,6 +500,10 @@ class ActivityMetricsLogger { } } + boolean isIntresetedToEventLog() { + return type == TYPE_TRANSITION_WARM_LAUNCH || type == TYPE_TRANSITION_COLD_LAUNCH; + } + PackageOptimizationInfo getPackageOptimizationInfo(ArtManagerInternal artManagerInternal) { return artManagerInternal == null || launchedActivityAppRecordRequiredAbi == null ? PackageOptimizationInfo.createWithNoInfo() @@ -1022,16 +1028,17 @@ class ActivityMetricsLogger { // This will avoid any races with other operations that modify the ActivityRecord. final TransitionInfoSnapshot infoSnapshot = new TransitionInfoSnapshot(info); if (info.isInterestingToLoggerAndObserver()) { - final long timestampNs = info.mLaunchingState.mStartRealtimeNs; final long uptimeNs = info.mLaunchingState.mStartUptimeNs; final int transitionDelay = info.mCurrentTransitionDelayMs; final int processState = info.mProcessState; final int processOomAdj = info.mProcessOomAdj; mLoggerHandler.post(() -> logAppTransition( - timestampNs, uptimeNs, transitionDelay, infoSnapshot, isHibernating, + uptimeNs, transitionDelay, infoSnapshot, isHibernating, processState, processOomAdj)); } - mLoggerHandler.post(() -> logAppDisplayed(infoSnapshot)); + if (infoSnapshot.isIntresetedToEventLog()) { + mLoggerHandler.post(() -> logAppDisplayed(infoSnapshot)); + } if (info.mPendingFullyDrawn != null) { info.mPendingFullyDrawn.run(); } @@ -1040,7 +1047,7 @@ class ActivityMetricsLogger { } // This gets called on another thread without holding the activity manager lock. - private void logAppTransition(long transitionStartTimeNs, long transitionDeviceUptimeNs, + private void logAppTransition(long transitionDeviceUptimeNs, int currentTransitionDelayMs, TransitionInfoSnapshot info, boolean isHibernating, int processState, int processOomAdj) { final LogMaker builder = new LogMaker(APP_TRANSITION); @@ -1108,7 +1115,7 @@ class ActivityMetricsLogger { isIncremental, isLoading, info.launchedActivityName.hashCode(), - TimeUnit.NANOSECONDS.toMillis(transitionStartTimeNs), + TimeUnit.NANOSECONDS.toMillis(info.timestampNs), processState, processOomAdj); @@ -1132,10 +1139,6 @@ class ActivityMetricsLogger { } private void logAppDisplayed(TransitionInfoSnapshot info) { - if (info.type != TYPE_TRANSITION_WARM_LAUNCH && info.type != TYPE_TRANSITION_COLD_LAUNCH) { - return; - } - EventLog.writeEvent(WM_ACTIVITY_LAUNCH_TIME, info.userId, info.activityRecordIdHashCode, info.launchedActivityShortComponentName, info.windowsDrawnDelayMs); @@ -1181,8 +1184,7 @@ class ActivityMetricsLogger { } /** @see android.app.Activity#reportFullyDrawn */ - TransitionInfoSnapshot logAppTransitionReportedDrawn(ActivityRecord r, - boolean restoredFromBundle) { + TransitionInfoSnapshot notifyFullyDrawn(ActivityRecord r, boolean restoredFromBundle) { final TransitionInfo info = mLastTransitionInfo.get(r); if (info == null) { return null; @@ -1191,7 +1193,7 @@ class ActivityMetricsLogger { // There are still undrawn activities, postpone reporting fully drawn until all of its // windows are drawn. So that is closer to an usable state. info.mPendingFullyDrawn = () -> { - logAppTransitionReportedDrawn(r, restoredFromBundle); + notifyFullyDrawn(r, restoredFromBundle); info.mPendingFullyDrawn = null; }; return null; @@ -1204,7 +1206,9 @@ class ActivityMetricsLogger { currentTimestampNs - info.mLaunchingState.mStartUptimeNs); final TransitionInfoSnapshot infoSnapshot = new TransitionInfoSnapshot(info, r, (int) startupTimeMs); - mLoggerHandler.post(() -> logAppFullyDrawn(infoSnapshot)); + if (infoSnapshot.isIntresetedToEventLog()) { + mLoggerHandler.post(() -> logAppFullyDrawn(infoSnapshot)); + } mLastTransitionInfo.remove(r); if (!info.isInterestingToLoggerAndObserver()) { @@ -1216,61 +1220,60 @@ class ActivityMetricsLogger { // fullfils (handling reportFullyDrawn() callbacks). Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityManager:ReportingFullyDrawn " + info.mLastLaunchedActivity.packageName); + mLoggerHandler.post(() -> logAppFullyDrawnMetrics(infoSnapshot, restoredFromBundle, + info.mProcessRunning)); + // Ends the trace started at the beginning of this function. This is located here to allow + // the trace slice to have a noticable duration. + Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); + // Notify reportFullyDrawn event. + launchObserverNotifyReportFullyDrawn(info, currentTimestampNs); + + return infoSnapshot; + } + + private void logAppFullyDrawnMetrics(TransitionInfoSnapshot info, boolean restoredFromBundle, + boolean processRunning) { final LogMaker builder = new LogMaker(APP_TRANSITION_REPORTED_DRAWN); - builder.setPackageName(r.packageName); - builder.addTaggedData(FIELD_CLASS_NAME, r.info.name); - builder.addTaggedData(APP_TRANSITION_REPORTED_DRAWN_MS, startupTimeMs); + builder.setPackageName(info.packageName); + builder.addTaggedData(FIELD_CLASS_NAME, info.launchedActivityName); + builder.addTaggedData(APP_TRANSITION_REPORTED_DRAWN_MS, + (long) info.windowsFullyDrawnDelayMs); builder.setType(restoredFromBundle ? TYPE_TRANSITION_REPORTED_DRAWN_WITH_BUNDLE : TYPE_TRANSITION_REPORTED_DRAWN_NO_BUNDLE); - builder.addTaggedData(APP_TRANSITION_PROCESS_RUNNING, - info.mProcessRunning ? 1 : 0); + builder.addTaggedData(APP_TRANSITION_PROCESS_RUNNING, processRunning ? 1 : 0); mMetricsLogger.write(builder); final PackageOptimizationInfo packageOptimizationInfo = - infoSnapshot.getPackageOptimizationInfo(getArtManagerInternal()); + info.getPackageOptimizationInfo(getArtManagerInternal()); // Incremental info boolean isIncremental = false, isLoading = false; - final String codePath = info.mLastLaunchedActivity.info.applicationInfo.getCodePath(); + final String codePath = info.applicationInfo.getCodePath(); if (codePath != null && IncrementalManager.isIncrementalPath(codePath)) { isIncremental = true; - isLoading = isIncrementalLoading(info.mLastLaunchedActivity.packageName, - info.mLastLaunchedActivity.mUserId); + isLoading = isIncrementalLoading(info.packageName, info.userId); } FrameworkStatsLog.write( FrameworkStatsLog.APP_START_FULLY_DRAWN, - info.mLastLaunchedActivity.info.applicationInfo.uid, - info.mLastLaunchedActivity.packageName, + info.applicationInfo.uid, + info.packageName, restoredFromBundle ? FrameworkStatsLog.APP_START_FULLY_DRAWN__TYPE__WITH_BUNDLE : FrameworkStatsLog.APP_START_FULLY_DRAWN__TYPE__WITHOUT_BUNDLE, - info.mLastLaunchedActivity.info.name, - info.mProcessRunning, - startupTimeMs, + info.launchedActivityName, + processRunning, + info.windowsFullyDrawnDelayMs, packageOptimizationInfo.getCompilationReason(), packageOptimizationInfo.getCompilationFilter(), - info.mSourceType, - info.mSourceEventDelayMs, + info.sourceType, + info.sourceEventDelayMs, isIncremental, isLoading, - info.mLastLaunchedActivity.info.name.hashCode(), - TimeUnit.NANOSECONDS.toMillis(info.mLaunchingState.mStartRealtimeNs)); - - // Ends the trace started at the beginning of this function. This is located here to allow - // the trace slice to have a noticable duration. - Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); - - // Notify reportFullyDrawn event. - launchObserverNotifyReportFullyDrawn(info, currentTimestampNs); - - return infoSnapshot; + info.launchedActivityName.hashCode(), + TimeUnit.NANOSECONDS.toMillis(info.timestampNs)); } private void logAppFullyDrawn(TransitionInfoSnapshot info) { - if (info.type != TYPE_TRANSITION_WARM_LAUNCH && info.type != TYPE_TRANSITION_COLD_LAUNCH) { - return; - } - StringBuilder sb = mStringBuilder; sb.setLength(0); sb.append("Fully drawn "); diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 0eee8a329a1e..8f8b57fb8cf5 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 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } prevDc.mClosingApps.remove(this); + prevDc.getDisplayPolicy().removeRelaunchingApp(this); if (prevDc.mFocusedApp == this) { prevDc.setFocusedApp(null); @@ -3130,8 +3131,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } // 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 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A void startRelaunching() { if (mPendingRelaunchCount == 0) { mRelaunchStartTime = SystemClock.elapsedRealtime(); + if (mVisibleRequested) { + mDisplayContent.getDisplayPolicy().addRelaunchingApp(this); + } } clearAllDrawn(); @@ -3972,7 +3976,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A 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 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return; } mPendingRelaunchCount = 0; + finishOrAbortReplacingWindow(); + } + + void finishOrAbortReplacingWindow() { mRelaunchStartTime = 0; + mDisplayContent.getDisplayPolicy().removeRelaunchingApp(this); } /** @@ -5102,6 +5111,9 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mTaskSupervisor.onProcessActivityStateChanged(app, false /* forceBatch */); } logAppCompatState(); + if (!visible) { + finishOrAbortReplacingWindow(); + } } /** @@ -6485,15 +6497,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } } - void reportFullyDrawnLocked(boolean restoredFromBundle) { - final TransitionInfoSnapshot info = mTaskSupervisor - .getActivityMetricsLogger().logAppTransitionReportedDrawn(this, restoredFromBundle); - if (info != null) { - mTaskSupervisor.reportActivityLaunched(false /* timeout */, this, - info.windowsFullyDrawnDelayMs, info.getLaunchState()); - } - } - void onFirstWindowDrawn(WindowState win) { firstWindowDrawn = true; // stop tracking @@ -6896,11 +6899,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A * {@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() { @@ -7901,8 +7901,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } @Override - float getSizeCompatScale() { - return hasSizeCompatBounds() ? mSizeCompatScale : super.getSizeCompatScale(); + float getCompatScale() { + return hasSizeCompatBounds() ? mSizeCompatScale : super.getCompatScale(); } @Override @@ -9204,10 +9204,10 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A + " preserveWindow=" + preserveWindow); if (andResume) { EventLogTags.writeWmRelaunchResumeActivity(mUserId, System.identityHashCode(this), - task.mTaskId, shortComponentName); + task.mTaskId, shortComponentName, Integer.toHexString(configChangeFlags)); } else { EventLogTags.writeWmRelaunchActivity(mUserId, System.identityHashCode(this), - task.mTaskId, shortComponentName); + task.mTaskId, shortComponentName, Integer.toHexString(configChangeFlags)); } startFreezingScreenLocked(0); diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index dc047aaab31c..a857d900d771 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -1663,7 +1663,8 @@ class ActivityStarter { } final Task startedTask = mStartActivity.getTask(); if (newTask) { - EventLogTags.writeWmCreateTask(mStartActivity.mUserId, startedTask.mTaskId); + EventLogTags.writeWmCreateTask(mStartActivity.mUserId, startedTask.mTaskId, + startedTask.getRootTaskId(), startedTask.getDisplayId()); } mStartActivity.logStartActivity(EventLogTags.WM_CREATE_ACTIVITY, startedTask); @@ -1856,6 +1857,11 @@ class ActivityStarter { + " from background: " + mSourceRecord + ". New task: " + newTask); boolean newOrEmptyTask = newTask || (targetTopActivity == null); + int action = newTask + ? FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED__ACTION__ACTIVITY_START_NEW_TASK + : (mSourceRecord.getTask().equals(targetTask) + ? FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED__ACTION__ACTIVITY_START_SAME_TASK + : FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED__ACTION__ACTIVITY_START_DIFFERENT_TASK); FrameworkStatsLog.write(FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED, /* caller_uid */ callerUid, @@ -1874,7 +1880,14 @@ class ActivityStarter { /* target_intent_action */ r.intent.getAction(), /* target_intent_flags */ - r.intent.getFlags() + r.intent.getFlags(), + /* action */ + action, + /* version */ + 1, + /* multi_window */ + targetTask != null && !targetTask.equals(mSourceRecord.getTask()) + && targetTask.isVisible() ); } } @@ -1976,12 +1989,6 @@ class ActivityStarter { ? targetTask.getTopNonFinishingActivity() : targetTaskTop; - // At this point we are certain we want the task moved to the front. If we need to dismiss - // any other always-on-top root tasks, now is the time to do it. - if (targetTaskTop.canTurnScreenOn() && mService.isDreaming()) { - targetTaskTop.mTaskSupervisor.wakeUp("recycleTask#turnScreenOnFlag"); - } - if (mMovedToFront) { // We moved the task to front, use starting window to hide initial drawn delay. targetTaskTop.showStartingWindow(true /* taskSwitch */); @@ -1993,6 +2000,12 @@ class ActivityStarter { // And for paranoia, make sure we have correctly resumed the top activity. resumeTargetRootTaskIfNeeded(); + // This is moving an existing task to front. But since dream activity has a higher z-order + // to cover normal activities, it needs the awakening event to be dismissed. + if (mService.isDreaming() && targetTaskTop.canTurnScreenOn()) { + targetTaskTop.mTaskSupervisor.wakeUp("recycleTask#turnScreenOnFlag"); + } + mLastStartActivityRecord = targetTaskTop; return mMovedToFront ? START_TASK_TO_FRONT : START_DELIVERED_TO_TOP; } diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index 3c457e1cc277..2f70edaf39cb 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_DEBUG; 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 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { // 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/AppTransitionController.java b/services/core/java/com/android/server/wm/AppTransitionController.java index 901153300169..bd22b32742a1 100644 --- a/services/core/java/com/android/server/wm/AppTransitionController.java +++ b/services/core/java/com/android/server/wm/AppTransitionController.java @@ -215,9 +215,20 @@ public class AppTransitionController { mWallpaperControllerLocked.adjustWallpaperWindowsForAppTransitionIfNeeded( mDisplayContent.mOpeningApps); + ArraySet<ActivityRecord> tmpOpenApps = mDisplayContent.mOpeningApps; + ArraySet<ActivityRecord> tmpCloseApps = mDisplayContent.mClosingApps; + if (mDisplayContent.mAtmService.mBackNavigationController.isWaitBackTransition()) { + tmpOpenApps = new ArraySet<>(mDisplayContent.mOpeningApps); + tmpCloseApps = new ArraySet<>(mDisplayContent.mClosingApps); + if (mDisplayContent.mAtmService.mBackNavigationController + .removeIfContainsBackAnimationTargets(tmpOpenApps, tmpCloseApps)) { + mDisplayContent.mAtmService.mBackNavigationController.clearBackAnimations(null); + } + } + @TransitionOldType final int transit = getTransitCompatType( - mDisplayContent.mAppTransition, mDisplayContent.mOpeningApps, - mDisplayContent.mClosingApps, mDisplayContent.mChangingContainers, + mDisplayContent.mAppTransition, tmpOpenApps, + tmpCloseApps, mDisplayContent.mChangingContainers, mWallpaperControllerLocked.getWallpaperTarget(), getOldWallpaper(), mDisplayContent.mSkipAppTransitionAnimation); mDisplayContent.mSkipAppTransitionAnimation = false; @@ -225,22 +236,21 @@ public class AppTransitionController { ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "handleAppTransitionReady: displayId=%d appTransition={%s}" + " openingApps=[%s] closingApps=[%s] transit=%s", - mDisplayContent.mDisplayId, appTransition.toString(), mDisplayContent.mOpeningApps, - mDisplayContent.mClosingApps, AppTransition.appTransitionOldToString(transit)); + mDisplayContent.mDisplayId, appTransition.toString(), tmpOpenApps, + tmpCloseApps, AppTransition.appTransitionOldToString(transit)); // Find the layout params of the top-most application window in the tokens, which is // what will control the animation theme. If all closing windows are obscured, then there is // no need to do an animation. This is the case, for example, when this transition is being // done behind a dream window. - final ArraySet<Integer> activityTypes = collectActivityTypes(mDisplayContent.mOpeningApps, - mDisplayContent.mClosingApps, mDisplayContent.mChangingContainers); + final ArraySet<Integer> activityTypes = collectActivityTypes(tmpOpenApps, + tmpCloseApps, mDisplayContent.mChangingContainers); final ActivityRecord animLpActivity = findAnimLayoutParamsToken(transit, activityTypes, - mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps, - mDisplayContent.mChangingContainers); + tmpOpenApps, tmpCloseApps, mDisplayContent.mChangingContainers); final ActivityRecord topOpeningApp = - getTopApp(mDisplayContent.mOpeningApps, false /* ignoreHidden */); + getTopApp(tmpOpenApps, false /* ignoreHidden */); final ActivityRecord topClosingApp = - getTopApp(mDisplayContent.mClosingApps, false /* ignoreHidden */); + getTopApp(tmpCloseApps, false /* ignoreHidden */); final ActivityRecord topChangingApp = getTopApp(mDisplayContent.mChangingContainers, false /* ignoreHidden */); final WindowManager.LayoutParams animLp = getAnimLp(animLpActivity); @@ -258,8 +268,7 @@ public class AppTransitionController { final int layoutRedo; mService.mSurfaceAnimationRunner.deferStartingAnimations(); try { - applyAnimations(mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps, transit, - animLp, voiceInteraction); + applyAnimations(tmpOpenApps, tmpCloseApps, transit, animLp, voiceInteraction); handleClosingApps(); handleOpeningApps(); handleChangingApps(transit); diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index 6ff2bb30cc0c..f5da4c80ba97 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -19,6 +19,9 @@ package com.android.server.wm; import static android.view.RemoteAnimationTarget.MODE_CLOSING; import static android.view.RemoteAnimationTarget.MODE_OPENING; import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE; +import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; +import static android.view.WindowManager.TRANSIT_CLOSE; +import static android.view.WindowManager.TRANSIT_TO_BACK; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_BACK_PREVIEW; @@ -32,6 +35,7 @@ import android.os.IBinder; import android.os.RemoteCallback; import android.os.RemoteException; import android.os.SystemProperties; +import android.util.ArraySet; import android.util.Slog; import android.view.IWindowFocusObserver; import android.view.RemoteAnimationTarget; @@ -41,7 +45,6 @@ import android.window.BackNavigationInfo; import android.window.IBackAnimationFinishedCallback; import android.window.OnBackInvokedCallbackInfo; import android.window.TaskSnapshot; -import android.window.WindowContainerToken; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; @@ -60,9 +63,9 @@ class BackNavigationController { private boolean mShowWallpaper; private Runnable mPendingAnimation; - // TODO (b/241808055) Find a appropriate time to remove during refactor - // Execute back animation with legacy transition system. Temporary flag for easier debugging. - static final boolean ENABLE_SHELL_TRANSITIONS = WindowManagerService.sEnableShellTransitions; + private final AnimationTargets mAnimationTargets = new AnimationTargets(); + private final ArrayList<WindowContainer> mTmpOpenApps = new ArrayList<>(); + private final ArrayList<WindowContainer> mTmpCloseApps = new ArrayList<>(); /** * true if the back predictability feature is enabled @@ -284,7 +287,6 @@ class BackNavigationController { } if (prepareAnimation) { - infoBuilder.setDepartingWCT(toWindowContainerToken(currentTask)); prepareAnimationIfNeeded(currentTask, prevTask, prevActivity, removedWindowContainer, backType, adapter); } @@ -303,11 +305,233 @@ class BackNavigationController { return infoBuilder.build(); } - private static WindowContainerToken toWindowContainerToken(WindowContainer<?> windowContainer) { - if (windowContainer == null || windowContainer.mRemoteToken == null) { - return null; + boolean isWaitBackTransition() { + return mAnimationTargets.mComposed && mAnimationTargets.mWaitTransition; + } + + // For legacy transition. + /** + * Once we find the transition targets match back animation targets, remove the target from + * list, so that transition won't count them in since the close animation was finished. + * + * @return {@code true} if the participants of this transition was animated by back gesture + * animations, and shouldn't join next transition. + */ + boolean removeIfContainsBackAnimationTargets(ArraySet<ActivityRecord> openApps, + ArraySet<ActivityRecord> closeApps) { + if (!isWaitBackTransition()) { + return false; + } + mTmpCloseApps.addAll(closeApps); + boolean result = false; + // Note: TmpOpenApps is empty. Unlike shell transition, the open apps will be removed from + // mOpeningApps if there is no visibility change. + if (mAnimationTargets.containsBackAnimationTargets(mTmpOpenApps, mTmpCloseApps)) { + // remove close target from close list, open target from open list; + // but the open target can be in close list. + for (int i = openApps.size() - 1; i >= 0; --i) { + final ActivityRecord ar = openApps.valueAt(i); + if (mAnimationTargets.isTarget(ar, true /* open */)) { + openApps.removeAt(i); + } + } + for (int i = closeApps.size() - 1; i >= 0; --i) { + final ActivityRecord ar = closeApps.valueAt(i); + if (mAnimationTargets.isTarget(ar, false /* open */)) { + closeApps.removeAt(i); + } + } + result = true; + } + mTmpCloseApps.clear(); + return result; + } + + // For shell transition + /** + * Check whether the transition targets was animated by back gesture animation. + * Because the opening target could request to do other stuff at onResume, so it could become + * close target for a transition. So the condition here is + * The closing target should only exist in close list, but the opening target can be either in + * open or close list. + * @return {@code true} if the participants of this transition was animated by back gesture + * animations, and shouldn't join next transition. + */ + boolean containsBackAnimationTargets(Transition transition) { + if (!mAnimationTargets.mComposed + || (transition.mType != TRANSIT_CLOSE && transition.mType != TRANSIT_TO_BACK)) { + return false; + } + final ArraySet<WindowContainer> targets = transition.mParticipants; + for (int i = targets.size() - 1; i >= 0; --i) { + final WindowContainer wc = targets.valueAt(i); + if (wc.asActivityRecord() == null && wc.asTask() == null) { + continue; + } + // WC can be visible due to setLaunchBehind + if (wc.isVisibleRequested()) { + mTmpOpenApps.add(wc); + } else { + mTmpCloseApps.add(wc); + } + } + final boolean result = mAnimationTargets.containsBackAnimationTargets( + mTmpOpenApps, mTmpCloseApps); + mTmpOpenApps.clear(); + mTmpCloseApps.clear(); + return result; + } + + boolean isMonitorTransitionTarget(WindowContainer wc) { + if (!mAnimationTargets.mComposed || !mAnimationTargets.mWaitTransition) { + return false; + } + return mAnimationTargets.isTarget(wc, wc.isVisibleRequested() /* open */); + } + + /** + * Cleanup animation, this can either happen when transition ready or finish. + * @param cleanupTransaction The transaction which the caller want to apply the internal + * cleanup together. + */ + void clearBackAnimations(SurfaceControl.Transaction cleanupTransaction) { + mAnimationTargets.clearBackAnimateTarget(cleanupTransaction); + } + + /** + * TODO: Animation composer + * prepareAnimationIfNeeded will become too complicated in order to support + * ActivityRecord/WindowState, using a factory class to create the RemoteAnimationTargets for + * different scenario. + */ + private static class AnimationTargets { + ActivityRecord mCloseTarget; // Must be activity + WindowContainer mOpenTarget; // Can be activity or task if activity was removed + private boolean mComposed; + private boolean mWaitTransition; + private int mSwitchType = UNKNOWN; + private SurfaceControl.Transaction mFinishedTransaction; + + private static final int UNKNOWN = 0; + private static final int TASK_SWITCH = 1; + private static final int ACTIVITY_SWITCH = 2; + + void reset(@NonNull WindowContainer close, @NonNull WindowContainer open) { + clearBackAnimateTarget(null); + if (close.asActivityRecord() != null && open.asActivityRecord() != null + && (close.asActivityRecord().getTask() == open.asActivityRecord().getTask())) { + mSwitchType = ACTIVITY_SWITCH; + mCloseTarget = close.asActivityRecord(); + } else if (close.asTask() != null && open.asTask() != null + && close.asTask() != open.asTask()) { + mSwitchType = TASK_SWITCH; + mCloseTarget = close.asTask().getTopNonFinishingActivity(); + } else { + mSwitchType = UNKNOWN; + return; + } + + mOpenTarget = open; + mComposed = false; + mWaitTransition = false; + } + + void composeNewAnimations(@NonNull WindowContainer close, @NonNull WindowContainer open) { + reset(close, open); + if (mSwitchType == UNKNOWN || mComposed || mCloseTarget == mOpenTarget + || mCloseTarget == null || mOpenTarget == null) { + return; + } + mComposed = true; + mWaitTransition = false; + } + + boolean containTarget(ArrayList<WindowContainer> wcs, boolean open) { + for (int i = wcs.size() - 1; i >= 0; --i) { + if (isTarget(wcs.get(i), open)) { + return true; + } + } + return wcs.isEmpty(); + } + + boolean isTarget(WindowContainer wc, boolean open) { + if (open) { + return wc == mOpenTarget || mOpenTarget.hasChild(wc); + } + if (mSwitchType == TASK_SWITCH) { + return wc == mCloseTarget + || (wc.asTask() != null && wc.hasChild(mCloseTarget)); + } else if (mSwitchType == ACTIVITY_SWITCH) { + return wc == mCloseTarget; + } + return false; + } + + boolean setFinishTransaction(SurfaceControl.Transaction finishTransaction) { + if (!mComposed) { + return false; + } + mFinishedTransaction = finishTransaction; + return true; + } + + void finishPresentAnimations(SurfaceControl.Transaction t) { + if (!mComposed) { + return; + } + final SurfaceControl.Transaction pt = t != null ? t + : mOpenTarget.getPendingTransaction(); + if (mFinishedTransaction != null) { + pt.merge(mFinishedTransaction); + mFinishedTransaction = null; + } + } + + void clearBackAnimateTarget(SurfaceControl.Transaction cleanupTransaction) { + finishPresentAnimations(cleanupTransaction); + mCloseTarget = null; + mOpenTarget = null; + mComposed = false; + mWaitTransition = false; + mSwitchType = UNKNOWN; + if (mFinishedTransaction != null) { + Slog.w(TAG, "Clear back animation, found un-processed finished transaction"); + if (cleanupTransaction != null) { + cleanupTransaction.merge(mFinishedTransaction); + } else { + mFinishedTransaction.apply(); + } + mFinishedTransaction = null; + } + } + + // The close target must in close list + // The open target can either in close or open list + boolean containsBackAnimationTargets(ArrayList<WindowContainer> openApps, + ArrayList<WindowContainer> closeApps) { + return containTarget(closeApps, false /* open */) + && (containTarget(openApps, true /* open */) + || containTarget(openApps, false /* open */)); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(128); + sb.append("AnimationTargets{"); + sb.append(" mOpenTarget= "); + sb.append(mOpenTarget); + sb.append(" mCloseTarget= "); + sb.append(mCloseTarget); + sb.append(" mSwitchType= "); + sb.append(mSwitchType); + sb.append(" mComposed= "); + sb.append(mComposed); + sb.append(" mWaitTransition= "); + sb.append(mWaitTransition); + sb.append('}'); + return sb.toString(); } - return windowContainer.mRemoteToken.toWindowContainerToken(); } private void prepareAnimationIfNeeded(Task currentTask, @@ -373,10 +597,6 @@ class BackNavigationController { leashes.add(screenshotSurface); } } else if (prevTask != null) { - if (!ENABLE_SHELL_TRANSITIONS) { - // Special handling for preventing next transition. - currentTask.mBackGestureStarted = true; - } prevActivity = prevTask.getTopNonFinishingActivity(); if (prevActivity != null) { // Make previous task show from behind by marking its top activity as visible @@ -417,35 +637,36 @@ class BackNavigationController { for (SurfaceControl sc: leashes) { finishedTransaction.remove(sc); } - synchronized (mWindowManagerService.mGlobalLock) { - if (ENABLE_SHELL_TRANSITIONS) { - if (!triggerBack) { - if (!needsScreenshot(backType)) { - restoreLaunchBehind(finalPrevActivity); - } + if (triggerBack) { + final SurfaceControl surfaceControl = + removedWindowContainer.getSurfaceControl(); + if (surfaceControl != null && surfaceControl.isValid()) { + // The animation is finish and start waiting for transition, + // hide the task surface before it re-parented to avoid flicker. + finishedTransaction.hide(surfaceControl); } + } else if (!needsScreenshot(backType)) { + restoreLaunchBehind(finalPrevActivity); + } + if (!mAnimationTargets.setFinishTransaction(finishedTransaction)) { + finishedTransaction.apply(); + } + if (!triggerBack) { + mAnimationTargets.clearBackAnimateTarget(null); } else { - if (triggerBack) { - final SurfaceControl surfaceControl = - removedWindowContainer.getSurfaceControl(); - if (surfaceControl != null && surfaceControl.isValid()) { - // When going back to home, hide the task surface before it - // re-parented to avoid flicker. - finishedTransaction.hide(surfaceControl); - } - } else { - currentTask.mBackGestureStarted = false; - if (!needsScreenshot(backType)) { - restoreLaunchBehind(finalPrevActivity); - } - } + mAnimationTargets.mWaitTransition = true; } } - finishedTransaction.apply(); + // TODO Add timeout monitor if transition didn't happen } }; - + if (backType == BackNavigationInfo.TYPE_CROSS_ACTIVITY) { + mAnimationTargets.composeNewAnimations(removedWindowContainer, prevActivity); + } else if (backType == BackNavigationInfo.TYPE_RETURN_TO_HOME + || backType == BackNavigationInfo.TYPE_CROSS_TASK) { + mAnimationTargets.composeNewAnimations(removedWindowContainer, prevTask); + } scheduleAnimationLocked(backType, targets, adapter, callback); } @@ -611,9 +832,8 @@ class BackNavigationController { } boolean isWallpaperVisible(WindowState w) { - if (mBackAnimationInProgress && w.isFocused()) { - return mShowWallpaper; - } - return false; + return mAnimationTargets.mComposed && mShowWallpaper + && w.mAttrs.type == TYPE_BASE_APPLICATION && w.mActivityRecord != null + && mAnimationTargets.isTarget(w.mActivityRecord, true /* open */); } } diff --git a/services/core/java/com/android/server/wm/DisplayArea.java b/services/core/java/com/android/server/wm/DisplayArea.java index b84b2d8805de..bedeabeb6141 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 @@ public class DisplayArea<T extends WindowContainer> extends WindowContainer<T> { } @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 8b3444318636..12efe0dc074f 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.WindowInsets.Type.InsetsType; 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 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp /** * Compat metrics computed based on {@link #mDisplayMetrics}. - * @see #updateDisplayAndOrientation(int, Configuration) + * @see #updateDisplayAndOrientation(Configuration) */ private final DisplayMetrics mCompatDisplayMetrics = new DisplayMetrics(); @@ -5031,7 +5032,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp * 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 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp 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 a1efd2d09184..1fef3c22a523 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 @@ public class DisplayPolicy { 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 @@ public class DisplayPolicy { mStatusBarBackgroundWindows.clear(); mStatusBarColorCheckedBounds.setEmpty(); mStatusBarBackgroundCheckedBounds.setEmpty(); + mSystemBarColorApps.clear(); mAllowLockscreenWhenOn = false; mShowingDream = false; @@ -1525,6 +1532,7 @@ public class DisplayPolicy { win.mAttrs.insetsFlags.appearance & APPEARANCE_LIGHT_STATUS_BARS, new Rect(win.getFrame()))); mStatusBarColorCheckedBounds.union(sTmpRect); + addSystemBarColorApp(win); } } @@ -1537,6 +1545,7 @@ public class DisplayPolicy { if (isOverlappingWithNavBar) { if (mNavBarColorWindowCandidate == null) { mNavBarColorWindowCandidate = win; + addSystemBarColorApp(win); } if (mNavBarBackgroundWindow == null) { mNavBarBackgroundWindow = win; @@ -1555,9 +1564,11 @@ public class DisplayPolicy { } } 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 @@ public class DisplayPolicy { } } - 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 @@ public class DisplayPolicy { // 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 @@ public class DisplayPolicy { // 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 @@ public class DisplayPolicy { // 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 @@ public class DisplayPolicy { 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 @@ public class DisplayPolicy { 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 @@ public class DisplayPolicy { 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 5d4904264056..6f821b55e54a 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 @@ class DisplayWindowPolicyControllerHelper { 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/EventLogTags.logtags b/services/core/java/com/android/server/wm/EventLogTags.logtags index 1e5a219e5e52..d94bf4bbbfa4 100644 --- a/services/core/java/com/android/server/wm/EventLogTags.logtags +++ b/services/core/java/com/android/server/wm/EventLogTags.logtags @@ -8,11 +8,11 @@ option java_package com.android.server.wm # An activity is being finished: 30001 wm_finish_activity (User|1|5),(Token|1|5),(Task ID|1|5),(Component Name|3),(Reason|3) # A task is being brought to the front of the screen: -30002 wm_task_to_front (User|1|5),(Task|1|5) +30002 wm_task_to_front (User|1|5),(Task|1|5),(Display Id|1|5) # An existing activity is being given a new intent: 30003 wm_new_intent (User|1|5),(Token|1|5),(Task ID|1|5),(Component Name|3),(Action|3),(MIME Type|3),(URI|3),(Flags|1|5) # A new task is being created: -30004 wm_create_task (User|1|5),(Task ID|1|5) +30004 wm_create_task (User|1|5),(Task ID|1|5),(Root Task ID|1|5),(Display Id|1|5) # A new activity is being created in an existing task: 30005 wm_create_activity (User|1|5),(Token|1|5),(Task ID|1|5),(Component Name|3),(Action|3),(MIME Type|3),(URI|3),(Flags|1|5) # An activity has been resumed into the foreground but was not already running: @@ -32,9 +32,9 @@ option java_package com.android.server.wm # An activity is being destroyed: 30018 wm_destroy_activity (User|1|5),(Token|1|5),(Task ID|1|5),(Component Name|3),(Reason|3) # An activity has been relaunched, resumed, and is now in the foreground: -30019 wm_relaunch_resume_activity (User|1|5),(Token|1|5),(Task ID|1|5),(Component Name|3) +30019 wm_relaunch_resume_activity (User|1|5),(Token|1|5),(Task ID|1|5),(Component Name|3),(config mask|3) # An activity has been relaunched: -30020 wm_relaunch_activity (User|1|5),(Token|1|5),(Task ID|1|5),(Component Name|3) +30020 wm_relaunch_activity (User|1|5),(Token|1|5),(Task ID|1|5),(Component Name|3),(config mask|3) # Activity set to resumed 30043 wm_set_resumed_activity (User|1|5),(Component Name|3),(Reason|3) @@ -45,9 +45,6 @@ option java_package com.android.server.wm # Attempting to stop an activity 30048 wm_stop_activity (User|1|5),(Token|1|5),(Component Name|3) -# The task is being removed from its parent task -30061 wm_remove_task (Task ID|1|5), (Root Task ID|1|5) - # An activity been add into stopping list 30066 wm_add_to_stopping (User|1|5),(Token|1|5),(Component Name|3),(Reason|3) @@ -57,11 +54,11 @@ option java_package com.android.server.wm # Out of memory for surfaces. 31000 wm_no_surface_memory (Window|3),(PID|1|5),(Operation|3) # Task created. -31001 wm_task_created (TaskId|1|5),(RootTaskId|1|5) +31001 wm_task_created (TaskId|1|5) # Task moved to top (1) or bottom (0). -31002 wm_task_moved (TaskId|1|5),(ToTop|1),(Index|1) +31002 wm_task_moved (TaskId|1|5),(Root Task ID|1|5),(Display Id|1|5),(ToTop|1),(Index|1) # Task removed with source explanation. -31003 wm_task_removed (TaskId|1|5),(Reason|3) +31003 wm_task_removed (TaskId|1|5),(Root Task ID|1|5),(Display Id|1|5),(Reason|3) # bootanim finished: 31007 wm_boot_animation_done (time|2|3) diff --git a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java index 554791a0da91..7fd093fffe95 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.graphics.Rect; 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 @@ import java.io.PrintWriter; */ 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 @@ final class ImeInsetsSourceProvider extends WindowContainerInsetsSourceProvider } /** - * 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 @@ final class ImeInsetsSourceProvider extends WindowContainerInsetsSourceProvider 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 @@ final class ImeInsetsSourceProvider extends WindowContainerInsetsSourceProvider (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 @@ final class ImeInsetsSourceProvider extends WindowContainerInsetsSourceProvider 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 d35b7c3d5fbe..8ecbc177896c 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 @@ interface InsetsControlTarget { * * @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 @@ interface InsetsControlTarget { * * @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 b9fa80cf2c0f..f66fa0f33b04 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 @@ class InsetsPolicy { 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 74a236bd862c..2dbccae6dde6 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 @@ final class LetterboxUiController { // Rounded corners should be displayed above the taskbar. bounds.bottom = Math.min(bounds.bottom, getTaskbarInsetsSource(mainWindow).getFrame().top); - if (mActivityRecord.inSizeCompatMode() - && mActivityRecord.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/Task.java b/services/core/java/com/android/server/wm/Task.java index d06f2716b84a..cdb332123fe2 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -67,7 +67,6 @@ import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.window.DisplayAreaOrganizer.FEATURE_UNDEFINED; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ADD_REMOVE; -import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_BACK_PREVIEW; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_LOCKTASK; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_RECENTS_ANIMATIONS; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STATES; @@ -607,12 +606,6 @@ class Task extends TaskFragment { boolean mLastSurfaceShowing = true; - /** - * Tracks if a back gesture is in progress. - * Skips any system transition animations if this is set to {@code true}. - */ - boolean mBackGestureStarted = false; - private Task(ActivityTaskManagerService atmService, int _taskId, Intent _intent, Intent _affinityIntent, String _affinity, String _rootAffinity, ComponentName _realActivity, ComponentName _origActivity, boolean _rootWasReset, @@ -680,7 +673,7 @@ class Task extends TaskFragment { mLaunchCookie = _launchCookie; mDeferTaskAppear = _deferTaskAppear; mRemoveWithTaskOrganizer = _removeWithTaskOrganizer; - EventLogTags.writeWmTaskCreated(mTaskId, isRootTask() ? INVALID_TASK_ID : getRootTaskId()); + EventLogTags.writeWmTaskCreated(mTaskId); } static Task fromWindowContainerToken(WindowContainerToken token) { @@ -1297,7 +1290,8 @@ class Task extends TaskFragment { } void updateTaskMovement(boolean toTop, int position) { - EventLogTags.writeWmTaskMoved(mTaskId, toTop ? 1 : 0, position); + EventLogTags.writeWmTaskMoved(mTaskId, getRootTaskId(), getDisplayId(), toTop ? 1 : 0, + position); final TaskDisplayArea taskDisplayArea = getDisplayArea(); if (taskDisplayArea != null && isLeafTask()) { taskDisplayArea.onLeafTaskMoved(this, toTop); @@ -2560,7 +2554,7 @@ class Task extends TaskFragment { } mRemoving = true; - EventLogTags.writeWmTaskRemoved(mTaskId, reason); + EventLogTags.writeWmTaskRemoved(mTaskId, getRootTaskId(), getDisplayId(), reason); clearPinnedTaskIfNeed(); // If applicable let the TaskOrganizer know the Task is vanishing. setTaskOrganizer(null); @@ -2573,7 +2567,8 @@ class Task extends TaskFragment { void reparent(Task rootTask, int position, boolean moveParents, String reason) { if (DEBUG_ROOT_TASK) Slog.i(TAG, "reParentTask: removing taskId=" + mTaskId + " from rootTask=" + getRootTask()); - EventLogTags.writeWmTaskRemoved(mTaskId, "reParentTask:" + reason); + EventLogTags.writeWmTaskRemoved(mTaskId, getRootTaskId(), getDisplayId(), + "reParentTask:" + reason); reparent(rootTask, position); @@ -3331,14 +3326,6 @@ class Task extends TaskFragment { } }); } - } else if (mBackGestureStarted) { - // Cancel playing transitions if a back navigation animation is in progress. - // This bit is set by {@link BackNavigationController} when a back gesture is started. - // It is used as a one-off transition overwrite that is cleared when the back gesture - // is committed and triggers a transition, or when the gesture is cancelled. - mBackGestureStarted = false; - mDisplayContent.mSkipAppTransitionAnimation = true; - ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "Skipping app transition animation. task=%s", this); } else { super.applyAnimationUnchecked(lp, enter, transit, isVoiceInteraction, sources); } diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java index b6c14bbfd8ed..6ff91af527ed 100644 --- a/services/core/java/com/android/server/wm/TaskDisplayArea.java +++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java @@ -455,7 +455,7 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> { } mLastLeafTaskToFrontId = t.mTaskId; - EventLogTags.writeWmTaskToFront(t.mUserId, t.mTaskId); + EventLogTags.writeWmTaskToFront(t.mUserId, t.mTaskId, getDisplayId()); // Notifying only when a leaf task moved to front. Or the listeners would be notified // couple times from the leaf task all the way up to the root task. mAtmService.getTaskChangeNotificationController().notifyTaskMovedToFront(t.getTaskInfo()); diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index 872542af0075..dfcad48046b0 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -291,6 +291,12 @@ class TaskFragment extends WindowContainer<WindowContainer> { private final IBinder mFragmentToken; /** + * Whether to delay the call to {@link #updateOrganizedTaskFragmentSurface()} when there is a + * configuration change. + */ + private boolean mDelayOrganizedTaskFragmentSurfaceUpdate; + + /** * Whether to delay the last activity of TaskFragment being immediately removed while finishing. * This should only be set on a embedded TaskFragment, where the organizer can have the * opportunity to perform animations and finishing the adjacent TaskFragment. @@ -2264,35 +2270,41 @@ class TaskFragment extends WindowContainer<WindowContainer> { @Override public void onConfigurationChanged(Configuration newParentConfig) { - // Task will animate differently. - if (mTaskFragmentOrganizer != null) { - mTmpPrevBounds.set(getBounds()); - } - super.onConfigurationChanged(newParentConfig); - final boolean shouldStartChangeTransition = shouldStartChangeTransition(mTmpPrevBounds); - if (shouldStartChangeTransition) { - initializeChangeTransition(mTmpPrevBounds); - } if (mTaskFragmentOrganizer != null) { - if (mTransitionController.isShellTransitionsEnabled() - && !mTransitionController.isCollecting(this)) { - // TaskFragmentOrganizer doesn't have access to the surface for security reasons, so - // update the surface here if it is not collected by Shell transition. - updateOrganizedTaskFragmentSurface(); - } else if (!mTransitionController.isShellTransitionsEnabled() - && !shouldStartChangeTransition) { - // Update the surface here instead of in the organizer so that we can make sure - // it can be synced with the surface freezer for legacy app transition. - updateOrganizedTaskFragmentSurface(); - } + updateOrganizedTaskFragmentSurface(); } sendTaskFragmentInfoChanged(); } + void deferOrganizedTaskFragmentSurfaceUpdate() { + mDelayOrganizedTaskFragmentSurfaceUpdate = true; + } + + void continueOrganizedTaskFragmentSurfaceUpdate() { + mDelayOrganizedTaskFragmentSurfaceUpdate = false; + updateOrganizedTaskFragmentSurface(); + } + private void updateOrganizedTaskFragmentSurface() { + if (mDelayOrganizedTaskFragmentSurfaceUpdate) { + return; + } + if (mTransitionController.isShellTransitionsEnabled() + && !mTransitionController.isCollecting(this)) { + // TaskFragmentOrganizer doesn't have access to the surface for security reasons, so + // update the surface here if it is not collected by Shell transition. + updateOrganizedTaskFragmentSurfaceUnchecked(); + } else if (!mTransitionController.isShellTransitionsEnabled() && !isAnimating()) { + // Update the surface here instead of in the organizer so that we can make sure + // it can be synced with the surface freezer for legacy app transition. + updateOrganizedTaskFragmentSurfaceUnchecked(); + } + } + + private void updateOrganizedTaskFragmentSurfaceUnchecked() { final SurfaceControl.Transaction t = getSyncTransaction(); updateSurfacePosition(t); updateOrganizedTaskFragmentSurfaceSize(t, false /* forceUpdate */); @@ -2346,7 +2358,7 @@ class TaskFragment extends WindowContainer<WindowContainer> { } /** Whether we should prepare a transition for this {@link TaskFragment} bounds change. */ - private boolean shouldStartChangeTransition(Rect startBounds) { + boolean shouldStartChangeTransition(Rect startBounds) { if (mTaskFragmentOrganizer == null || !canStartChangeTransition()) { return false; } @@ -2366,7 +2378,7 @@ class TaskFragment extends WindowContainer<WindowContainer> { void setSurfaceControl(SurfaceControl sc) { super.setSurfaceControl(sc); if (mTaskFragmentOrganizer != null) { - updateOrganizedTaskFragmentSurface(); + updateOrganizedTaskFragmentSurfaceUnchecked(); // If the TaskFragmentOrganizer was set before we created the SurfaceControl, we need to // emit the callbacks now. sendTaskFragmentAppeared(); diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index a64bd694605c..ef6859092689 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -928,10 +928,16 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe mFlags |= TRANSIT_FLAG_KEYGUARD_LOCKED; } + // Check whether the participants were animated from back navigation. + final boolean markBackAnimated = mController.mAtm.mBackNavigationController + .containsBackAnimationTargets(this); // Resolve the animating targets from the participants mTargets = calculateTargets(mParticipants, mChanges); final TransitionInfo info = calculateTransitionInfo(mType, mFlags, mTargets, mChanges, transaction); + if (markBackAnimated) { + mController.mAtm.mBackNavigationController.clearBackAnimations(mStartTransaction); + } if (mOverrideOptions != null) { info.setAnimationOptions(mOverrideOptions); if (mOverrideOptions.getType() == ANIM_OPEN_CROSS_PROFILE_APPS) { @@ -1935,9 +1941,20 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe final Task task = wc.asTask(); if (task != null) { final ActivityRecord topActivity = task.getTopNonFinishingActivity(); - if (topActivity != null && topActivity.mStartingData != null - && topActivity.mStartingData.hasImeSurface()) { - flags |= FLAG_WILL_IME_SHOWN; + if (topActivity != null) { + if (topActivity.mStartingData != null + && topActivity.mStartingData.hasImeSurface()) { + flags |= FLAG_WILL_IME_SHOWN; + } + if (topActivity.mAtmService.mBackNavigationController + .isMonitorTransitionTarget(topActivity)) { + flags |= TransitionInfo.FLAG_BACK_GESTURE_ANIMATED; + } + } else { + if (task.mAtmService.mBackNavigationController + .isMonitorTransitionTarget(task)) { + flags |= TransitionInfo.FLAG_BACK_GESTURE_ANIMATED; + } } if (task.voiceSession != null) { flags |= FLAG_IS_VOICE_INTERACTION; @@ -1951,6 +1968,10 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe flags |= FLAG_IS_VOICE_INTERACTION; } flags |= record.mTransitionChangeFlags; + if (record.mAtmService.mBackNavigationController + .isMonitorTransitionTarget(record)) { + flags |= TransitionInfo.FLAG_BACK_GESTURE_ANIMATED; + } } final TaskFragment taskFragment = wc.asTaskFragment(); if (taskFragment != null && task == null) { diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index 37bef3a833ee..25df5112e395 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -423,7 +423,7 @@ class TransitionController { Transition newTransition = null; if (isCollecting()) { if (displayChange != null) { - throw new IllegalArgumentException("Provided displayChange for a non-new request"); + Slog.e(TAG, "Provided displayChange for a non-new request", new Throwable()); } // Make the collecting transition wait until this request is ready. mCollectingTransition.setReady(readyGroupRef, false); diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java index c206a15503de..bab3a0553e63 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.SurfaceControl; 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 @@ public abstract class WindowManagerInternal { * 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 99a0e9032add..df343db8b3bf 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.WindowManagerGlobal; 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; @@ -1842,7 +1843,7 @@ public class WindowManagerService extends IWindowManager.Stub // Make this invalid which indicates a null attached frame. outAttachedFrame.set(0, 0, -1, -1); } - outSizeCompatScale[0] = win.getSizeCompatScale(); + outSizeCompatScale[0] = win.getCompatScaleForClient(); } Binder.restoreCallingIdentity(origId); @@ -8014,7 +8015,8 @@ public class WindowManagerService extends IWindowManager.Stub } @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 @@ public class WindowManagerService extends IWindowManager.Stub 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 @@ public class WindowManagerService extends IWindowManager.Stub 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 @@ public class WindowManagerService extends IWindowManager.Stub 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/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index 741537629dcb..de12a4ef7739 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -48,6 +48,7 @@ import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_ORGANI import static com.android.server.wm.ActivityTaskManagerService.LAYOUT_REASON_CONFIG_CHANGED; import static com.android.server.wm.ActivityTaskManagerService.enforceTaskPermission; import static com.android.server.wm.ActivityTaskSupervisor.PRESERVE_WINDOWS; +import static com.android.server.wm.DragResizeMode.DRAG_RESIZE_MODE_FREEFORM; import static com.android.server.wm.Task.FLAG_FORCE_HIDDEN_FOR_PINNED_TASK; import static com.android.server.wm.Task.FLAG_FORCE_HIDDEN_FOR_TASK_ORG; import static com.android.server.wm.TaskFragment.EMBEDDING_ALLOWED; @@ -144,6 +145,8 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub @VisibleForTesting final ArrayMap<IBinder, TaskFragment> mLaunchTaskFragments = new ArrayMap<>(); + private final Rect mTmpBounds = new Rect(); + WindowOrganizerController(ActivityTaskManagerService atm) { mService = atm; mGlobalLock = atm.mGlobalLock; @@ -702,7 +705,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } private int applyTaskChanges(Task tr, WindowContainerTransaction.Change c) { - int effects = 0; + int effects = applyChanges(tr, c, null /* errorCallbackToken */); final SurfaceControl.Transaction t = c.getBoundsChangeTransaction(); if ((c.getChangeMask() & WindowContainerTransaction.Change.CHANGE_HIDDEN) != 0) { @@ -717,6 +720,10 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub effects = TRANSACT_EFFECTS_LIFECYCLE; } + if ((c.getChangeMask() & WindowContainerTransaction.Change.CHANGE_DRAG_RESIZING) != 0) { + tr.setDragResizing(c.getDragResizing(), DRAG_RESIZE_MODE_FREEFORM); + } + final int childWindowingMode = c.getActivityWindowingMode(); if (childWindowingMode > -1) { tr.forAllActivities(a -> { a.setWindowingMode(childWindowingMode); }); @@ -759,6 +766,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub private int applyDisplayAreaChanges(DisplayArea displayArea, WindowContainerTransaction.Change c) { final int[] effects = new int[1]; + effects[0] = applyChanges(displayArea, c, null /* errorCallbackToken */); if ((c.getChangeMask() & WindowContainerTransaction.Change.CHANGE_IGNORE_ORIENTATION_REQUEST) != 0) { @@ -779,6 +787,27 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub return effects[0]; } + private int applyTaskFragmentChanges(@NonNull TaskFragment taskFragment, + @NonNull WindowContainerTransaction.Change c, @Nullable IBinder errorCallbackToken) { + if (taskFragment.isEmbeddedTaskFragmentInPip()) { + // No override from organizer for embedded TaskFragment in a PIP Task. + return 0; + } + + // When the TaskFragment is resized, we may want to create a change transition for it, for + // which we want to defer the surface update until we determine whether or not to start + // change transition. + mTmpBounds.set(taskFragment.getBounds()); + taskFragment.deferOrganizedTaskFragmentSurfaceUpdate(); + final int effects = applyChanges(taskFragment, c, errorCallbackToken); + if (taskFragment.shouldStartChangeTransition(mTmpBounds)) { + taskFragment.initializeChangeTransition(mTmpBounds); + } + taskFragment.continueOrganizedTaskFragmentSurfaceUpdate(); + mTmpBounds.set(0, 0, 0, 0); + return effects; + } + private int applyHierarchyOp(WindowContainerTransaction.HierarchyOp hop, int effects, int syncId, @Nullable Transition transition, boolean isInLockTaskMode, @NonNull CallerInfo caller, @Nullable IBinder errorCallbackToken, @@ -1444,20 +1473,15 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub private int applyWindowContainerChange(WindowContainer wc, WindowContainerTransaction.Change c, @Nullable IBinder errorCallbackToken) { sanitizeWindowContainer(wc); - if (wc.asTaskFragment() != null && wc.asTaskFragment().isEmbeddedTaskFragmentInPip()) { - // No override from organizer for embedded TaskFragment in a PIP Task. - return 0; - } - - int effects = applyChanges(wc, c, errorCallbackToken); - - if (wc instanceof DisplayArea) { - effects |= applyDisplayAreaChanges(wc.asDisplayArea(), c); - } else if (wc instanceof Task) { - effects |= applyTaskChanges(wc.asTask(), c); + if (wc.asDisplayArea() != null) { + return applyDisplayAreaChanges(wc.asDisplayArea(), c); + } else if (wc.asTask() != null) { + return applyTaskChanges(wc.asTask(), c); + } else if (wc.asTaskFragment() != null) { + return applyTaskFragmentChanges(wc.asTaskFragment(), c, errorCallbackToken); + } else { + return applyChanges(wc, c, errorCallbackToken); } - - return effects; } @Override diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index f06a8c9ffe82..86dd0b5452b5 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.WindowManager; 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 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP // 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,19 +1257,21 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP 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 getSizeCompatScale() { - return 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; } /** @@ -1537,10 +1540,11 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP mWmService.makeWindowFreezingScreenIfNeededLocked(this); // If the orientation is changing, or we're starting or ending a drag resizing action, - // then we need to hold off on unfreezing the display until this window has been - // redrawn; to do that, we need to go through the process of getting informed by the - // application when it has finished drawing. - if (getOrientationChanging() || dragResizingChanged) { + // or we're resizing an embedded Activity, then we need to hold off on unfreezing the + // display until this window has been redrawn; to do that, we need to go through the + // process of getting informed by the application when it has finished drawing. + if (getOrientationChanging() || dragResizingChanged + || isEmbeddedActivityResizeChanged()) { if (dragResizingChanged) { ProtoLog.v(WM_DEBUG_RESIZE, "Resize start waiting for draw, " @@ -3865,7 +3869,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP outFrames.attachedFrame.scale(mInvGlobalScale); } } - outFrames.sizeCompatScale = mSizeCompatScale; + + 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 @@ -4015,7 +4020,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP 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); } } @@ -4025,20 +4030,30 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP } @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); } } @@ -4146,6 +4161,20 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP return mActivityRecord == null || mActivityRecord.isFullyTransparentBarAllowed(frame); } + /** + * Whether this window belongs to a resizing embedded activity. + */ + private boolean isEmbeddedActivityResizeChanged() { + if (mActivityRecord == null || !isVisibleRequested()) { + // No need to update if the window is in the background. + return false; + } + + final TaskFragment embeddedTaskFragment = mActivityRecord.getOrganizedTaskFragment(); + return embeddedTaskFragment != null + && mDisplayContent.mChangingContainers.contains(embeddedTaskFragment); + } + boolean isDragResizeChanged() { return mDragResizing != computeDragResizing(); } @@ -6015,7 +6044,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP 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 7c481f51dfd0..f2527b622208 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 @@ class WindowToken extends WindowContainer<WindowState> { * @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 3c788197b191..57b977cc865a 100644 --- a/services/core/jni/Android.bp +++ b/services/core/jni/Android.bp @@ -170,7 +170,7 @@ cc_defaults { "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 000cb839002b..d975760cbfc2 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 @@ static void reportActualWorkDuration(int64_t session_ptr, 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 @@ static void nativeReportActualWorkDuration(JNIEnv* env, jclass /* clazz */, jlon 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 @@ static const JNINativeMethod sHintManagerServiceMethods[] = { {"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 0d872370dcdc..20303472e8db 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -260,10 +260,9 @@ static std::string getStringElementFromJavaArray(JNIEnv* env, jobjectArray array // --- NativeInputManager --- -class NativeInputManager : public virtual RefBase, - public virtual InputReaderPolicyInterface, - public virtual InputDispatcherPolicyInterface, - public virtual PointerControllerPolicyInterface { +class NativeInputManager : public virtual InputReaderPolicyInterface, + public virtual InputDispatcherPolicyInterface, + public virtual PointerControllerPolicyInterface { protected: virtual ~NativeInputManager(); @@ -450,6 +449,10 @@ void NativeInputManager::dump(std::string& dump) { 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"; @@ -1516,8 +1519,14 @@ static jlong nativeInit(JNIEnv* env, jclass /* clazz */, jobject serviceObj, return 0; } - NativeInputManager* im = new NativeInputManager(serviceObj, messageQueue->getLooper()); - im->incStrong(0); + static std::once_flag nativeInitialize; + NativeInputManager* im = nullptr; + std::call_once(nativeInitialize, [&]() { + // Create the NativeInputManager, which should not be destroyed or deallocated for the + // lifetime of the process. + im = new NativeInputManager(serviceObj, messageQueue->getLooper()); + }); + LOG_ALWAYS_FATAL_IF(im == nullptr, "NativeInputManager was already initialized."); return reinterpret_cast<jlong>(im); } diff --git a/services/core/jni/com_android_server_location_GnssLocationProvider.cpp b/services/core/jni/com_android_server_location_GnssLocationProvider.cpp index 9fa23c277d46..e1de05cf6c7c 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 @@ static void android_location_gnss_hal_GnssNative_agps_set_id(JNIEnv* env, jclass 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 @@ static const JNINativeMethod sLocationProviderMethods[] = { 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 34e4976dcca0..c7a1af77389d 100644 --- a/services/core/jni/gnss/AGnssRil.cpp +++ b/services/core/jni/gnss/AGnssRil.cpp @@ -84,8 +84,19 @@ jboolean AGnssRil::updateNetworkState(jboolean connected, jint type, jboolean ro 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 @@ jboolean AGnssRil_V1_0::updateNetworkState(jboolean connected, jint type, jboole 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 ce14a77d56c4..b7e02825252c 100644 --- a/services/core/jni/gnss/AGnssRil.h +++ b/services/core/jni/gnss/AGnssRil.h @@ -43,6 +43,8 @@ public: 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 @@ public: 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 @@ public: 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/credentials/java/com/android/server/credentials/CreateRequestSession.java b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java new file mode 100644 index 000000000000..e07bc7761156 --- /dev/null +++ b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.credentials; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.credentials.CreateCredentialRequest; +import android.credentials.CredentialManager; +import android.credentials.ICreateCredentialCallback; +import android.credentials.ui.ProviderData; +import android.credentials.ui.RequestInfo; +import android.service.credentials.CredentialProviderInfo; +import android.util.Log; + +import java.util.ArrayList; + +/** + * Central session for a single {@link CredentialManager#executeCreateCredential} request. + * This class listens to the responses from providers, and the UX app, and updates the + * provider(s) state maintained in {@link ProviderCreateSession}. + */ +public final class CreateRequestSession extends RequestSession<CreateCredentialRequest, + ICreateCredentialCallback> { + private static final String TAG = "CreateRequestSession"; + + CreateRequestSession(@NonNull Context context, int userId, + CreateCredentialRequest request, + ICreateCredentialCallback callback, + String callingPackage) { + super(context, userId, request, callback, RequestInfo.TYPE_CREATE, callingPackage); + } + + /** + * Creates a new provider session, and adds it to list of providers that are contributing to + * this request session. + * + * @return the provider session that was started + */ + @Override + @Nullable + public ProviderSession initiateProviderSession(CredentialProviderInfo providerInfo, + RemoteCredentialService remoteCredentialService) { + ProviderCreateSession providerCreateSession = ProviderCreateSession + .createNewSession(mContext, mUserId, providerInfo, + this, remoteCredentialService); + if (providerCreateSession != null) { + Log.i(TAG, "In startProviderSession - provider session created and being added"); + mProviders.put(providerCreateSession.getComponentName().flattenToString(), + providerCreateSession); + } + return providerCreateSession; + } + + @Override + protected void launchUiWithProviderData(ArrayList<ProviderData> providerDataList) { + mHandler.post(() -> mCredentialManagerUi.show(RequestInfo.newCreateRequestInfo( + mRequestId, mClientRequest, mIsFirstUiTurn, mClientCallingPackage), + providerDataList)); + } +} diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java index 321f022f526f..374da1c8e7e3 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java @@ -23,6 +23,7 @@ import android.annotation.UserIdInt; import android.content.Context; import android.content.pm.PackageManager; import android.credentials.CreateCredentialRequest; +import android.credentials.GetCredentialOption; import android.credentials.GetCredentialRequest; import android.credentials.IClearCredentialSessionCallback; import android.credentials.ICreateCredentialCallback; @@ -33,6 +34,7 @@ import android.os.CancellationSignal; import android.os.ICancellationSignal; import android.os.UserHandle; import android.provider.Settings; +import android.service.credentials.GetCredentialsRequest; import android.text.TextUtils; import android.util.Log; import android.util.Slog; @@ -43,6 +45,7 @@ import com.android.server.infra.SecureSettingsServiceNameResolver; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; +import java.util.stream.Collectors; /** * Entry point service for credential management. @@ -91,17 +94,15 @@ public final class CredentialManagerService extends return new ArrayList<>(); } List<CredentialManagerServiceImpl> serviceList = new ArrayList<>(serviceNames.length); - for (int i = 0; i < serviceNames.length; i++) { - Log.i(TAG, "in newServiceListLocked, service: " + serviceNames[i]); - if (TextUtils.isEmpty(serviceNames[i])) { + for (String serviceName : serviceNames) { + Log.i(TAG, "in newServiceListLocked, service: " + serviceName); + if (TextUtils.isEmpty(serviceName)) { continue; } try { serviceList.add(new CredentialManagerServiceImpl(this, mLock, resolvedUserId, - serviceNames[i])); - } catch (PackageManager.NameNotFoundException e) { - Log.i(TAG, "Unable to add serviceInfo : " + e.getMessage()); - } catch (SecurityException e) { + serviceName)); + } catch (PackageManager.NameNotFoundException | SecurityException e) { Log.i(TAG, "Unable to add serviceInfo : " + e.getMessage()); } } @@ -115,15 +116,31 @@ public final class CredentialManagerService extends synchronized (mLock) { final List<CredentialManagerServiceImpl> services = getServiceListForUserLocked(userId); - services.forEach(s -> { + for (CredentialManagerServiceImpl s : services) { c.accept(s); - }); + } } } finally { Binder.restoreCallingIdentity(origId); } } + private List<ProviderSession> initiateProviderSessions(RequestSession session, + List<String> requestOptions) { + List<ProviderSession> providerSessions = new ArrayList<>(); + // Invoke all services of a user to initiate a provider session + runForUser((service) -> { + if (service.isServiceCapable(requestOptions)) { + ProviderSession providerSession = service + .initiateProviderSessionForRequest(session); + if (providerSession != null) { + providerSessions.add(providerSession); + } + } + }); + return providerSessions; + } + final class CredentialManagerServiceStub extends ICredentialManager.Stub { @Override public ICancellationSignal executeGetCredential( @@ -137,11 +154,22 @@ public final class CredentialManagerService extends // New request session, scoped for this request only. final GetRequestSession session = new GetRequestSession(getContext(), UserHandle.getCallingUserId(), - callback); - - // Invoke all services of a user - runForUser((service) -> { - service.getCredential(request, session, callingPackage); + callback, + request, + callingPackage); + + // Initiate all provider sessions + List<ProviderSession> providerSessions = + initiateProviderSessions(session, request.getGetCredentialOptions() + .stream().map(GetCredentialOption::getType) + .collect(Collectors.toList())); + // TODO : Return error when no providers available + + // Iterate over all provider sessions and invoke the request + providerSessions.forEach(providerGetSession -> { + providerGetSession.getRemoteCredentialService().onGetCredentials( + (GetCredentialsRequest) providerGetSession.getProviderRequest(), + /*callback=*/providerGetSession); }); return cancelTransport; } @@ -151,9 +179,29 @@ public final class CredentialManagerService extends CreateCredentialRequest request, ICreateCredentialCallback callback, String callingPackage) { - // TODO: implement. - Log.i(TAG, "executeCreateCredential"); + Log.i(TAG, "starting executeCreateCredential with callingPackage: " + callingPackage); + // TODO : Implement cancellation ICancellationSignal cancelTransport = CancellationSignal.createTransport(); + + // New request session, scoped for this request only. + final CreateRequestSession session = new CreateRequestSession(getContext(), + UserHandle.getCallingUserId(), + request, + callback, + callingPackage); + + // Initiate all provider sessions + List<ProviderSession> providerSessions = + initiateProviderSessions(session, List.of(request.getType())); + // TODO : Return error when no providers available + + // Iterate over all provider sessions and invoke the request + providerSessions.forEach(providerCreateSession -> { + providerCreateSession.getRemoteCredentialService().onCreateCredential( + (android.service.credentials.CreateCredentialRequest) + providerCreateSession.getProviderRequest(), + /*callback=*/providerCreateSession); + }); return cancelTransport; } diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java b/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java index cc03f9b89119..0c323043a7a3 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java @@ -21,13 +21,13 @@ import android.annotation.Nullable; import android.content.ComponentName; import android.content.pm.PackageManager; import android.content.pm.ServiceInfo; -import android.credentials.GetCredentialRequest; import android.service.credentials.CredentialProviderInfo; -import android.service.credentials.GetCredentialsRequest; import android.util.Slog; import com.android.server.infra.AbstractPerUserSystemService; +import java.util.List; + /** * Per-user, per remote service implementation of {@link CredentialManagerService} @@ -61,50 +61,38 @@ public final class CredentialManagerServiceImpl extends return mInfo.getServiceInfo(); } - public void getCredential(GetCredentialRequest request, GetRequestSession requestSession, - String callingPackage) { - Slog.i(TAG, "in getCredential in CredManServiceImpl"); + /** + * Starts a provider session and associates it with the given request session. */ + @Nullable + public ProviderSession initiateProviderSessionForRequest( + RequestSession requestSession) { + Slog.i(TAG, "in initiateProviderSessionForRequest in CredManServiceImpl"); if (mInfo == null) { - Slog.i(TAG, "in getCredential in CredManServiceImpl, but mInfo is null"); - return; + Slog.i(TAG, "in initiateProviderSessionForRequest in CredManServiceImpl, " + + "but mInfo is null. This shouldn't happen"); + return null; } - - // TODO : Determine if remoteService instance can be reused across requests final RemoteCredentialService remoteService = new RemoteCredentialService( getContext(), mInfo.getServiceInfo().getComponentName(), mUserId); - ProviderGetSession providerSession = new ProviderGetSession(mInfo, - requestSession, mUserId, remoteService); - // Set the provider info to the session when the request is initiated. This happens here - // because there is one serviceImpl per remote provider, and so we can only retrieve - // the provider information in the scope of this instance, whereas the session is for the - // entire request. - requestSession.addProviderSession(providerSession); - GetCredentialsRequest filteredRequest = getRequestWithValidType(request, callingPackage); - if (filteredRequest != null) { - remoteService.onGetCredentials(getRequestWithValidType(request, callingPackage), - providerSession); - } + ProviderSession providerSession = + requestSession.initiateProviderSession(mInfo, remoteService); + return providerSession; } - @Nullable - private GetCredentialsRequest getRequestWithValidType(GetCredentialRequest request, - String callingPackage) { - GetCredentialsRequest.Builder builder = - new GetCredentialsRequest.Builder(callingPackage); - request.getGetCredentialOptions().forEach( option -> { - if (mInfo.hasCapability(option.getType())) { - Slog.i(TAG, "Provider can handle: " + option.getType()); - builder.addGetCredentialOption(option); + /** Return true if at least one capability found. */ + boolean isServiceCapable(List<String> requestedOptions) { + if (mInfo == null) { + Slog.i(TAG, "in isServiceCapable, mInfo is null"); + return false; + } + for (String capability : requestedOptions) { + if (mInfo.hasCapability(capability)) { + Slog.i(TAG, "Provider can handle: " + capability); + return true; } else { - Slog.i(TAG, "Skipping request as provider cannot handle it"); + Slog.i(TAG, "Provider cannot handle: " + capability); } - }); - - try { - return builder.build(); - } catch (IllegalArgumentException | NullPointerException e) { - Slog.i(TAG, "issue with request build: " + e.getMessage()); } - return null; + return false; } } diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java index dcf094f99aae..e889594ff857 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java @@ -37,6 +37,7 @@ public class CredentialManagerUi { @NonNull private final CredentialManagerUiCallback mCallbacks; @NonNull private final Context mContext; + // TODO : Use for starting the activity for this user private final int mUserId; @NonNull private final ResultReceiver mResultReceiver = new ResultReceiver( new Handler(Looper.getMainLooper())) { @@ -56,7 +57,7 @@ public class CredentialManagerUi { Slog.i(TAG, "No selection found in UI result"); } } else if (resultCode == UserSelectionDialogResult.RESULT_CODE_DIALOG_CANCELED) { - mCallbacks.onUiCancelation(); + mCallbacks.onUiCancellation(); } } @@ -67,7 +68,7 @@ public class CredentialManagerUi { /** Called when the user makes a selection. */ void onUiSelection(UserSelectionDialogResult selection); /** Called when the user cancels the UI. */ - void onUiCancelation(); + void onUiCancellation(); } public CredentialManagerUi(Context context, int userId, CredentialManagerUiCallback callbacks) { @@ -83,9 +84,8 @@ public class CredentialManagerUi { */ public void show(RequestInfo requestInfo, ArrayList<ProviderData> providerDataList) { Log.i(TAG, "In show"); - Intent intent = IntentFactory.newIntent( - requestInfo, providerDataList, - new ArrayList<>(), mResultReceiver); + Intent intent = IntentFactory.newIntent(requestInfo, providerDataList, new ArrayList<>(), + mResultReceiver); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); mContext.startActivity(intent); } diff --git a/services/credentials/java/com/android/server/credentials/GetRequestSession.java b/services/credentials/java/com/android/server/credentials/GetRequestSession.java index 80f0fec06825..8238632fc6c2 100644 --- a/services/credentials/java/com/android/server/credentials/GetRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/GetRequestSession.java @@ -16,9 +16,10 @@ package com.android.server.credentials; -import android.content.ComponentName; +import android.annotation.Nullable; import android.content.Context; import android.credentials.Credential; +import android.credentials.GetCredentialRequest; import android.credentials.GetCredentialResponse; import android.credentials.IGetCredentialCallback; import android.credentials.ui.ProviderData; @@ -26,62 +27,52 @@ import android.credentials.ui.RequestInfo; import android.credentials.ui.UserSelectionDialogResult; import android.os.RemoteException; import android.service.credentials.CredentialEntry; +import android.service.credentials.CredentialProviderInfo; import android.util.Log; -import android.util.Slog; import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; /** * Central session for a single getCredentials request. This class listens to the * responses from providers, and the UX app, and updates the provider(S) state. */ -public final class GetRequestSession extends RequestSession { +public final class GetRequestSession extends RequestSession<GetCredentialRequest, + IGetCredentialCallback> { private static final String TAG = "GetRequestSession"; - private final IGetCredentialCallback mClientCallback; - private final Map<String, ProviderGetSession> mProviders; - public GetRequestSession(Context context, int userId, - IGetCredentialCallback callback) { - super(context, userId, RequestInfo.TYPE_GET); - mClientCallback = callback; - mProviders = new HashMap<>(); + IGetCredentialCallback callback, GetCredentialRequest request, + String callingPackage) { + super(context, userId, request, callback, RequestInfo.TYPE_GET, callingPackage); } /** - * Adds a new provider to the list of providers that are contributing to this session. + * Creates a new provider session, and adds it list of providers that are contributing to + * this session. + * @return the provider session created within this request session, for the given provider + * info. */ - public void addProviderSession(ProviderGetSession providerSession) { - mProviders.put(providerSession.getComponentName().flattenToString(), - providerSession); - } - @Override - public void onProviderStatusChanged(ProviderSession.Status status, - ComponentName componentName) { - Log.i(TAG, "in onStatusChanged"); - if (ProviderSession.isTerminatingStatus(status)) { - Log.i(TAG, "in onStatusChanged terminating status"); - - ProviderGetSession session = mProviders.remove(componentName.flattenToString()); - if (session != null) { - Slog.i(TAG, "Provider session removed."); - } else { - Slog.i(TAG, "Provider session null, did not exist."); - } - } else if (ProviderSession.isCompletionStatus(status)) { - Log.i(TAG, "in onStatusChanged isCompletionStatus status"); - onProviderResponseComplete(); + @Nullable + public ProviderSession initiateProviderSession(CredentialProviderInfo providerInfo, + RemoteCredentialService remoteCredentialService) { + ProviderGetSession providerGetSession = ProviderGetSession + .createNewSession(mContext, mUserId, providerInfo, + this, remoteCredentialService); + if (providerGetSession != null) { + Log.i(TAG, "In startProviderSession - provider session created and being added"); + mProviders.put(providerGetSession.getComponentName().flattenToString(), + providerGetSession); } + return providerGetSession; } + // TODO: Override for this method not needed once get selection logic is + // moved to ProviderGetSession @Override public void onUiSelection(UserSelectionDialogResult selection) { String providerId = selection.getProviderId(); - ProviderGetSession providerSession = mProviders.get(providerId); + ProviderGetSession providerSession = (ProviderGetSession) mProviders.get(providerId); if (providerSession != null) { CredentialEntry credentialEntry = providerSession.getCredentialEntry( selection.getEntrySubkey()); @@ -89,57 +80,17 @@ public final class GetRequestSession extends RequestSession { respondToClientAndFinish(credentialEntry.getCredential()); } // TODO : Handle action chips and authentication selection - return; } // TODO : finish session and respond to client if provider not found } @Override - public void onUiCancelation() { - // User canceled the activity - // TODO : Send error code to client - finishSession(); - } - - private void onProviderResponseComplete() { - Log.i(TAG, "in onProviderResponseComplete"); - if (isResponseCompleteAcrossProviders()) { - Log.i(TAG, "in onProviderResponseComplete - isResponseCompleteAcrossProviders"); - getProviderDataAndInitiateUi(); - } - } - - private void getProviderDataAndInitiateUi() { - ArrayList<ProviderData> providerDataList = new ArrayList<>(); - for (ProviderGetSession session : mProviders.values()) { - Log.i(TAG, "preparing data for : " + session.getComponentName()); - providerDataList.add(session.prepareUiData()); - } - if (!providerDataList.isEmpty()) { - Log.i(TAG, "provider list not empty about to initiate ui"); - initiateUi(providerDataList); - } - } - - private void initiateUi(ArrayList<ProviderData> providerDataList) { + protected void launchUiWithProviderData(ArrayList<ProviderData> providerDataList) { mHandler.post(() -> mCredentialManagerUi.show(RequestInfo.newGetRequestInfo( mRequestId, null, mIsFirstUiTurn, ""), providerDataList)); } - /** - * Iterates over all provider sessions and returns true if all have responded. - */ - private boolean isResponseCompleteAcrossProviders() { - AtomicBoolean isRequestComplete = new AtomicBoolean(true); - mProviders.forEach( (packageName, session) -> { - if (session.getStatus() != ProviderSession.Status.COMPLETE) { - isRequestComplete.set(false); - } - }); - return isRequestComplete.get(); - } - private void respondToClientAndFinish(Credential credential) { try { mClientCallback.onResponse(new GetCredentialResponse(credential)); @@ -148,17 +99,4 @@ public final class GetRequestSession extends RequestSession { } finishSession(); } - - private void finishSession() { - clearProviderSessions(); - } - - private void clearProviderSessions() { - for (ProviderGetSession session : mProviders.values()) { - // TODO : Evaluate if we should unbind remote services here or wait for them - // to automatically unbind when idle. Re-binding frequently also has a cost. - //session.destroy(); - } - mProviders.clear(); - } } diff --git a/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java new file mode 100644 index 000000000000..49c416f943c1 --- /dev/null +++ b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java @@ -0,0 +1,231 @@ +/* + * 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.credentials; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.content.Context; +import android.credentials.Credential; +import android.credentials.ui.CreateCredentialProviderData; +import android.credentials.ui.Entry; +import android.os.Bundle; +import android.service.credentials.CreateCredentialRequest; +import android.service.credentials.CreateCredentialResponse; +import android.service.credentials.CredentialProviderInfo; +import android.service.credentials.CredentialProviderService; +import android.service.credentials.SaveEntry; +import android.util.Log; +import android.util.Slog; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Central provider session that listens for provider callbacks, and maintains provider state. + * Will likely split this into remote response state and UI state. + */ +public final class ProviderCreateSession extends ProviderSession< + CreateCredentialRequest, CreateCredentialResponse> { + private static final String TAG = "ProviderCreateSession"; + + // Key to be used as an entry key for a save entry + private static final String SAVE_ENTRY_KEY = "save_entry_key"; + + @NonNull + private final Map<String, SaveEntry> mUiSaveEntries = new HashMap<>(); + /** The complete request to be used in the second round. */ + private final CreateCredentialRequest mCompleteRequest; + + /** Creates a new provider session to be used by the request session. */ + @Nullable public static ProviderCreateSession createNewSession( + Context context, + @UserIdInt int userId, + CredentialProviderInfo providerInfo, + CreateRequestSession createRequestSession, + RemoteCredentialService remoteCredentialService) { + CreateCredentialRequest providerRequest = + createProviderRequest(providerInfo.getCapabilities(), + createRequestSession.mClientRequest, + createRequestSession.mClientCallingPackage); + if (providerRequest != null) { + return new ProviderCreateSession(context, providerInfo, createRequestSession, userId, + remoteCredentialService, providerRequest); + } + Log.i(TAG, "Unable to create provider session"); + return null; + } + + @Nullable + private static CreateCredentialRequest createProviderRequest(List<String> providerCapabilities, + android.credentials.CreateCredentialRequest clientRequest, + String clientCallingPackage) { + String capability = clientRequest.getType(); + if (providerCapabilities.contains(capability)) { + return new CreateCredentialRequest(clientCallingPackage, capability, + clientRequest.getData()); + } + Log.i(TAG, "Unable to create provider request - capabilities do not match"); + return null; + } + + private static CreateCredentialRequest getFirstRoundRequest(CreateCredentialRequest request) { + // TODO: Replace with first round bundle from request when ready + return new CreateCredentialRequest( + request.getCallingPackage(), + request.getType(), + new Bundle()); + } + + private ProviderCreateSession( + @NonNull Context context, + @NonNull CredentialProviderInfo info, + @NonNull ProviderInternalCallback callbacks, + @UserIdInt int userId, + @NonNull RemoteCredentialService remoteCredentialService, + @NonNull CreateCredentialRequest request) { + super(context, info, getFirstRoundRequest(request), callbacks, userId, + remoteCredentialService); + // TODO : Replace with proper splitting of request + mCompleteRequest = request; + setStatus(Status.PENDING); + } + + /** Returns the save entry maintained in state by this provider session. */ + public SaveEntry getUiSaveEntry(String entryId) { + return mUiSaveEntries.get(entryId); + } + + @Override + public void onProviderResponseSuccess( + @Nullable CreateCredentialResponse response) { + Log.i(TAG, "in onProviderResponseSuccess"); + onUpdateResponse(response); + } + + /** Called when the provider response resulted in a failure. */ + @Override + public void onProviderResponseFailure(int errorCode, @Nullable CharSequence message) { + updateStatusAndInvokeCallback(toStatus(errorCode)); + } + + /** Called when provider service dies. */ + @Override + public void onProviderServiceDied(RemoteCredentialService service) { + if (service.getComponentName().equals(mProviderInfo.getServiceInfo().getComponentName())) { + updateStatusAndInvokeCallback(Status.SERVICE_DEAD); + } else { + Slog.i(TAG, "Component names different in onProviderServiceDied - " + + "this should not happen"); + } + } + + private void onUpdateResponse(CreateCredentialResponse response) { + Log.i(TAG, "updateResponse with save entries"); + mProviderResponse = response; + updateStatusAndInvokeCallback(Status.SAVE_ENTRIES_RECEIVED); + } + + @Override + @Nullable protected CreateCredentialProviderData prepareUiData() + throws IllegalArgumentException { + Log.i(TAG, "In prepareUiData"); + if (!ProviderSession.isUiInvokingStatus(getStatus())) { + Log.i(TAG, "In prepareUiData not in uiInvokingStatus"); + return null; + } + final CreateCredentialResponse response = getProviderResponse(); + if (response == null) { + Log.i(TAG, "In prepareUiData response null"); + throw new IllegalStateException("Response must be in completion mode"); + } + if (response.getSaveEntries() != null) { + Log.i(TAG, "In prepareUiData save entries not null"); + return prepareUiProviderData( + prepareUiSaveEntries(response.getSaveEntries()), + null, + /*isDefaultProvider=*/false); + } + return null; + } + + @Override + public void onProviderIntentResult(Bundle resultData) { + Credential credential = resultData.getParcelable( + CredentialProviderService.EXTRA_SAVE_CREDENTIAL, + Credential.class); + if (credential == null) { + Log.i(TAG, "Credential returned from intent is null"); + return; + } + updateFinalCredentialResponse(credential); + } + + @Override + public void onUiEntrySelected(String entryType, String entryKey) { + if (entryType.equals(SAVE_ENTRY_KEY)) { + SaveEntry saveEntry = mUiSaveEntries.get(entryKey); + if (saveEntry == null) { + Log.i(TAG, "Save entry not found"); + return; + } + // TODO: Uncomment when pending intent works + // onSaveEntrySelected(saveEntry); + } + } + + @Override + public void onProviderIntentCancelled() { + //TODO (Implement) + } + + private List<Entry> prepareUiSaveEntries(@NonNull List<SaveEntry> saveEntries) { + Log.i(TAG, "in populateUiSaveEntries"); + List<Entry> uiSaveEntries = new ArrayList<>(); + + // Populate the save entries + for (SaveEntry saveEntry : saveEntries) { + String entryId = generateEntryId(); + mUiSaveEntries.put(entryId, saveEntry); + Log.i(TAG, "in prepareUiProviderData creating ui entry with id " + entryId); + uiSaveEntries.add(new Entry(SAVE_ENTRY_KEY, entryId, saveEntry.getSlice())); + } + return uiSaveEntries; + } + + private void updateFinalCredentialResponse(@NonNull Credential credential) { + mFinalCredentialResponse = credential; + updateStatusAndInvokeCallback(Status.CREDENTIAL_RECEIVED_FROM_INTENT); + } + + private CreateCredentialProviderData prepareUiProviderData(List<Entry> saveEntries, + Entry remoteEntry, boolean isDefaultProvider) { + return new CreateCredentialProviderData.Builder( + mComponentName.flattenToString()) + .setSaveEntries(saveEntries) + .setIsDefaultProvider(isDefaultProvider) + .build(); + } + + private void onSaveEntrySelected(SaveEntry saveEntry) { + mProviderIntentController.setupAndInvokePendingIntent(saveEntry.getPendingIntent(), + mProviderRequest); + setStatus(Status.PENDING_INTENT_INVOKED); + } +} diff --git a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java index ff2107a95d25..362d98167462 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java @@ -18,13 +18,16 @@ package com.android.server.credentials; import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.slice.Slice; +import android.annotation.UserIdInt; +import android.content.Context; +import android.credentials.GetCredentialOption; import android.credentials.ui.Entry; import android.credentials.ui.GetCredentialProviderData; +import android.os.Bundle; import android.service.credentials.Action; import android.service.credentials.CredentialEntry; import android.service.credentials.CredentialProviderInfo; -import android.service.credentials.CredentialsDisplayContent; +import android.service.credentials.GetCredentialsRequest; import android.service.credentials.GetCredentialsResponse; import android.util.Log; import android.util.Slog; @@ -38,75 +41,95 @@ import java.util.UUID; /** * Central provider session that listens for provider callbacks, and maintains provider state. * Will likely split this into remote response state and UI state. + * + * @hide */ -public final class ProviderGetSession extends ProviderSession<GetCredentialsResponse> - implements RemoteCredentialService.ProviderCallbacks<GetCredentialsResponse> { +public final class ProviderGetSession extends ProviderSession<GetCredentialsRequest, + GetCredentialsResponse> + implements + RemoteCredentialService.ProviderCallbacks<GetCredentialsResponse> { private static final String TAG = "ProviderGetSession"; // Key to be used as an entry key for a credential entry private static final String CREDENTIAL_ENTRY_KEY = "credential_key"; - private GetCredentialsResponse mResponse; - @NonNull - private final Map<String, CredentialEntry> mUiCredentials = new HashMap<>(); - + private final Map<String, CredentialEntry> mUiCredentialEntries = new HashMap<>(); @NonNull - private final Map<String, Action> mUiActions = new HashMap<>(); - - public ProviderGetSession(CredentialProviderInfo info, - ProviderInternalCallback callbacks, - int userId, RemoteCredentialService remoteCredentialService) { - super(info, callbacks, userId, remoteCredentialService); - setStatus(Status.PENDING); + private final Map<String, Action> mUiActionsEntries = new HashMap<>(); + private Action mAuthenticationAction = null; + + /** Creates a new provider session to be used by the request session. */ + @Nullable public static ProviderGetSession createNewSession( + Context context, + @UserIdInt int userId, + CredentialProviderInfo providerInfo, + GetRequestSession getRequestSession, + RemoteCredentialService remoteCredentialService) { + GetCredentialsRequest providerRequest = + createProviderRequest(providerInfo.getCapabilities(), + getRequestSession.mClientRequest, + getRequestSession.mClientCallingPackage); + if (providerRequest != null) { + return new ProviderGetSession(context, providerInfo, getRequestSession, userId, + remoteCredentialService, providerRequest); + } + Log.i(TAG, "Unable to create provider session"); + return null; } - /** Updates the response being maintained in state by this provider session. */ - @Override - public void updateResponse(GetCredentialsResponse response) { - if (response.getAuthenticationAction() != null) { - // TODO : Implement authentication logic - } else if (response.getCredentialsDisplayContent() != null) { - Log.i(TAG , "updateResponse with credentialEntries"); - mResponse = response; - updateStatusAndInvokeCallback(Status.COMPLETE); + @Nullable + private static GetCredentialsRequest createProviderRequest(List<String> providerCapabilities, + android.credentials.GetCredentialRequest clientRequest, + String clientCallingPackage) { + List<GetCredentialOption> filteredOptions = new ArrayList<>(); + for (GetCredentialOption option : clientRequest.getGetCredentialOptions()) { + if (providerCapabilities.contains(option.getType())) { + Log.i(TAG, "In createProviderRequest - capability found : " + option.getType()); + filteredOptions.add(option); + } else { + Log.i(TAG, "In createProviderRequest - capability not " + + "found : " + option.getType()); + } + } + if (!filteredOptions.isEmpty()) { + return new GetCredentialsRequest.Builder(clientCallingPackage).setGetCredentialOptions( + filteredOptions).build(); } + Log.i(TAG, "In createProviderRequest - returning null"); + return null; } - /** Returns the response being maintained in this provider session. */ - @Override - @Nullable - public GetCredentialsResponse getResponse() { - return mResponse; + public ProviderGetSession(Context context, + CredentialProviderInfo info, + ProviderInternalCallback callbacks, + int userId, RemoteCredentialService remoteCredentialService, + GetCredentialsRequest request) { + super(context, info, request, callbacks, userId, remoteCredentialService); + setStatus(Status.PENDING); } /** Returns the credential entry maintained in state by this provider session. */ @Nullable public CredentialEntry getCredentialEntry(@NonNull String entryId) { - return mUiCredentials.get(entryId); - } - - /** Returns the action entry maintained in state by this provider session. */ - @Nullable - public Action getAction(@NonNull String entryId) { - return mUiActions.get(entryId); + return mUiCredentialEntries.get(entryId); } /** Called when the provider response has been updated by an external source. */ - @Override + @Override // Callback from the remote provider public void onProviderResponseSuccess(@Nullable GetCredentialsResponse response) { Log.i(TAG, "in onProviderResponseSuccess"); - updateResponse(response); + onUpdateResponse(response); } /** Called when the provider response resulted in a failure. */ - @Override + @Override // Callback from the remote provider public void onProviderResponseFailure(int errorCode, @Nullable CharSequence message) { updateStatusAndInvokeCallback(toStatus(errorCode)); } /** Called when provider service dies. */ - @Override + @Override // Callback from the remote provider public void onProviderServiceDied(RemoteCredentialService service) { if (service.getComponentName().equals(mProviderInfo.getServiceInfo().getComponentName())) { updateStatusAndInvokeCallback(Status.SERVICE_DEAD); @@ -116,77 +139,106 @@ public final class ProviderGetSession extends ProviderSession<GetCredentialsResp } } + @Override // Callback from the provider intent controller class + public void onProviderIntentResult(Bundle resultData) { + // TODO : Implement + } + @Override - protected GetCredentialProviderData prepareUiData() throws IllegalArgumentException { - Log.i(TAG, "In prepareUiData"); - if (!ProviderSession.isCompletionStatus(getStatus())) { - Log.i(TAG, "In prepareUiData not complete"); + public void onProviderIntentCancelled() { + // TODO : Implement + } - throw new IllegalStateException("Status must be in completion mode"); + @Override // Selection call from the request provider + protected void onUiEntrySelected(String entryType, String entryId) { + // TODO: Implement + } + + @Override // Call from request session to data to be shown on the UI + @Nullable protected GetCredentialProviderData prepareUiData() throws IllegalArgumentException { + Log.i(TAG, "In prepareUiData"); + if (!ProviderSession.isUiInvokingStatus(getStatus())) { + Log.i(TAG, "In prepareUiData - provider does not want to show UI: " + + mComponentName.flattenToString()); + return null; } - GetCredentialsResponse response = getResponse(); + GetCredentialsResponse response = getProviderResponse(); if (response == null) { Log.i(TAG, "In prepareUiData response null"); - throw new IllegalStateException("Response must be in completion mode"); } if (response.getAuthenticationAction() != null) { - Log.i(TAG, "In prepareUiData auth not null"); - - return prepareUiProviderDataWithAuthentication(response.getAuthenticationAction()); + Log.i(TAG, "In prepareUiData - top level authentication mode"); + return prepareUiProviderData(null, null, + prepareUiAuthenticationActionEntry(response.getAuthenticationAction()), + /*remoteEntry=*/null); } if (response.getCredentialsDisplayContent() != null){ - Log.i(TAG, "In prepareUiData credentials not null"); - - return prepareUiProviderDataWithCredentials(response.getCredentialsDisplayContent()); + Log.i(TAG, "In prepareUiData displayContent not null"); + return prepareUiProviderData(populateUiActionEntries( + response.getCredentialsDisplayContent().getActions()), + prepareUiCredentialEntries(response.getCredentialsDisplayContent() + .getCredentialEntries()), + /*authenticationActionEntry=*/null, /*remoteEntry=*/null); } return null; } - /** - * To be called by {@link ProviderGetSession} when the UI is to be invoked. - */ - @Nullable - private GetCredentialProviderData prepareUiProviderDataWithCredentials(@NonNull - CredentialsDisplayContent content) { - Log.i(TAG, "in prepareUiProviderData"); - List<Entry> credentialEntries = new ArrayList<>(); - List<Entry> actionChips = new ArrayList<>(); - Entry authenticationEntry = null; + private Entry prepareUiAuthenticationActionEntry(@NonNull Action authenticationAction) { + String entryId = generateEntryId(); + mUiActionsEntries.put(entryId, authenticationAction); + return new Entry(ACTION_ENTRY_KEY, entryId, authenticationAction.getSlice()); + } + + private List<Entry> prepareUiCredentialEntries(@NonNull + List<CredentialEntry> credentialEntries) { + Log.i(TAG, "in prepareUiProviderDataWithCredentials"); + List<Entry> credentialUiEntries = new ArrayList<>(); // Populate the credential entries - for (CredentialEntry credentialEntry : content.getCredentialEntries()) { - String entryId = UUID.randomUUID().toString(); - mUiCredentials.put(entryId, credentialEntry); + for (CredentialEntry credentialEntry : credentialEntries) { + String entryId = generateEntryId(); + mUiCredentialEntries.put(entryId, credentialEntry); Log.i(TAG, "in prepareUiProviderData creating ui entry with id " + entryId); - Slice slice = credentialEntry.getSlice(); - // TODO : Remove conversion of string to int after change in Entry class - credentialEntries.add(new Entry(CREDENTIAL_ENTRY_KEY, entryId, + credentialUiEntries.add(new Entry(CREDENTIAL_ENTRY_KEY, entryId, credentialEntry.getSlice())); } - // populate the action chip - for (Action action : content.getActions()) { + return credentialUiEntries; + } + + private List<Entry> populateUiActionEntries(@Nullable List<Action> actions) { + List<Entry> actionEntries = new ArrayList<>(); + for (Action action : actions) { String entryId = UUID.randomUUID().toString(); - mUiActions.put(entryId, action); + mUiActionsEntries.put(entryId, action); // TODO : Remove conversion of string to int after change in Entry class - actionChips.add(new Entry(ACTION_ENTRY_KEY, entryId, - action.getSlice())); + actionEntries.add(new Entry(ACTION_ENTRY_KEY, entryId, action.getSlice())); } + return actionEntries; + } - return new GetCredentialProviderData.Builder(mComponentName.flattenToString()) + private GetCredentialProviderData prepareUiProviderData(List<Entry> actionEntries, + List<Entry> credentialEntries, Entry authenticationActionEntry, + Entry remoteEntry) { + return new GetCredentialProviderData.Builder( + mComponentName.flattenToString()).setActionChips(actionEntries) .setCredentialEntries(credentialEntries) - .setActionChips(actionChips) - .setAuthenticationEntry(authenticationEntry) + .setAuthenticationEntry(authenticationActionEntry) .build(); } - /** - * To be called by {@link ProviderGetSession} when the UI is to be invoked. - */ - @Nullable - private GetCredentialProviderData prepareUiProviderDataWithAuthentication(@NonNull - Action authenticationEntry) { - // TODO : Implement authentication flow - return null; + /** Updates the response being maintained in state by this provider session. */ + private void onUpdateResponse(GetCredentialsResponse response) { + mProviderResponse = response; + if (response.getAuthenticationAction() != null) { + Log.i(TAG , "updateResponse with authentication entry"); + // TODO validate authentication action + mAuthenticationAction = response.getAuthenticationAction(); + updateStatusAndInvokeCallback(Status.REQUIRES_AUTHENTICATION); + } else if (response.getCredentialsDisplayContent() != null) { + Log.i(TAG , "updateResponse with credentialEntries"); + // TODO validate response + updateStatusAndInvokeCallback(Status.CREDENTIALS_RECEIVED); + } } } diff --git a/services/credentials/java/com/android/server/credentials/ProviderIntentController.java b/services/credentials/java/com/android/server/credentials/ProviderIntentController.java new file mode 100644 index 000000000000..0f2e8ecdbbc6 --- /dev/null +++ b/services/credentials/java/com/android/server/credentials/ProviderIntentController.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.credentials; + +import android.annotation.NonNull; +import android.annotation.SuppressLint; +import android.annotation.UserIdInt; +import android.app.Activity; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.IntentSender; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Parcel; +import android.os.ResultReceiver; +import android.service.credentials.CreateCredentialRequest; +import android.service.credentials.CredentialProviderService; +import android.util.Log; + +/** + * Class that invokes providers' pending intents and listens to the responses. + */ +@SuppressLint("LongLogTag") +public class ProviderIntentController { + private static final String TAG = "ProviderIntentController"; + /** + * Interface to be implemented by any class that wishes to get callbacks from the UI. + */ + public interface ProviderIntentControllerCallback { + /** Called when the user makes a selection. */ + void onProviderIntentResult(Bundle resultData); + /** Called when the user cancels the UI. */ + void onProviderIntentCancelled(); + } + + private final int mUserId; + private final Context mContext; + private final ProviderIntentControllerCallback mCallback; + private final ResultReceiver mResultReceiver = new ResultReceiver( + new Handler(Looper.getMainLooper())) { + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + Log.i(TAG, "onReceiveResult in providerIntentController"); + + if (resultCode == Activity.RESULT_OK) { + Log.i(TAG, "onReceiveResult - ACTIVITYOK"); + mCallback.onProviderIntentResult(resultData); + } else if (resultCode == Activity.RESULT_CANCELED) { + Log.i(TAG, "onReceiveResult - RESULTCANCELED"); + mCallback.onProviderIntentCancelled(); + } + // Drop unknown result + } + }; + + public ProviderIntentController(@UserIdInt int userId, + Context context, + ProviderIntentControllerCallback callback) { + mUserId = userId; + mContext = context; + mCallback = callback; + } + + /** Sets up the request data and invokes the given pending intent. */ + public void setupAndInvokePendingIntent(@NonNull PendingIntent pendingIntent, + CreateCredentialRequest request) { + Log.i(TAG, "in invokePendingIntent"); + setupIntent(pendingIntent, request); + Log.i(TAG, "in invokePendingIntent receiver set up"); + Log.i(TAG, "creator package: " + pendingIntent.getIntentSender() + .getCreatorPackage()); + + try { + mContext.startIntentSender(pendingIntent.getIntentSender(), + null, 0, 0, 0); + } catch (IntentSender.SendIntentException e) { + Log.i(TAG, "Error while invoking pending intent"); + } + + } + + private void setupIntent(PendingIntent pendingIntent, CreateCredentialRequest request) { + pendingIntent.getIntent().putExtra(Intent.EXTRA_RESULT_RECEIVER, + toIpcFriendlyResultReceiver(mResultReceiver)); + pendingIntent.getIntent().putExtra( + CredentialProviderService.EXTRA_CREATE_CREDENTIAL_REQUEST_PARAMS, + request.getData()); + } + + private <T extends ResultReceiver> ResultReceiver toIpcFriendlyResultReceiver( + T resultReceiver) { + final Parcel parcel = Parcel.obtain(); + resultReceiver.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + + final ResultReceiver ipcFriendly = ResultReceiver.CREATOR.createFromParcel(parcel); + parcel.recycle(); + + return ipcFriendly; + } +} diff --git a/services/credentials/java/com/android/server/credentials/ProviderSession.java b/services/credentials/java/com/android/server/credentials/ProviderSession.java index 3a9f96432d8c..14a915754863 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderSession.java @@ -17,25 +17,70 @@ package com.android.server.credentials; import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.ComponentName; +import android.content.Context; +import android.credentials.Credential; import android.credentials.ui.ProviderData; +import android.os.Bundle; import android.service.credentials.CredentialProviderException; import android.service.credentials.CredentialProviderInfo; +import java.util.UUID; + /** * Provider session storing the state of provider response and ui entries. - * @param <T> The request type expected from the remote provider, for a given request session. + * @param <T> The request to be sent to the provider + * @param <R> The response to be expected from the provider */ -public abstract class ProviderSession<T> implements RemoteCredentialService.ProviderCallbacks<T> { +public abstract class ProviderSession<T, R> implements RemoteCredentialService.ProviderCallbacks<R>, + ProviderIntentController.ProviderIntentControllerCallback { // Key to be used as the entry key for an action entry protected static final String ACTION_ENTRY_KEY = "action_key"; + @NonNull protected final Context mContext; @NonNull protected final ComponentName mComponentName; @NonNull protected final CredentialProviderInfo mProviderInfo; @NonNull protected final RemoteCredentialService mRemoteCredentialService; @NonNull protected final int mUserId; @NonNull protected Status mStatus = Status.NOT_STARTED; @NonNull protected final ProviderInternalCallback mCallbacks; + @NonNull protected final ProviderIntentController mProviderIntentController; + @Nullable protected Credential mFinalCredentialResponse; + @NonNull protected final T mProviderRequest; + @Nullable protected R mProviderResponse; + + /** + * Returns true if the given status reflects that the provider state is ready to be shown + * on the credMan UI. + */ + public static boolean isUiInvokingStatus(Status status) { + return status == Status.CREDENTIALS_RECEIVED || status == Status.SAVE_ENTRIES_RECEIVED; + } + + /** + * Returns true if the given status reflects that the provider is waiting for a remote + * response. + */ + public static boolean isStatusWaitingForRemoteResponse(Status status) { + return status == Status.PENDING; + } + + /** + * Returns true if the given status means that the provider session must be terminated. + */ + public static boolean isTerminatingStatus(Status status) { + return status == Status.CANCELED || status == Status.SERVICE_DEAD; + } + + /** + * Returns true if the given status reflects that the provider is done getting the response, + * and is ready to return the final credential back to the user. + */ + public static boolean isCompletionStatus(Status status) { + return status == Status.CREDENTIAL_RECEIVED_FROM_INTENT + || status == Status.CREDENTIAL_RECEIVED_FROM_SELECTION; + } /** * Interface to be implemented by any class that wishes to get a callback when a particular @@ -49,35 +94,49 @@ public abstract class ProviderSession<T> implements RemoteCredentialService.Prov void onProviderStatusChanged(Status status, ComponentName componentName); } - protected ProviderSession(@NonNull CredentialProviderInfo info, + protected ProviderSession(@NonNull Context context, @NonNull CredentialProviderInfo info, + @NonNull T providerRequest, @NonNull ProviderInternalCallback callbacks, @NonNull int userId, @NonNull RemoteCredentialService remoteCredentialService) { + mContext = context; mProviderInfo = info; + mProviderRequest = providerRequest; mCallbacks = callbacks; mUserId = userId; mComponentName = info.getServiceInfo().getComponentName(); mRemoteCredentialService = remoteCredentialService; + mProviderIntentController = new ProviderIntentController(userId, context, this); } - /** Update the response state stored with the provider session. */ - protected abstract void updateResponse (T response); - - /** Update the response state stored with the provider session. */ - protected abstract T getResponse (); - - /** Should be overridden to prepare, and stores state for {@link ProviderData} to be - * shown on the UI. */ - protected abstract ProviderData prepareUiData(); - /** Provider status at various states of the request session. */ + // TODO: Review status values, and adjust where needed enum Status { NOT_STARTED, PENDING, REQUIRES_AUTHENTICATION, - COMPLETE, + CREDENTIALS_RECEIVED, SERVICE_DEAD, - CANCELED + CREDENTIAL_RECEIVED_FROM_INTENT, + PENDING_INTENT_INVOKED, + CREDENTIAL_RECEIVED_FROM_SELECTION, + SAVE_ENTRIES_RECEIVED, CANCELED + } + + /** Converts exception to a provider session status. */ + @NonNull + public static Status toStatus( + @CredentialProviderException.CredentialProviderError int errorCode) { + // TODO : Add more mappings as more flows are supported + return Status.CANCELED; + } + + protected String generateEntryId() { + return UUID.randomUUID().toString(); + } + + public Credential getFinalCredentialResponse() { + return mFinalCredentialResponse; } protected void setStatus(@NonNull Status status) { @@ -94,31 +153,38 @@ public abstract class ProviderSession<T> implements RemoteCredentialService.Prov return mComponentName; } + @NonNull + protected RemoteCredentialService getRemoteCredentialService() { + return mRemoteCredentialService; + } + /** Updates the status .*/ protected void updateStatusAndInvokeCallback(@NonNull Status status) { setStatus(status); mCallbacks.onProviderStatusChanged(status, mComponentName); } - @NonNull - public static Status toStatus( - @CredentialProviderException.CredentialProviderError int errorCode) { - // TODO : Add more mappings as more flows are supported - return Status.CANCELED; + /** Get the request to be sent to the provider. */ + protected T getProviderRequest() { + return mProviderRequest; } - /** - * Returns true if the given status means that the provider session must be terminated. - */ - public static boolean isTerminatingStatus(Status status) { - return status == Status.CANCELED || status == Status.SERVICE_DEAD; + /** Update the response state stored with the provider session. */ + @Nullable protected R getProviderResponse() { + return mProviderResponse; } - /** - * Returns true if the given status means that the provider is done getting the response, - * and is ready for user interaction. - */ - public static boolean isCompletionStatus(Status status) { - return status == Status.COMPLETE || status == Status.REQUIRES_AUTHENTICATION; - } + /** Should be overridden to prepare, and stores state for {@link ProviderData} to be + * shown on the UI. */ + @Nullable protected abstract ProviderData prepareUiData(); + + /** Should be overridden to handle the selected entry from the UI. */ + protected abstract void onUiEntrySelected(String entryType, String entryId); + + @Override + public abstract void onProviderIntentResult(Bundle resultData); + + @Override + public abstract void onProviderIntentCancelled(); + } diff --git a/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java b/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java index d0b6e7d6238c..c2464b5d235e 100644 --- a/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java +++ b/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java @@ -24,11 +24,14 @@ import android.content.Intent; import android.os.Handler; import android.os.ICancellationSignal; import android.os.RemoteException; +import android.service.credentials.CreateCredentialRequest; +import android.service.credentials.CreateCredentialResponse; import android.service.credentials.CredentialProviderException; import android.service.credentials.CredentialProviderException.CredentialProviderError; import android.service.credentials.CredentialProviderService; import android.service.credentials.GetCredentialsRequest; import android.service.credentials.GetCredentialsResponse; +import android.service.credentials.ICreateCredentialCallback; import android.service.credentials.ICredentialProviderService; import android.service.credentials.IGetCredentialsCallback; import android.text.format.DateUtils; @@ -76,7 +79,7 @@ public class RemoteCredentialService extends ServiceConnector.Impl<ICredentialPr public RemoteCredentialService(@NonNull Context context, @NonNull ComponentName componentName, int userId) { super(context, new Intent(CredentialProviderService.SERVICE_INTERFACE) - .setComponent(componentName), Context.BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS, + .setComponent(componentName), /*bindingFlags=*/0, userId, ICredentialProviderService.Stub::asInterface); mComponentName = componentName; } @@ -101,7 +104,7 @@ public class RemoteCredentialService extends ServiceConnector.Impl<ICredentialPr * provider service. * @param request the request to be sent to the provider * @param callback the callback to be used to send back the provider response to the - * {@link ProviderSession} class that maintains provider state + * {@link ProviderGetSession} class that maintains provider state */ public void onGetCredentials(@NonNull GetCredentialsRequest request, ProviderCallbacks<GetCredentialsResponse> callback) { @@ -114,21 +117,21 @@ public class RemoteCredentialService extends ServiceConnector.Impl<ICredentialPr CompletableFuture<GetCredentialsResponse> getCredentials = new CompletableFuture<>(); ICancellationSignal cancellationSignal = service.onGetCredentials(request, new IGetCredentialsCallback.Stub() { - @Override - public void onSuccess(GetCredentialsResponse response) { - Log.i(TAG, "In onSuccess in RemoteCredentialService"); - getCredentials.complete(response); - } - - @Override - public void onFailure(@CredentialProviderError int errorCode, - CharSequence message) { - Log.i(TAG, "In onFailure in RemoteCredentialService"); - String errorMsg = message == null ? "" : String.valueOf(message); - getCredentials.completeExceptionally(new CredentialProviderException( - errorCode, errorMsg)); - } - }); + @Override + public void onSuccess(GetCredentialsResponse response) { + Log.i(TAG, "In onSuccess in RemoteCredentialService"); + getCredentials.complete(response); + } + + @Override + public void onFailure(@CredentialProviderError int errorCode, + CharSequence message) { + Log.i(TAG, "In onFailure in RemoteCredentialService"); + String errorMsg = message == null ? "" : String.valueOf(message); + getCredentials.completeExceptionally(new CredentialProviderException( + errorCode, errorMsg)); + } + }); CompletableFuture<GetCredentialsResponse> future = futureRef.get(); if (future != null && future.isCancelled()) { dispatchCancellationSignal(cancellationSignal); @@ -137,38 +140,91 @@ public class RemoteCredentialService extends ServiceConnector.Impl<ICredentialPr } return getCredentials; }).orTimeout(TIMEOUT_REQUEST_MILLIS, TimeUnit.MILLISECONDS); + + futureRef.set(connectThenExecute); + connectThenExecute.whenComplete((result, error) -> Handler.getMain().post(() -> + handleExecutionResponse(result, error, cancellationSink, callback))); + } + + /** Main entry point to be called for executing a createCredential call on the remote + * provider service. + * @param request the request to be sent to the provider + * @param callback the callback to be used to send back the provider response to the + * {@link ProviderCreateSession} class that maintains provider state + */ + public void onCreateCredential(@NonNull CreateCredentialRequest request, + ProviderCallbacks<CreateCredentialResponse> callback) { + Log.i(TAG, "In onCreateCredential in RemoteCredentialService"); + AtomicReference<ICancellationSignal> cancellationSink = new AtomicReference<>(); + AtomicReference<CompletableFuture<CreateCredentialResponse>> futureRef = + new AtomicReference<>(); + + CompletableFuture<CreateCredentialResponse> connectThenExecute = postAsync(service -> { + CompletableFuture<CreateCredentialResponse> createCredentialFuture = + new CompletableFuture<>(); + ICancellationSignal cancellationSignal = service.onCreateCredential( + request, new ICreateCredentialCallback.Stub() { + @Override + public void onSuccess(CreateCredentialResponse response) { + Log.i(TAG, "In onSuccess onCreateCredential " + + "in RemoteCredentialService"); + createCredentialFuture.complete(response); + } + + @Override + public void onFailure(@CredentialProviderError int errorCode, + CharSequence message) { + Log.i(TAG, "In onFailure in RemoteCredentialService"); + String errorMsg = message == null ? "" : String.valueOf(message); + createCredentialFuture.completeExceptionally( + new CredentialProviderException(errorCode, errorMsg)); + }}); + CompletableFuture<CreateCredentialResponse> future = futureRef.get(); + if (future != null && future.isCancelled()) { + dispatchCancellationSignal(cancellationSignal); + } else { + cancellationSink.set(cancellationSignal); + } + return createCredentialFuture; + }).orTimeout(TIMEOUT_REQUEST_MILLIS, TimeUnit.MILLISECONDS); + futureRef.set(connectThenExecute); + connectThenExecute.whenComplete((result, error) -> Handler.getMain().post(() -> + handleExecutionResponse(result, error, cancellationSink, callback))); + } - connectThenExecute.whenComplete((result, error) -> Handler.getMain().post(() -> { - if (error == null) { - Log.i(TAG, "In RemoteCredentialService execute error is null"); - callback.onProviderResponseSuccess(result); + private <T> void handleExecutionResponse(T result, + Throwable error, + AtomicReference<ICancellationSignal> cancellationSink, + ProviderCallbacks<T> callback) { + if (error == null) { + Log.i(TAG, "In RemoteCredentialService execute error is null"); + callback.onProviderResponseSuccess(result); + } else { + if (error instanceof TimeoutException) { + Log.i(TAG, "In RemoteCredentialService execute error is timeout"); + dispatchCancellationSignal(cancellationSink.get()); + callback.onProviderResponseFailure( + CredentialProviderException.ERROR_TIMEOUT, + error.getMessage()); + } else if (error instanceof CancellationException) { + Log.i(TAG, "In RemoteCredentialService execute error is cancellation"); + dispatchCancellationSignal(cancellationSink.get()); + callback.onProviderResponseFailure( + CredentialProviderException.ERROR_TASK_CANCELED, + error.getMessage()); + } else if (error instanceof CredentialProviderException) { + Log.i(TAG, "In RemoteCredentialService execute error is provider error"); + callback.onProviderResponseFailure(((CredentialProviderException) error) + .getErrorCode(), + error.getMessage()); } else { - if (error instanceof TimeoutException) { - Log.i(TAG, "In RemoteCredentialService execute error is timeout"); - dispatchCancellationSignal(cancellationSink.get()); - callback.onProviderResponseFailure( - CredentialProviderException.ERROR_TIMEOUT, - error.getMessage()); - } else if (error instanceof CancellationException) { - Log.i(TAG, "In RemoteCredentialService execute error is cancellation"); - dispatchCancellationSignal(cancellationSink.get()); - callback.onProviderResponseFailure( - CredentialProviderException.ERROR_TASK_CANCELED, - error.getMessage()); - } else if (error instanceof CredentialProviderException) { - Log.i(TAG, "In RemoteCredentialService execute error is provider error"); - callback.onProviderResponseFailure(((CredentialProviderException) error) - .getErrorCode(), - error.getMessage()); - } else { - Log.i(TAG, "In RemoteCredentialService execute error is unknown"); - callback.onProviderResponseFailure( - CredentialProviderException.ERROR_UNKNOWN, - error.getMessage()); - } + Log.i(TAG, "In RemoteCredentialService execute error is unknown"); + callback.onProviderResponseFailure( + CredentialProviderException.ERROR_UNKNOWN, + error.getMessage()); } - })); + } } private void dispatchCancellationSignal(@Nullable ICancellationSignal signal) { diff --git a/services/credentials/java/com/android/server/credentials/RequestSession.java b/services/credentials/java/com/android/server/credentials/RequestSession.java index 1bacbb342edb..056d0e8718be 100644 --- a/services/credentials/java/com/android/server/credentials/RequestSession.java +++ b/services/credentials/java/com/android/server/credentials/RequestSession.java @@ -20,18 +20,30 @@ import android.annotation.NonNull; import android.annotation.UserIdInt; import android.content.ComponentName; import android.content.Context; +import android.credentials.ui.ProviderData; import android.credentials.ui.UserSelectionDialogResult; import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.os.Looper; +import android.service.credentials.CredentialProviderInfo; +import android.util.Log; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; /** * Base class of a request session, that listens to UI events. This class must be extended * every time a new response type is expected from the providers. */ -abstract class RequestSession implements CredentialManagerUi.CredentialManagerUiCallback, +abstract class RequestSession<T, U> implements CredentialManagerUi.CredentialManagerUiCallback, ProviderSession.ProviderInternalCallback { + private static final String TAG = "RequestSession"; + + // TODO: Revise access levels of attributes + @NonNull protected final T mClientRequest; + @NonNull protected final U mClientCallback; @NonNull protected final IBinder mRequestId; @NonNull protected final Context mContext; @NonNull protected final CredentialManagerUi mCredentialManagerUi; @@ -39,29 +51,120 @@ abstract class RequestSession implements CredentialManagerUi.CredentialManagerUi @NonNull protected final Handler mHandler; @NonNull protected boolean mIsFirstUiTurn = true; @UserIdInt protected final int mUserId; + @NonNull protected final String mClientCallingPackage; + + protected final Map<String, ProviderSession> mProviders = new HashMap<>(); protected RequestSession(@NonNull Context context, - @UserIdInt int userId, @NonNull String requestType) { + @UserIdInt int userId, @NonNull T clientRequest, U clientCallback, + @NonNull String requestType, + String clientCallingPackage) { mContext = context; mUserId = userId; + mClientRequest = clientRequest; + mClientCallback = clientCallback; mRequestType = requestType; + mClientCallingPackage = clientCallingPackage; mHandler = new Handler(Looper.getMainLooper(), null, true); mRequestId = new Binder(); mCredentialManagerUi = new CredentialManagerUi(mContext, mUserId, this); } - /** Returns the unique identifier of this request session. */ - public IBinder getRequestId() { - return mRequestId; + public abstract ProviderSession initiateProviderSession(CredentialProviderInfo providerInfo, + RemoteCredentialService remoteCredentialService); + + protected abstract void launchUiWithProviderData(ArrayList<ProviderData> providerDataList); + + // UI callbacks + + @Override // from CredentialManagerUiCallbacks + public void onUiSelection(UserSelectionDialogResult selection) { + String providerId = selection.getProviderId(); + Log.i(TAG, "onUiSelection, providerId: " + providerId); + ProviderSession providerSession = mProviders.get(providerId); + if (providerSession == null) { + Log.i(TAG, "providerSession not found in onUiSelection"); + return; + } + Log.i(TAG, "Provider session found"); + providerSession.onUiEntrySelected(selection.getEntryKey(), + selection.getEntrySubkey()); } - @Override // from CredentialManagerUiCallback - public abstract void onUiSelection(UserSelectionDialogResult selection); + @Override // from CredentialManagerUiCallbacks + public void onUiCancellation() { + // User canceled the activity + finishSession(); + } - @Override // from CredentialManagerUiCallback - public abstract void onUiCancelation(); + @Override // from provider session + public void onProviderStatusChanged(ProviderSession.Status status, + ComponentName componentName) { + Log.i(TAG, "in onStatusChanged with status: " + status); + if (ProviderSession.isTerminatingStatus(status)) { + Log.i(TAG, "in onStatusChanged terminating status"); + onProviderTerminated(componentName); + //TODO: Check if this was the provider we were waiting for and can invoke the UI now + } else if (ProviderSession.isCompletionStatus(status)) { + Log.i(TAG, "in onStatusChanged isCompletionStatus status"); + onProviderResponseComplete(componentName); + } else if (ProviderSession.isUiInvokingStatus(status)) { + Log.i(TAG, "in onStatusChanged isUiInvokingStatus status"); + onProviderResponseRequiresUi(); + } + } - @Override // from ProviderInternalCallback - public abstract void onProviderStatusChanged(ProviderSession.Status status, ComponentName componentName); + protected void onProviderTerminated(ComponentName componentName) { + //TODO: Implement + } + + protected void onProviderResponseComplete(ComponentName componentName) { + //TODO: Implement + } + + protected void onProviderResponseRequiresUi() { + Log.i(TAG, "in onProviderResponseComplete"); + // TODO: Determine whether UI has already been invoked, and deal accordingly + if (!isAnyProviderPending()) { + Log.i(TAG, "in onProviderResponseComplete - isResponseCompleteAcrossProviders"); + getProviderDataAndInitiateUi(); + } else { + Log.i(TAG, "Can't invoke UI - waiting on some providers"); + } + } + + protected void finishSession() { + clearProviderSessions(); + } + + protected void clearProviderSessions() { + //TODO: Implement + mProviders.clear(); + } + + private boolean isAnyProviderPending() { + for (ProviderSession session : mProviders.values()) { + if (ProviderSession.isStatusWaitingForRemoteResponse(session.getStatus())) { + return true; + } + } + return false; + } + + private void getProviderDataAndInitiateUi() { + ArrayList<ProviderData> providerDataList = new ArrayList<>(); + for (ProviderSession session : mProviders.values()) { + Log.i(TAG, "preparing data for : " + session.getComponentName()); + ProviderData providerData = session.prepareUiData(); + if (providerData != null) { + Log.i(TAG, "Provider data is not null"); + providerDataList.add(providerData); + } + } + if (!providerDataList.isEmpty()) { + Log.i(TAG, "provider list not empty about to initiate ui"); + launchUiWithProviderData(providerDataList); + } + } } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 316c736ff92a..89cbf5324ed4 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 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { @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 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } @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 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { "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 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { 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 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { + " 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 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } @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 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { 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 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { // 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 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { 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/java/com/android/server/BootUserInitializer.java b/services/java/com/android/server/BootUserInitializer.java index 46e59a9f10dd..c3329795d4e2 100644 --- a/services/java/com/android/server/BootUserInitializer.java +++ b/services/java/com/android/server/BootUserInitializer.java @@ -83,9 +83,10 @@ final class BootUserInitializer { Slogf.d(TAG, "Creating initial user"); t.traceBegin("create-initial-user"); try { + int flags = UserInfo.FLAG_ADMIN | UserInfo.FLAG_MAIN; // TODO(b/204091126): proper name for user UserInfo newUser = um.createUserEvenWhenDisallowed("Real User", - UserManager.USER_TYPE_FULL_SECONDARY, UserInfo.FLAG_ADMIN, + UserManager.USER_TYPE_FULL_SECONDARY, flags, /* disallowedPackages= */ null, /* token= */ null); Slogf.i(TAG, "Created initial user: %s", newUser.toFullString()); initialUserId = newUser.id; diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index fe2d0be0fe36..d406e300a0eb 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -103,6 +103,7 @@ import com.android.internal.util.ConcurrentUtils; import com.android.internal.util.EmergencyAffordanceManager; import com.android.internal.util.FrameworkStatsLog; import com.android.internal.widget.ILockSettings; +import com.android.internal.widget.LockSettingsInternal; import com.android.server.am.ActivityManagerService; import com.android.server.ambientcontext.AmbientContextManagerService; import com.android.server.appbinding.AppBindingService; @@ -3019,6 +3020,14 @@ public final class SystemServer implements Dumpable { t.traceEnd(); }, t); + t.traceBegin("LockSettingsThirdPartyAppsStarted"); + LockSettingsInternal lockSettingsInternal = + LocalServices.getService(LockSettingsInternal.class); + if (lockSettingsInternal != null) { + lockSettingsInternal.onThirdPartyAppsStarted(); + } + t.traceEnd(); + t.traceBegin("StartSystemUI"); try { startSystemUi(context, windowManagerF); 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 3fbc4004785d..640bde330cee 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 @@ public class InputMethodManagerServiceTestBase { .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 @@ public class InputMethodManagerServiceTestBase { .setCurrentMethodNotVisible(); } verify(mMockInputMethod, times(hideSoftInput ? 1 : 0)) - .hideSoftInput(any(), anyInt(), any()); + .hideSoftInput(any(), any(), anyInt(), any()); } } diff --git a/services/tests/mockingservicestests/Android.bp b/services/tests/mockingservicestests/Android.bp index 73b1907c9f42..681bfcf68cc3 100644 --- a/services/tests/mockingservicestests/Android.bp +++ b/services/tests/mockingservicestests/Android.bp @@ -56,6 +56,7 @@ android_test { "service-jobscheduler", "service-permission.impl", "service-sdksandbox.impl", + "services.backup", "services.companion", "services.core", "services.devicepolicy", diff --git a/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java index d78f6d83d0ab..24e5175ecbdc 100644 --- a/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java @@ -1507,6 +1507,39 @@ public class GameManagerServiceTests { } @Test + public void testSwitchUser() { + mockManageUsersGranted(); + mockModifyGameModeGranted(); + + mockDeviceConfigBattery(); + final Context context = InstrumentationRegistry.getContext(); + GameManagerService gameManagerService = new GameManagerService(mMockContext, + mTestLooper.getLooper(), context.getFilesDir()); + startUser(gameManagerService, USER_ID_1); + startUser(gameManagerService, USER_ID_2); + gameManagerService.setGameMode(mPackageName, GameManager.GAME_MODE_BATTERY, USER_ID_1); + checkReportedModes(gameManagerService, GameManager.GAME_MODE_STANDARD, + GameManager.GAME_MODE_BATTERY); + assertEquals(gameManagerService.getGameMode(mPackageName, USER_ID_1), + GameManager.GAME_MODE_BATTERY); + + mockDeviceConfigAll(); + switchUser(gameManagerService, USER_ID_1, USER_ID_2); + assertEquals(gameManagerService.getGameMode(mPackageName, USER_ID_2), + GameManager.GAME_MODE_STANDARD); + checkReportedModes(gameManagerService, GameManager.GAME_MODE_STANDARD, + GameManager.GAME_MODE_BATTERY, GameManager.GAME_MODE_PERFORMANCE); + gameManagerService.setGameMode(mPackageName, GameManager.GAME_MODE_PERFORMANCE, USER_ID_2); + gameManagerService.setGameMode(mPackageName, GameManager.GAME_MODE_BATTERY, USER_ID_1); + + switchUser(gameManagerService, USER_ID_2, USER_ID_1); + checkReportedModes(gameManagerService, GameManager.GAME_MODE_STANDARD, + GameManager.GAME_MODE_BATTERY, GameManager.GAME_MODE_PERFORMANCE); + gameManagerService.setGameMode(mPackageName, GameManager.GAME_MODE_PERFORMANCE, USER_ID_2); + gameManagerService.setGameMode(mPackageName, GameManager.GAME_MODE_BATTERY, USER_ID_1); + } + + @Test public void testUpdateResolutionScalingFactor() { mockModifyGameModeGranted(); mockDeviceConfigBattery(); 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 e1713b0beb77..98e895a86f9e 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.MODE_IGNORED; 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 @@ public class AppOpsUidStateTrackerTest { 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 @@ public class AppOpsUidStateTrackerTest { .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 @@ public class AppOpsUidStateTrackerTest { .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 @@ public class AppOpsUidStateTrackerTest { 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 @@ public class AppOpsUidStateTrackerTest { 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 @@ public class AppOpsUidStateTrackerTest { 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 @@ public class AppOpsUidStateTrackerTest { 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 @@ public class AppOpsUidStateTrackerTest { .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 @@ public class AppOpsUidStateTrackerTest { .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 @@ public class AppOpsUidStateTrackerTest { 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 @@ public class AppOpsUidStateTrackerTest { 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 @@ public class AppOpsUidStateTrackerTest { 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/mockingservicestests/src/com/android/server/backup/BackupAndRestoreFeatureFlagsTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/BackupAndRestoreFeatureFlagsTest.java new file mode 100644 index 000000000000..f53599731e10 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/backup/BackupAndRestoreFeatureFlagsTest.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.backup; + +import android.platform.test.annotations.Presubmit; +import android.provider.DeviceConfig; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.modules.utils.testing.TestableDeviceConfig; + +import static com.google.common.truth.Truth.assertThat; + + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@Presubmit +@RunWith(AndroidJUnit4.class) +public class BackupAndRestoreFeatureFlagsTest { + @Rule + public TestableDeviceConfig.TestableDeviceConfigRule + mDeviceConfigRule = new TestableDeviceConfig.TestableDeviceConfigRule(); + + @Test + public void getBackupTransportFutureTimeoutMillis_notSet_returnsDefault() { + assertThat( + BackupAndRestoreFeatureFlags.getBackupTransportFutureTimeoutMillis()).isEqualTo( + 600000); + } + + @Test + public void getBackupTransportFutureTimeoutMillis_set_returnsSetValue() { + DeviceConfig.setProperty("backup_and_restore", "backup_transport_future_timeout_millis", + "1234", false); + + assertThat( + BackupAndRestoreFeatureFlags.getBackupTransportFutureTimeoutMillis()).isEqualTo( + 1234); + } + + @Test + public void getBackupTransportCallbackTimeoutMillis_notSet_returnsDefault() { + assertThat( + BackupAndRestoreFeatureFlags.getBackupTransportCallbackTimeoutMillis()).isEqualTo( + 300000); + } + + @Test + public void getBackupTransportCallbackTimeoutMillis_set_returnsSetValue() { + DeviceConfig.setProperty("backup_and_restore", "backup_transport_callback_timeout_millis", + "5678", false); + + assertThat( + BackupAndRestoreFeatureFlags.getBackupTransportCallbackTimeoutMillis()).isEqualTo( + 5678); + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUMDTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUMDTest.java index 923c3e385b5e..9be370fe3045 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUMDTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUMDTest.java @@ -153,14 +153,8 @@ public final class UserVisibilityMediatorMUMDTest extends UserVisibilityMediator assertUserAssignedToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID); } - @Test - public void testUnassignUserFromDisplay() { - testAssignUserToDisplay_displayAvailable(); - - mMediator.unassignUserFromDisplay(USER_ID); - - assertNoUserAssignedToDisplay(); - } + // TODO(b/244644281): when start & assign are merged, rename tests above and also call + // stopUserAndAssertState() at the end of them @Test public void testIsUserVisible_bgUserOnSecondaryDisplay() { diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorSUSDTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorSUSDTest.java index 7af5f5d6b2fe..7abdd9e7bbaf 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorSUSDTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorSUSDTest.java @@ -15,8 +15,6 @@ */ package com.android.server.pm; -import static android.os.UserHandle.USER_SYSTEM; - import static org.junit.Assert.assertThrows; import org.junit.Test; @@ -34,6 +32,9 @@ public final class UserVisibilityMediatorSUSDTest extends UserVisibilityMediator super(/* usersOnSecondaryDisplaysEnabled= */ false); } + // TODO(b/244644281): when start & assign are merged, rename tests below and also call + // stopUserAndAssertState() at the end of them + @Test public void testAssignUserToDisplay_otherDisplay_currentUser() { mockCurrentUser(USER_ID); @@ -59,15 +60,4 @@ public final class UserVisibilityMediatorSUSDTest extends UserVisibilityMediator assertThrows(UnsupportedOperationException.class, () -> mMediator .assignUserToDisplay(PROFILE_USER_ID, PARENT_USER_ID, SECONDARY_DISPLAY_ID)); } - - @Test - public void testUnassignUserFromDisplay_ignored() { - mockCurrentUser(USER_ID); - - mMediator.unassignUserFromDisplay(USER_SYSTEM); - mMediator.unassignUserFromDisplay(USER_ID); - mMediator.unassignUserFromDisplay(OTHER_USER_ID); - - assertNoUserAssignedToDisplay(); - } } diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java index 7b20092b503a..e8be97db717d 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java @@ -20,11 +20,11 @@ import static android.os.UserHandle.USER_NULL; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.INVALID_DISPLAY; +import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_FAILURE; +import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE; +import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE; +import static com.android.server.pm.UserManagerInternal.userAssignmentResultToString; import static com.android.server.pm.UserVisibilityMediator.INITIAL_CURRENT_USER_ID; -import static com.android.server.pm.UserVisibilityMediator.START_USER_RESULT_FAILURE; -import static com.android.server.pm.UserVisibilityMediator.START_USER_RESULT_SUCCESS_INVISIBLE; -import static com.android.server.pm.UserVisibilityMediator.START_USER_RESULT_SUCCESS_VISIBLE; -import static com.android.server.pm.UserVisibilityMediator.startUserResultToString; import static com.google.common.truth.Truth.assertWithMessage; @@ -100,79 +100,92 @@ abstract class UserVisibilityMediatorTestCase extends ExtendedMockitoTestCase { @Test public final void testStartUser_currentUser() { - int result = mMediator.startUser(USER_ID, USER_ID, FG, DEFAULT_DISPLAY); - assertStartUserResult(result, START_USER_RESULT_SUCCESS_VISIBLE); + int result = mMediator.startOnly(USER_ID, USER_ID, FG, DEFAULT_DISPLAY); + assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE); assertCurrentUser(USER_ID); assertIsCurrentUserOrRunningProfileOfCurrentUser(USER_ID); assertStartedProfileGroupIdOf(USER_ID, USER_ID); + + stopUserAndAssertState(USER_ID); } @Test public final void testStartUser_currentUserSecondaryDisplay() { - int result = mMediator.startUser(USER_ID, USER_ID, FG, SECONDARY_DISPLAY_ID); - assertStartUserResult(result, START_USER_RESULT_FAILURE); + int result = mMediator.startOnly(USER_ID, USER_ID, FG, SECONDARY_DISPLAY_ID); + assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE); assertCurrentUser(INITIAL_CURRENT_USER_ID); assertIsNotCurrentUserOrRunningProfileOfCurrentUser(USER_ID); assertStartedProfileGroupIdOf(USER_ID, NO_PROFILE_GROUP_ID); + + stopUserAndAssertState(USER_ID); } @Test public final void testStartUser_profileBg_parentStarted() { mockCurrentUser(PARENT_USER_ID); - int result = mMediator.startUser(PROFILE_USER_ID, PARENT_USER_ID, BG, DEFAULT_DISPLAY); - assertStartUserResult(result, START_USER_RESULT_SUCCESS_VISIBLE); + int result = mMediator.startOnly(PROFILE_USER_ID, PARENT_USER_ID, BG, DEFAULT_DISPLAY); + assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE); assertCurrentUser(PARENT_USER_ID); assertIsCurrentUserOrRunningProfileOfCurrentUser(PROFILE_USER_ID); assertStartedProfileGroupIdOf(PROFILE_USER_ID, PARENT_USER_ID); - assertIsStartedProfile(PROFILE_USER_ID); + assertProfileIsStarted(PROFILE_USER_ID); + + stopUserAndAssertState(USER_ID); } @Test public final void testStartUser_profileBg_parentNotStarted() { - int result = mMediator.startUser(PROFILE_USER_ID, PARENT_USER_ID, BG, DEFAULT_DISPLAY); - assertStartUserResult(result, START_USER_RESULT_SUCCESS_INVISIBLE); + int result = mMediator.startOnly(PROFILE_USER_ID, PARENT_USER_ID, BG, DEFAULT_DISPLAY); + assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE); assertCurrentUser(INITIAL_CURRENT_USER_ID); assertIsNotCurrentUserOrRunningProfileOfCurrentUser(PROFILE_USER_ID); assertStartedProfileGroupIdOf(PROFILE_USER_ID, PARENT_USER_ID); - assertIsStartedProfile(PROFILE_USER_ID); + assertProfileIsStarted(PROFILE_USER_ID); + + stopUserAndAssertState(USER_ID); } @Test public final void testStartUser_profileBg_secondaryDisplay() { - int result = mMediator.startUser(PROFILE_USER_ID, PARENT_USER_ID, BG, SECONDARY_DISPLAY_ID); - assertStartUserResult(result, START_USER_RESULT_FAILURE); + int result = mMediator.startOnly(PROFILE_USER_ID, PARENT_USER_ID, BG, SECONDARY_DISPLAY_ID); + assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE); assertCurrentUser(INITIAL_CURRENT_USER_ID); assertIsNotCurrentUserOrRunningProfileOfCurrentUser(PROFILE_USER_ID); + + stopUserAndAssertState(USER_ID); } @Test public final void testStartUser_profileFg() { - int result = mMediator.startUser(PROFILE_USER_ID, PARENT_USER_ID, FG, DEFAULT_DISPLAY); - assertStartUserResult(result, START_USER_RESULT_FAILURE); + int result = mMediator.startOnly(PROFILE_USER_ID, PARENT_USER_ID, FG, DEFAULT_DISPLAY); + assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE); assertCurrentUser(INITIAL_CURRENT_USER_ID); assertIsNotCurrentUserOrRunningProfileOfCurrentUser(PROFILE_USER_ID); - assertStartedProfileGroupIdOf(PROFILE_USER_ID, NO_PROFILE_GROUP_ID); + + stopUserAndAssertState(USER_ID); } @Test public final void testStartUser_profileFgSecondaryDisplay() { - int result = mMediator.startUser(PROFILE_USER_ID, PARENT_USER_ID, FG, SECONDARY_DISPLAY_ID); + int result = mMediator.startOnly(PROFILE_USER_ID, PARENT_USER_ID, FG, SECONDARY_DISPLAY_ID); - assertStartUserResult(result, START_USER_RESULT_FAILURE); + assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE); assertCurrentUser(INITIAL_CURRENT_USER_ID); + + stopUserAndAssertState(USER_ID); } @Test public final void testGetStartedProfileGroupId_whenStartedWithNoProfileGroupId() { - int result = mMediator.startUser(USER_ID, NO_PROFILE_GROUP_ID, FG, DEFAULT_DISPLAY); - assertStartUserResult(result, START_USER_RESULT_SUCCESS_VISIBLE); + int result = mMediator.startOnly(USER_ID, NO_PROFILE_GROUP_ID, FG, DEFAULT_DISPLAY); + assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE); assertWithMessage("shit").that(mMediator.getStartedProfileGroupId(USER_ID)) .isEqualTo(USER_ID); @@ -386,59 +399,78 @@ abstract class UserVisibilityMediatorTestCase extends ExtendedMockitoTestCase { .isEqualTo(USER_ID); } + /** + * Stops the given user and assert the proper state is set. + * + * <p>This method should be called at the end of tests that starts a user, so it can test + * {@code stopUser()} as well (technically speaking, {@code stopUser()} should be tested on its + * own methods, but it depends on the user being started at first place, so pragmatically + * speaking, it's better to "reuse" such tests for both (start and stop) + */ + private void stopUserAndAssertState(@UserIdInt int userId) { + mMediator.stopUser(userId); + + assertUserIsStopped(userId); + assertNoUserAssignedToDisplay(); + } + // TODO(b/244644281): remove if start & assign are merged; if they aren't, add a note explaining // it's not meant to be used to test startUser() itself. protected void mockCurrentUser(@UserIdInt int userId) { Log.d(TAG, "mockCurrentUser(" + userId + ")"); - int result = mMediator.startUser(userId, userId, FG, DEFAULT_DISPLAY); - if (result != START_USER_RESULT_SUCCESS_VISIBLE) { + int result = mMediator.startOnly(userId, userId, FG, DEFAULT_DISPLAY); + if (result != USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE) { throw new IllegalStateException("Failed to mock current user " + userId - + ": mediator returned " + startUserResultToString(result)); + + ": mediator returned " + userAssignmentResultToString(result)); } } - // TODO(b/244644281): remove if start & assign are merged; if they aren't, add a note explaining + // TODO(b/244644281): remove when start & assign are merged; or add a note explaining // it's not meant to be used to test startUser() itself. protected void startDefaultProfile() { mockCurrentUser(PARENT_USER_ID); Log.d(TAG, "starting default profile (" + PROFILE_USER_ID + ") in background after starting" + " its parent (" + PARENT_USER_ID + ") on foreground"); - int result = mMediator.startUser(PROFILE_USER_ID, PARENT_USER_ID, BG, DEFAULT_DISPLAY); - if (result != START_USER_RESULT_SUCCESS_VISIBLE) { + int result = mMediator.startOnly(PROFILE_USER_ID, PARENT_USER_ID, BG, DEFAULT_DISPLAY); + if (result != USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE) { throw new IllegalStateException("Failed to start profile user " + PROFILE_USER_ID - + ": mediator returned " + startUserResultToString(result)); + + ": mediator returned " + userAssignmentResultToString(result)); } } - // TODO(b/244644281): remove if start & assign are merged; if they aren't, add a note explaining + // TODO(b/244644281): remove when start & assign are merged; or add a note explaining // it's not meant to be used to test stopUser() itself. protected void stopDefaultProfile() { Log.d(TAG, "stopping default profile"); mMediator.stopUser(PROFILE_USER_ID); } - // TODO(b/244644281): remove if start & assign are merged; if they aren't, add a note explaining + // TODO(b/244644281): remove when start & assign are merged; or add a note explaining // it's not meant to be used to test assignUserToDisplay() itself. protected final void assignUserToDisplay(@UserIdInt int userId, int displayId) { Log.d(TAG, "assignUserToDisplay(" + userId + ", " + displayId + ")"); - int result = mMediator.startUser(userId, userId, BG, displayId); - if (result != START_USER_RESULT_SUCCESS_INVISIBLE) { + int result = mMediator.startOnly(userId, userId, BG, displayId); + if (result != USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE) { throw new IllegalStateException("Failed to startuser " + userId - + " on background: mediator returned " + startUserResultToString(result)); + + " on background: mediator returned " + userAssignmentResultToString(result)); } mMediator.assignUserToDisplay(userId, userId, displayId); } + // TODO(b/244644281): remove when start & assign are merged; or rename to + // assertNoUserAssignedToSecondaryDisplays protected final void assertNoUserAssignedToDisplay() { - assertWithMessage("uses on secondary displays") + assertWithMessage("users on secondary displays") .that(mMediator.getUsersOnSecondaryDisplays()) .isEmpty(); } + // TODO(b/244644281): remove when start & assign are merged; or rename to + // assertUserAssignedToSecondaryDisplay protected final void assertUserAssignedToDisplay(@UserIdInt int userId, int displayId) { - assertWithMessage("uses on secondary displays") + assertWithMessage("users on secondary displays") .that(mMediator.getUsersOnSecondaryDisplays()) .containsExactly(userId, displayId); } @@ -446,24 +478,44 @@ abstract class UserVisibilityMediatorTestCase extends ExtendedMockitoTestCase { private void assertCurrentUser(@UserIdInt int userId) { assertWithMessage("mediator.getCurrentUserId()").that(mMediator.getCurrentUserId()) .isEqualTo(userId); + if (userId != INITIAL_CURRENT_USER_ID) { + assertUserIsStarted(userId); + } + } + + private void assertUserIsStarted(@UserIdInt int userId) { + assertWithMessage("mediator.isStarted(%s)", userId).that(mMediator.isStartedUser(userId)) + .isTrue(); + } + + private void assertUserIsStopped(@UserIdInt int userId) { + assertWithMessage("mediator.isStarted(%s)", userId).that(mMediator.isStartedUser(userId)) + .isFalse(); } - private void assertIsStartedProfile(@UserIdInt int userId) { + private void assertProfileIsStarted(@UserIdInt int userId) { assertWithMessage("mediator.isStartedProfile(%s)", userId) .that(mMediator.isStartedProfile(userId)) .isTrue(); + assertUserIsStarted(userId); } - private void assertStartedProfileGroupIdOf(@UserIdInt int profileId, @UserIdInt int parentId) { - assertWithMessage("mediator.getStartedProfileGroupId(%s)", profileId) - .that(mMediator.getStartedProfileGroupId(profileId)) - .isEqualTo(parentId); + private void assertStartedProfileGroupIdOf(@UserIdInt int userId, + @UserIdInt int profileGroupId) { + assertWithMessage("mediator.getStartedProfileGroupId(%s)", userId) + .that(mMediator.getStartedProfileGroupId(userId)) + .isEqualTo(profileGroupId); } - private void assertIsCurrentUserOrRunningProfileOfCurrentUser(int userId) { + private void assertIsCurrentUserOrRunningProfileOfCurrentUser(@UserIdInt int userId) { assertWithMessage("mediator.isCurrentUserOrRunningProfileOfCurrentUser(%s)", userId) .that(mMediator.isCurrentUserOrRunningProfileOfCurrentUser(userId)) .isTrue(); + if (mMediator.getCurrentUserId() == userId) { + assertUserIsStarted(userId); + } else { + assertProfileIsStarted(userId); + } } private void assertIsNotCurrentUserOrRunningProfileOfCurrentUser(int userId) { @@ -474,8 +526,8 @@ abstract class UserVisibilityMediatorTestCase extends ExtendedMockitoTestCase { private void assertStartUserResult(int actualResult, int expectedResult) { assertWithMessage("startUser() result (where %s=%s and %s=%s)", - actualResult, startUserResultToString(actualResult), - expectedResult, startUserResultToString(expectedResult)) + actualResult, userAssignmentResultToString(actualResult), + expectedResult, userAssignmentResultToString(expectedResult)) .that(actualResult).isEqualTo(expectedResult); } } diff --git a/services/tests/servicestests/AndroidManifest.xml b/services/tests/servicestests/AndroidManifest.xml index 6551bdebe06e..6349b21183fe 100644 --- a/services/tests/servicestests/AndroidManifest.xml +++ b/services/tests/servicestests/AndroidManifest.xml @@ -107,6 +107,9 @@ <queries> <package android:name="com.android.servicestests.apps.suspendtestapp" /> + <intent> + <action android:name="android.media.browse.MediaBrowserService" /> + </intent> </queries> <!-- Uses API introduced in O (26) --> diff --git a/services/tests/servicestests/AndroidTest.xml b/services/tests/servicestests/AndroidTest.xml index 9052f58b2a8b..9c7ce835632a 100644 --- a/services/tests/servicestests/AndroidTest.xml +++ b/services/tests/servicestests/AndroidTest.xml @@ -33,6 +33,7 @@ <option name="test-file-name" value="SimpleServiceTestApp1.apk" /> <option name="test-file-name" value="SimpleServiceTestApp2.apk" /> <option name="test-file-name" value="SimpleServiceTestApp3.apk" /> + <option name="test-file-name" value="FakeMediaApp.apk" /> </target_preparer> <!-- Create place to store apks --> diff --git a/services/tests/servicestests/res/xml/usertypes_test_full.xml b/services/tests/servicestests/res/xml/usertypes_test_full.xml index 099ccbe5b5f6..95681434f193 100644 --- a/services/tests/servicestests/res/xml/usertypes_test_full.xml +++ b/services/tests/servicestests/res/xml/usertypes_test_full.xml @@ -16,7 +16,8 @@ <user-types> <full-type name='android.test.1' - max-allowed-per-parent='12' > + max-allowed-per-parent='12' + max-allowed='17' > <default-restrictions no_remove_user='true' no_bluetooth='true' /> <badge-colors> <item res='@*android:color/profile_badge_1' /> diff --git a/services/tests/servicestests/res/xml/usertypes_test_profile.xml b/services/tests/servicestests/res/xml/usertypes_test_profile.xml index b27f49d3fbeb..1a6dae372a18 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/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java index 80cee50cef79..a49214f9b4f5 100644 --- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java @@ -39,6 +39,8 @@ import static com.android.server.am.UserController.USER_CURRENT_MSG; import static com.android.server.am.UserController.USER_START_MSG; import static com.android.server.am.UserController.USER_SWITCH_TIMEOUT_MSG; import static com.android.server.am.UserController.USER_VISIBILITY_CHANGED_MSG; +import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE; +import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE; import static com.google.android.collect.Lists.newArrayList; import static com.google.android.collect.Sets.newHashSet; @@ -100,6 +102,7 @@ import com.android.server.FgThread; import com.android.server.SystemService; import com.android.server.am.UserState.KeyEvictedCallback; import com.android.server.pm.UserManagerInternal; +import com.android.server.pm.UserManagerInternal.UserAssignmentResult; import com.android.server.pm.UserManagerService; import com.android.server.wm.WindowManagerService; @@ -162,10 +165,15 @@ public class UserControllerTest { USER_VISIBILITY_CHANGED_MSG, USER_CURRENT_MSG); - private static final Set<Integer> START_BACKGROUND_USER_MESSAGE_CODES = newHashSet( + private static final Set<Integer> START_INVISIBLE_BACKGROUND_USER_MESSAGE_CODES = newHashSet( USER_START_MSG, REPORT_LOCKED_BOOT_COMPLETE_MSG); + private static final Set<Integer> START_VISIBLE_BACKGROUND_USER_MESSAGE_CODES = newHashSet( + USER_START_MSG, + USER_VISIBILITY_CHANGED_MSG, + REPORT_LOCKED_BOOT_COMPLETE_MSG); + @Before public void setUp() throws Exception { runWithDexmakerShareClassLoader(() -> { @@ -184,6 +192,12 @@ public class UserControllerTest { mockIsUsersOnSecondaryDisplaysEnabled(false); // All UserController params are set to default. + // Starts with a generic assumption that the user starts visible, but on tests where + // that's not the case, the test should call mockAssignUserToMainDisplay() + doReturn(UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE) + .when(mInjector.mUserManagerInternalMock) + .assignUserToDisplayOnStart(anyInt(), anyInt(), anyBoolean(), anyInt()); + mUserController = new UserController(mInjector); mUserController.setAllowUserUnlocking(true); setUpUser(TEST_USER_ID, NO_USERINFO_FLAGS); @@ -211,16 +225,29 @@ public class UserControllerTest { @Test public void testStartUser_background() { + mockAssignUserToMainDisplay(TEST_USER_ID, /* foreground= */ false, + USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE); boolean started = mUserController.startUser(TEST_USER_ID, /* foreground= */ false); assertWithMessage("startUser(%s, foreground=false)", TEST_USER_ID).that(started).isTrue(); verify(mInjector.getWindowManager(), never()).startFreezingScreen(anyInt(), anyInt()); verify(mInjector.getWindowManager(), never()).setSwitchingUser(anyBoolean()); verify(mInjector, never()).clearAllLockedTasks(anyString()); - startBackgroundUserAssertions(); + startBackgroundUserAssertions(/*visible= */ false); verifyUserAssignedToDisplay(TEST_USER_ID, Display.DEFAULT_DISPLAY); } @Test + public void testStartUser_displayAssignmentFailed() { + doReturn(UserManagerInternal.USER_ASSIGNMENT_RESULT_FAILURE) + .when(mInjector.mUserManagerInternalMock) + .assignUserToDisplayOnStart(eq(TEST_USER_ID), anyInt(), eq(true), anyInt()); + + boolean started = mUserController.startUser(TEST_USER_ID, /* foreground= */ true); + + assertWithMessage("startUser(%s, foreground=true)", TEST_USER_ID).that(started).isFalse(); + } + + @Test public void testStartUserOnSecondaryDisplay_defaultDisplay() { assertThrows(IllegalArgumentException.class, () -> mUserController .startUserOnSecondaryDisplay(TEST_USER_ID, Display.DEFAULT_DISPLAY)); @@ -240,7 +267,7 @@ public class UserControllerTest { verify(mInjector.getWindowManager(), never()).startFreezingScreen(anyInt(), anyInt()); verify(mInjector.getWindowManager(), never()).setSwitchingUser(anyBoolean()); verify(mInjector, never()).clearAllLockedTasks(anyString()); - startBackgroundUserAssertions(); + startBackgroundUserAssertions(/*visible= */ true); } @Test @@ -266,6 +293,8 @@ public class UserControllerTest { @Test public void testStartPreCreatedUser_background() throws Exception { + mockAssignUserToMainDisplay(TEST_PRE_CREATED_USER_ID, /* foreground= */ false, + USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE); assertTrue(mUserController.startUser(TEST_PRE_CREATED_USER_ID, /* foreground= */ false)); // Make sure no intents have been fired for pre-created users. assertTrue(mInjector.mSentIntents.isEmpty()); @@ -284,8 +313,6 @@ public class UserControllerTest { // binder calls, but their side effects (in this case, that the user is stopped right away) assertWithMessage("wrong binder message calls").that(mInjector.mHandler.getMessageCodes()) .containsExactly(USER_START_MSG); - - verifyUserNeverAssignedToDisplay(); } private void startUserAssertions( @@ -295,8 +322,10 @@ public class UserControllerTest { assertEquals("Unexpected message sent", expectedMessageCodes, actualCodes); } - private void startBackgroundUserAssertions() { - startUserAssertions(START_BACKGROUND_USER_ACTIONS, START_BACKGROUND_USER_MESSAGE_CODES); + private void startBackgroundUserAssertions(boolean visible) { + startUserAssertions(START_BACKGROUND_USER_ACTIONS, + visible ? START_VISIBLE_BACKGROUND_USER_MESSAGE_CODES + : START_INVISIBLE_BACKGROUND_USER_MESSAGE_CODES); } private void startForegroundUserAssertions() { @@ -680,19 +709,24 @@ public class UserControllerTest { @Test public void testStartProfile() throws Exception { + mockAssignUserToMainDisplay(TEST_PRE_CREATED_USER_ID, /* foreground= */ false, + USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE); setUpAndStartProfileInBackground(TEST_USER_ID1); - startBackgroundUserAssertions(); + startBackgroundUserAssertions(/*visible= */ true); verifyUserAssignedToDisplay(TEST_USER_ID1, Display.DEFAULT_DISPLAY); } @Test public void testStartProfile_whenUsersOnSecondaryDisplaysIsEnabled() throws Exception { + mockAssignUserToMainDisplay(TEST_USER_ID1, /* foreground= */ false, + USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE); + mockIsUsersOnSecondaryDisplaysEnabled(true); setUpAndStartProfileInBackground(TEST_USER_ID1); - startBackgroundUserAssertions(); + startBackgroundUserAssertions(/*visible= */ true); verifyUserAssignedToDisplay(TEST_USER_ID1, Display.DEFAULT_DISPLAY); } @@ -949,22 +983,29 @@ public class UserControllerTest { when(mInjector.isUsersOnSecondaryDisplaysEnabled()).thenReturn(value); } + private void mockAssignUserToMainDisplay(@UserIdInt int userId, boolean foreground, + @UserAssignmentResult int result) { + when(mInjector.mUserManagerInternalMock.assignUserToDisplayOnStart(eq(userId), + /* profileGroupId= */ anyInt(), eq(foreground), eq(Display.DEFAULT_DISPLAY))) + .thenReturn(result); + } + private void verifyUserAssignedToDisplay(@UserIdInt int userId, int displayId) { - verify(mInjector.getUserManagerInternal()).assignUserToDisplay(eq(userId), anyInt(), + verify(mInjector.getUserManagerInternal()).assignUserToDisplayOnStart(eq(userId), anyInt(), anyBoolean(), eq(displayId)); } private void verifyUserNeverAssignedToDisplay() { - verify(mInjector.getUserManagerInternal(), never()).assignUserToDisplay(anyInt(), anyInt(), - anyBoolean(), anyInt()); + verify(mInjector.getUserManagerInternal(), never()).assignUserToDisplayOnStart(anyInt(), + anyInt(), anyBoolean(), anyInt()); } private void verifyUserUnassignedFromDisplay(@UserIdInt int userId) { - verify(mInjector.getUserManagerInternal()).unassignUserFromDisplay(userId); + verify(mInjector.getUserManagerInternal()).unassignUserFromDisplayOnStop(userId); } private void verifyUserUnassignedFromDisplayNeverCalled(@UserIdInt int userId) { - verify(mInjector.getUserManagerInternal(), never()).unassignUserFromDisplay(userId); + verify(mInjector.getUserManagerInternal(), never()).unassignUserFromDisplayOnStop(userId); } private void verifySystemUserVisibilityChangedNotified(boolean visible) { 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 4c939f077940..0262f564911b 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 @@ public class VirtualAudioControllerTest { /* 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 ddb3049b5cd1..8e669f0fd40d 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.KeyChain; 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 @@ public class DevicePolicyManagerTest extends DpmTestBase { } @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 @@ public class DevicePolicyManagerTest extends DpmTestBase { 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 @@ public class DevicePolicyManagerTest extends DpmTestBase { 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 @@ public class DevicePolicyManagerTest extends DpmTestBase { // 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 @@ public class DevicePolicyManagerTest extends DpmTestBase { } @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 @@ public class DevicePolicyManagerTest extends DpmTestBase { .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 fa5b6b2025c8..657bda633ab5 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 android.view.Display.DEFAULT_DISPLAY_GROUP; 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 android.view.DisplayInfo; 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 @@ public class LogicalDisplayMapperTest { @Mock Resources mResourcesMock; @Mock IPowerManager mIPowerManagerMock; @Mock IThermalService mIThermalServiceMock; - @Mock DeviceStateToLayoutMap mDeviceStateToLayoutMapMock; @Captor ArgumentCaptor<LogicalDisplay> mDisplayCaptor; @@ -133,13 +128,11 @@ public class LogicalDisplayMapperTest { 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 @@ public class LogicalDisplayMapperTest { /* 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 9c8e72c3e835..f5029ecae079 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 @@ public class BackgroundRestrictionsTest { 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 808130aa9568..164161e34b6f 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 @@ public class JobStoreTest { } @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 @@ public class JobStoreTest { 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/media/MediaButtonReceiverHolderTest.java b/services/tests/servicestests/src/com/android/server/media/MediaButtonReceiverHolderTest.java new file mode 100644 index 000000000000..1c4ee691fc77 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/media/MediaButtonReceiverHolderTest.java @@ -0,0 +1,45 @@ +/* + * 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.media; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.google.common.truth.Truth; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class MediaButtonReceiverHolderTest { + + @Test + public void createMediaButtonReceiverHolder_resolvesNullComponentName() { + Context context = InstrumentationRegistry.getInstrumentation().getContext(); + Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON); + PendingIntent pi = PendingIntent.getBroadcast(context, /* requestCode= */ 0, intent, + PendingIntent.FLAG_IMMUTABLE); + MediaButtonReceiverHolder a = MediaButtonReceiverHolder.create(/* userId= */ 0, pi, + context.getPackageName()); + Truth.assertWithMessage("Component name must match PendingIntent creator package.").that( + a.getComponentName()).isNull(); + } +} diff --git a/services/tests/servicestests/src/com/android/server/media/OWNERS b/services/tests/servicestests/src/com/android/server/media/OWNERS new file mode 100644 index 000000000000..55ffde223374 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/media/OWNERS @@ -0,0 +1,2 @@ +# Bug component: 137631 +include platform/frameworks/av:/media/janitors/media_solutions_OWNERS
\ No newline at end of file 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 5ece871ce1b0..1f952c4014d0 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 @@ public class UserManagerServiceUserPropertiesTest { .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 @@ public class UserManagerServiceUserPropertiesTest { // 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 @@ public class UserManagerServiceUserPropertiesTest { 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 5f480044d44b..d7c1e3760ab2 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 @@ public class UserManagerServiceUserTypeTest { /* 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 @@ public class UserManagerServiceUserTypeTest { } assertEquals(17, type.getDefaultUserPropertiesReference().getShowInLauncher()); + assertTrue(type.getDefaultUserPropertiesReference().getUseParentsContacts()); assertEquals(23, type.getBadgeLabel(0)); assertEquals(24, type.getBadgeLabel(1)); @@ -182,6 +184,7 @@ public class UserManagerServiceUserTypeTest { 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 @@ public class UserManagerServiceUserTypeTest { 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 @@ public class UserManagerServiceUserTypeTest { 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 @@ public class UserManagerServiceUserTypeTest { 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); @@ -347,6 +355,7 @@ public class UserManagerServiceUserTypeTest { UserTypeDetails details = builders.get(userTypeFull).createUserTypeDetails(); assertEquals(UNLIMITED_NUMBER_OF_USERS, details.getMaxAllowedPerParent()); assertFalse(details.isEnabled()); + assertEquals(17, details.getMaxAllowed()); assertTrue(UserRestrictionsUtils.areEqual( makeRestrictionsBundle("no_remove_user", "no_bluetooth"), details.getDefaultRestrictions())); 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 a3c45b77e6b2..2e7e583ad837 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 @@ package com.android.server.pm; 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 @@ public final class UserManagerTest { @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 @@ public final class UserManagerTest { .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 @@ public final class UserManagerTest { // 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 397770bec822..dcbdcdc98017 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.content.Context; 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 @@ public class HintManagerServiceTest { } @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/FakeMediaApp/Android.bp b/services/tests/servicestests/test-apps/FakeMediaApp/Android.bp new file mode 100644 index 000000000000..a4041b79e0ff --- /dev/null +++ b/services/tests/servicestests/test-apps/FakeMediaApp/Android.bp @@ -0,0 +1,37 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +android_test_helper_app { + name: "FakeMediaApp", + + sdk_version: "current", + + srcs: ["**/*.java"], + + dex_preopt: { + enabled: false, + }, + optimize: { + enabled: false, + }, +} diff --git a/services/tests/servicestests/test-apps/FakeMediaApp/AndroidManifest.xml b/services/tests/servicestests/test-apps/FakeMediaApp/AndroidManifest.xml new file mode 100644 index 000000000000..c08ee7a43976 --- /dev/null +++ b/services/tests/servicestests/test-apps/FakeMediaApp/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" + package="com.android.servicestests.apps.fakemediaapp"> + + <application> + <receiver + android:name=".FakeMediaButtonBroadcastReceiver" + android:enabled="true" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MEDIA_BUTTON" /> + </intent-filter> + </receiver> + </application> + +</manifest> diff --git a/services/tests/servicestests/test-apps/FakeMediaApp/OWNERS b/services/tests/servicestests/test-apps/FakeMediaApp/OWNERS new file mode 100644 index 000000000000..55ffde223374 --- /dev/null +++ b/services/tests/servicestests/test-apps/FakeMediaApp/OWNERS @@ -0,0 +1,2 @@ +# Bug component: 137631 +include platform/frameworks/av:/media/janitors/media_solutions_OWNERS
\ No newline at end of file diff --git a/services/tests/servicestests/test-apps/FakeMediaApp/src/FakeMediaButtonBroadcastReceiver.java b/services/tests/servicestests/test-apps/FakeMediaApp/src/FakeMediaButtonBroadcastReceiver.java new file mode 100644 index 000000000000..41f0cf52301d --- /dev/null +++ b/services/tests/servicestests/test-apps/FakeMediaApp/src/FakeMediaButtonBroadcastReceiver.java @@ -0,0 +1,32 @@ +/* + * 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 com.android.servicestests.apps.fakemediaapp; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +public class FakeMediaButtonBroadcastReceiver extends BroadcastReceiver { + + private static final String TAG = "FakeMediaButtonBroadcastReceiver"; + + @Override + public void onReceive(Context context, Intent intent) { + Log.v(TAG, "onReceive not expected"); + } +} 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 3e79407bb0ef..b8585f26c185 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 class TestJobService extends JobService { 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 class TestJobService extends JobService { 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/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 668345d51c1b..d54d1fed1016 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -7605,6 +7605,65 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + public void testAddAutomaticZenRule_systemCallTakesPackageFromOwner() throws Exception { + mService.isSystemUid = true; + ZenModeHelper mockZenModeHelper = mock(ZenModeHelper.class); + when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt())) + .thenReturn(true); + mService.setZenHelper(mockZenModeHelper); + ComponentName owner = new ComponentName("android", "ProviderName"); + ZenPolicy zenPolicy = new ZenPolicy.Builder().allowAlarms(true).build(); + boolean isEnabled = true; + AutomaticZenRule rule = new AutomaticZenRule("test", owner, owner, mock(Uri.class), + zenPolicy, NotificationManager.INTERRUPTION_FILTER_PRIORITY, isEnabled); + mBinderService.addAutomaticZenRule(rule, "com.android.settings"); + + // verify that zen mode helper gets passed in a package name of "android" + verify(mockZenModeHelper).addAutomaticZenRule(eq("android"), eq(rule), anyString()); + } + + @Test + public void testAddAutomaticZenRule_systemAppIdCallTakesPackageFromOwner() throws Exception { + // The multi-user case: where the calling uid doesn't match the system uid, but the calling + // *appid* is the system. + mService.isSystemUid = false; + mService.isSystemAppId = true; + ZenModeHelper mockZenModeHelper = mock(ZenModeHelper.class); + when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt())) + .thenReturn(true); + mService.setZenHelper(mockZenModeHelper); + ComponentName owner = new ComponentName("android", "ProviderName"); + ZenPolicy zenPolicy = new ZenPolicy.Builder().allowAlarms(true).build(); + boolean isEnabled = true; + AutomaticZenRule rule = new AutomaticZenRule("test", owner, owner, mock(Uri.class), + zenPolicy, NotificationManager.INTERRUPTION_FILTER_PRIORITY, isEnabled); + mBinderService.addAutomaticZenRule(rule, "com.android.settings"); + + // verify that zen mode helper gets passed in a package name of "android" + verify(mockZenModeHelper).addAutomaticZenRule(eq("android"), eq(rule), anyString()); + } + + @Test + public void testAddAutomaticZenRule_nonSystemCallTakesPackageFromArg() throws Exception { + mService.isSystemUid = false; + mService.isSystemAppId = false; + ZenModeHelper mockZenModeHelper = mock(ZenModeHelper.class); + when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt())) + .thenReturn(true); + mService.setZenHelper(mockZenModeHelper); + ComponentName owner = new ComponentName("android", "ProviderName"); + ZenPolicy zenPolicy = new ZenPolicy.Builder().allowAlarms(true).build(); + boolean isEnabled = true; + AutomaticZenRule rule = new AutomaticZenRule("test", owner, owner, mock(Uri.class), + zenPolicy, NotificationManager.INTERRUPTION_FILTER_PRIORITY, isEnabled); + mBinderService.addAutomaticZenRule(rule, "another.package"); + + // verify that zen mode helper gets passed in the package name from the arg, not the owner + verify(mockZenModeHelper).addAutomaticZenRule( + eq("another.package"), eq(rule), anyString()); + } + + @Test public void testAreNotificationsEnabledForPackage() throws Exception { mBinderService.areNotificationsEnabledForPackage(mContext.getPackageName(), mUid); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java b/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java index 8cf74fbf88b7..61a6985d473e 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java @@ -32,6 +32,7 @@ import java.util.Set; public class TestableNotificationManagerService extends NotificationManagerService { int countSystemChecks = 0; boolean isSystemUid = true; + boolean isSystemAppId = true; int countLogSmartSuggestionsVisible = 0; Set<Integer> mChannelToastsSent = new HashSet<>(); @@ -58,6 +59,12 @@ public class TestableNotificationManagerService extends NotificationManagerServi } @Override + protected boolean isCallingAppIdSystem() { + countSystemChecks++; + return isSystemUid || isSystemAppId; + } + + @Override protected boolean isCallerSystemOrPhone() { countSystemChecks++; return isSystemUid; diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java index ba61980928bc..49edde585430 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -1672,6 +1672,36 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test + public void testAddAutomaticZenRule_claimedSystemOwner() { + // Make sure anything that claims to have a "system" owner but not actually part of the + // system package still gets limited on number of rules + for (int i = 0; i < RULE_LIMIT_PER_PACKAGE; i++) { + ScheduleInfo si = new ScheduleInfo(); + si.startHour = i; + AutomaticZenRule zenRule = new AutomaticZenRule("name" + i, + new ComponentName("android", "ScheduleConditionProvider" + i), + null, // configuration activity + ZenModeConfig.toScheduleConditionId(si), + new ZenPolicy.Builder().build(), + NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); + String id = mZenModeHelperSpy.addAutomaticZenRule("pkgname", zenRule, "test"); + assertNotNull(id); + } + try { + AutomaticZenRule zenRule = new AutomaticZenRule("name", + new ComponentName("android", "ScheduleConditionProviderFinal"), + null, // configuration activity + ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), + new ZenPolicy.Builder().build(), + NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); + String id = mZenModeHelperSpy.addAutomaticZenRule("pkgname", zenRule, "test"); + fail("allowed too many rules to be created"); + } catch (IllegalArgumentException e) { + // yay + } + } + + @Test public void testAddAutomaticZenRule_CA() { AutomaticZenRule zenRule = new AutomaticZenRule("name", null, diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java index 376399a3d363..85c49753f240 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java @@ -324,7 +324,7 @@ public class ActivityMetricsLaunchObserverTests extends WindowTestsBase { // The activity reports fully drawn before windows drawn, then the fully drawn event will // be pending (see {@link WindowingModeTransitionInfo#pendingFullyDrawn}). - mActivityMetricsLogger.logAppTransitionReportedDrawn(mTopActivity, false); + mActivityMetricsLogger.notifyFullyDrawn(mTopActivity, false /* restoredFromBundle */); notifyTransitionStarting(mTopActivity); // The pending fully drawn event should send when the actual windows drawn event occurs. final ActivityMetricsLogger.TransitionInfoSnapshot info = notifyWindowsDrawn(mTopActivity); @@ -337,7 +337,7 @@ public class ActivityMetricsLaunchObserverTests extends WindowTestsBase { verifyNoMoreInteractions(mLaunchObserver); final ActivityMetricsLogger.TransitionInfoSnapshot fullyDrawnInfo = mActivityMetricsLogger - .logAppTransitionReportedDrawn(mTopActivity, false /* restoredFromBundle */); + .notifyFullyDrawn(mTopActivity, false /* restoredFromBundle */); assertWithMessage("Invisible event must be dropped").that(fullyDrawnInfo).isNull(); } 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 3a8e1ccce122..bd8da4e713b7 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 @@ public class ActivityRecordTests extends WindowTestsBase { 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/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java index 07dba003f504..fc1989e84ac5 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java @@ -1262,6 +1262,26 @@ public class ActivityStarterTests extends WindowTestsBase { } @Test + public void testRecycleTaskWakeUpWhenDreaming() { + doNothing().when(mWm.mAtmService.mTaskSupervisor).wakeUp(anyString()); + doReturn(true).when(mWm.mAtmService).isDreaming(); + final ActivityStarter starter = prepareStarter(0 /* flags */); + final ActivityRecord target = new ActivityBuilder(mAtm).setCreateTask(true).build(); + starter.mStartActivity = target; + target.mVisibleRequested = false; + target.setTurnScreenOn(true); + // Assume the flag was consumed by relayout. + target.setCurrentLaunchCanTurnScreenOn(false); + startActivityInner(starter, target, null /* source */, null /* options */, + null /* inTask */, null /* inTaskFragment */); + // The flag should be set again when resuming (from recycleTask) the target as top. + assertTrue(target.currentLaunchCanTurnScreenOn()); + // In real case, dream activity has a higher priority (TaskDisplayArea#getPriority) that + // will be put at a higher z-order. So it relies on wakeUp() to be dismissed. + verify(mWm.mAtmService.mTaskSupervisor).wakeUp(anyString()); + } + + @Test public void testTargetTaskInSplitScreen() { final ActivityStarter starter = prepareStarter(FLAG_ACTIVITY_LAUNCH_ADJACENT, false /* mockGetRootTask */); 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 52af8adcf9f6..d99946f0a5c4 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.assertNotEquals; 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 @@ public class DisplayPolicyTests extends WindowTestsBase { 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 21197baaf8cc..db1d15a4584a 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 class DisplayWindowPolicyControllerTests extends WindowTestsBase { 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 ac3d0f0d3f28..75c5b6e13777 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 @@ public class DualDisplayAreaGroupPolicyTest extends WindowTestsBase { 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 eb8b89d59310..a26cad98f4d2 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 @@ public class ImeInsetsSourceProviderTest extends WindowTestsBase { 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 @@ public class ImeInsetsSourceProviderTest extends WindowTestsBase { 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 @@ public class ImeInsetsSourceProviderTest extends WindowTestsBase { 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 e5842b400ba3..e65610f0959b 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; 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 @@ public class SizeCompatTests extends WindowTestsBase { /** 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/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java index 83f17897eb62..3ff2c0e0d024 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java @@ -118,10 +118,13 @@ public class TaskFragmentTest extends WindowTestsBase { doReturn(true).when(mTaskFragment).isVisibleRequested(); clearInvocations(mTransaction); + mTaskFragment.deferOrganizedTaskFragmentSurfaceUpdate(); mTaskFragment.setBounds(endBounds); + assertTrue(mTaskFragment.shouldStartChangeTransition(startBounds)); + mTaskFragment.initializeChangeTransition(startBounds); + mTaskFragment.continueOrganizedTaskFragmentSurfaceUpdate(); // Surface reset when prepare transition. - verify(mTaskFragment).initializeChangeTransition(startBounds); verify(mTransaction).setPosition(mLeash, 0, 0); verify(mTransaction).setWindowCrop(mLeash, 0, 0); @@ -166,7 +169,7 @@ public class TaskFragmentTest extends WindowTestsBase { mTaskFragment.setBounds(endBounds); - verify(mTaskFragment, never()).initializeChangeTransition(any()); + assertFalse(mTaskFragment.shouldStartChangeTransition(startBounds)); } /** 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 bb5acebfacd7..6e72bf360295 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.IWindow; 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 @@ public class TestIWindow extends IWindow.Stub { } @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 0139f6a5695a..1b888f629251 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java @@ -95,6 +95,8 @@ import android.view.InsetsSource; import android.view.InsetsState; import android.view.SurfaceControl; import android.view.WindowManager; +import android.window.ITaskFragmentOrganizer; +import android.window.TaskFragmentOrganizer; import androidx.test.filters.SmallTest; @@ -800,6 +802,39 @@ public class WindowStateTests extends WindowTestsBase { } @Test + public void testEmbeddedActivityResizing_clearAllDrawn() { + final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); + mAtm.mTaskFragmentOrganizerController.registerOrganizer( + ITaskFragmentOrganizer.Stub.asInterface(organizer.getOrganizerToken().asBinder())); + final Task task = createTask(mDisplayContent); + final TaskFragment embeddedTf = createTaskFragmentWithEmbeddedActivity(task, organizer); + final ActivityRecord embeddedActivity = embeddedTf.getTopMostActivity(); + final WindowState win = createWindow(null /* parent */, TYPE_APPLICATION, embeddedActivity, + "App window"); + doReturn(true).when(embeddedActivity).isVisible(); + embeddedActivity.mVisibleRequested = true; + makeWindowVisible(win); + win.mLayoutSeq = win.getDisplayContent().mLayoutSeq; + // Set the bounds twice: + // 1. To make sure there is no orientation change after #reportResized, which can also cause + // #clearAllDrawn. + // 2. Make #isLastConfigReportedToClient to be false after #reportResized, so it can process + // to check if we need redraw. + embeddedTf.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); + embeddedTf.setBounds(0, 0, 1000, 2000); + win.reportResized(); + embeddedTf.setBounds(500, 0, 1000, 2000); + + // Clear all drawn when the embedded TaskFragment is in mDisplayContent.mChangingContainers. + win.updateResizingWindowIfNeeded(); + verify(embeddedActivity, never()).clearAllDrawn(); + + mDisplayContent.mChangingContainers.add(embeddedTf); + win.updateResizingWindowIfNeeded(); + verify(embeddedActivity).clearAllDrawn(); + } + + @Test public void testCantReceiveTouchWhenAppTokenHiddenRequested() { final WindowState win0 = createWindow(null, TYPE_APPLICATION, "win0"); win0.mActivityRecord.mVisibleRequested = false; @@ -1000,7 +1035,7 @@ public class WindowStateTests extends WindowTestsBase { 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 +1072,7 @@ public class WindowStateTests extends WindowTestsBase { 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 eca7cbbc5486..ab042d15963b 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.SurfaceControl.Transaction; 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 @@ class WindowTestsBase extends SystemServiceTestsBase { } @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/services/usb/java/com/android/server/usb/UsbService.java b/services/usb/java/com/android/server/usb/UsbService.java index 86f877fcd531..72f6cc3649a7 100644 --- a/services/usb/java/com/android/server/usb/UsbService.java +++ b/services/usb/java/com/android/server/usb/UsbService.java @@ -504,6 +504,15 @@ public class UsbService extends IUsbManager.Stub { } @Override + public boolean hasDevicePermissionWithIdentity(UsbDevice device, String packageName, + int pid, int uid) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_USB, null); + + final int userId = UserHandle.getUserId(uid); + return getPermissionsForUser(userId).hasPermission(device, packageName, pid, uid); + } + + @Override public boolean hasAccessoryPermission(UsbAccessory accessory) { final int uid = Binder.getCallingUid(); final int pid = Binder.getCallingPid(); @@ -518,6 +527,14 @@ public class UsbService extends IUsbManager.Stub { } @Override + public boolean hasAccessoryPermissionWithIdentity(UsbAccessory accessory, int pid, int uid) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_USB, null); + + final int userId = UserHandle.getUserId(uid); + return getPermissionsForUser(userId).hasPermission(accessory, pid, uid); + } + + @Override public void requestDevicePermission(UsbDevice device, String packageName, PendingIntent pi) { final int uid = Binder.getCallingUid(); final int pid = Binder.getCallingPid(); diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordAudioStreamManager.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordAudioStreamManager.java index f9211181c924..d5eea1f3ff35 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordAudioStreamManager.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordAudioStreamManager.java @@ -163,7 +163,7 @@ final class HotwordAudioStreamManager { private static class SingleAudioStreamCopyTask implements Callable<Void> { // TODO: Make this buffer size customizable from updateState() - private static final int COPY_BUFFER_LENGTH = 1_024; + private static final int COPY_BUFFER_LENGTH = 2_560; private final String mStreamTaskId; private final ParcelFileDescriptor mAudioSource; diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java index 1fc54d6bbcae..0721c281f15c 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java @@ -19,7 +19,6 @@ package com.android.server.voiceinteraction; import static android.Manifest.permission.CAPTURE_AUDIO_HOTWORD; import static android.Manifest.permission.RECORD_AUDIO; import static android.service.attention.AttentionService.PROXIMITY_UNKNOWN; -import static android.service.voice.HotwordDetectedResult.EXTRA_PROXIMITY_METERS; import static android.service.voice.HotwordDetectionService.AUDIO_SOURCE_EXTERNAL; import static android.service.voice.HotwordDetectionService.AUDIO_SOURCE_MICROPHONE; import static android.service.voice.HotwordDetectionService.ENABLE_PROXIMITY_RESULT; @@ -206,7 +205,7 @@ final class HotwordDetectionConnection { @Nullable AttentionManagerInternal mAttentionManagerInternal = null; final AttentionManagerInternal.ProximityUpdateCallbackInternal mProximityCallbackInternal = - this::setProximityMeters; + this::setProximityValue; volatile HotwordDetectionServiceIdentity mIdentity; @@ -504,7 +503,7 @@ final class HotwordDetectionConnection { mSoftwareCallback.onError(); return; } - saveProximityMetersToBundle(result); + saveProximityValueToBundle(result); HotwordDetectedResult newResult; try { newResult = mHotwordAudioStreamManager.startCopyingAudioStreams(result); @@ -639,7 +638,7 @@ final class HotwordDetectionConnection { externalCallback.onError(CALLBACK_ONDETECTED_GOT_SECURITY_EXCEPTION); return; } - saveProximityMetersToBundle(result); + saveProximityValueToBundle(result); HotwordDetectedResult newResult; try { newResult = mHotwordAudioStreamManager.startCopyingAudioStreams(result); @@ -1242,15 +1241,15 @@ final class HotwordDetectionConnection { }); } - private void saveProximityMetersToBundle(HotwordDetectedResult result) { + private void saveProximityValueToBundle(HotwordDetectedResult result) { synchronized (mLock) { if (result != null && mProximityMeters != PROXIMITY_UNKNOWN) { - result.getExtras().putDouble(EXTRA_PROXIMITY_METERS, mProximityMeters); + result.setProximity(mProximityMeters); } } } - private void setProximityMeters(double proximityMeters) { + private void setProximityValue(double proximityMeters) { synchronized (mLock) { mProximityMeters = proximityMeters; } diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java index 5d1901d96bcd..0a660b0dd44c 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java @@ -639,7 +639,7 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne private void logDetectorCreateEventIfNeeded(IHotwordRecognitionStatusCallback callback, int detectorType, boolean isCreated, int voiceInteractionServiceUid) { if (callback != null) { - HotwordMetricsLogger.writeDetectorCreateEvent(detectorType, true, + HotwordMetricsLogger.writeDetectorCreateEvent(detectorType, isCreated, voiceInteractionServiceUid); } diff --git a/telecomm/OWNERS b/telecomm/OWNERS index eb0c4327ec46..dcaf858a0a0b 100644 --- a/telecomm/OWNERS +++ b/telecomm/OWNERS @@ -4,3 +4,7 @@ breadley@google.com tgunn@google.com xiaotonj@google.com rgreenwalt@google.com +chinmayd@google.com +grantmenke@google.com +pmadapurmath@google.com +tjstuart@google.com
\ No newline at end of file diff --git a/telecomm/java/android/telecom/Call.java b/telecomm/java/android/telecom/Call.java index 432af3aa1aa0..5cef2cb0a161 100644 --- a/telecomm/java/android/telecom/Call.java +++ b/telecomm/java/android/telecom/Call.java @@ -168,6 +168,18 @@ public final class Call { public static final String AVAILABLE_PHONE_ACCOUNTS = "selectPhoneAccountAccounts"; /** + * Extra key intended for {@link InCallService}s that notify the user of an incoming call. When + * EXTRA_IS_SUPPRESSED_BY_DO_NOT_DISTURB returns true, the {@link InCallService} should not + * interrupt the user of the incoming call because the call is being suppressed by Do Not + * Disturb settings. + * + * This extra will be removed from the {@link Call} object for {@link InCallService}s that do + * not hold the {@link android.Manifest.permission#READ_CONTACTS} permission. + */ + public static final String EXTRA_IS_SUPPRESSED_BY_DO_NOT_DISTURB = + "android.telecom.extra.IS_SUPPRESSED_BY_DO_NOT_DISTURB"; + + /** * Key for extra used to pass along a list of {@link PhoneAccountSuggestion}s to the in-call * UI when a call enters the {@link #STATE_SELECT_PHONE_ACCOUNT} state. The list included here * will have the same length and be in the same order as the list passed with diff --git a/telecomm/java/android/telecom/ParcelableCallAnalytics.java b/telecomm/java/android/telecom/ParcelableCallAnalytics.java index ff87ab00ae8b..a69dfb0b255f 100644 --- a/telecomm/java/android/telecom/ParcelableCallAnalytics.java +++ b/telecomm/java/android/telecom/ParcelableCallAnalytics.java @@ -111,6 +111,8 @@ public class ParcelableCallAnalytics implements Parcelable { public static final int FILTERING_INITIATED = 106; public static final int FILTERING_COMPLETED = 107; public static final int FILTERING_TIMED_OUT = 108; + public static final int DND_CHECK_INITIATED = 109; + public static final int DND_CHECK_COMPLETED = 110; public static final int SKIP_RINGING = 200; public static final int SILENCE = 201; @@ -195,6 +197,7 @@ public class ParcelableCallAnalytics implements Parcelable { public static final int BLOCK_CHECK_FINISHED_TIMING = 9; public static final int FILTERING_COMPLETED_TIMING = 10; public static final int FILTERING_TIMED_OUT_TIMING = 11; + public static final int DND_PRE_CALL_PRE_CHECK_TIMING = 12; /** {@hide} */ public static final int START_CONNECTION_TO_REQUEST_DISCONNECT_TIMING = 12; diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index 936fad51f095..f38b902f7531 100644 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -8698,6 +8698,15 @@ public class CarrierConfigManager { public static final String KEY_VONR_ENABLED_BOOL = "vonr_enabled_bool"; /** + * Boolean indicating the default VoNR user preference setting. + * If true, the VoNR setting will be enabled. If false, it will be disabled initially. + * + * Enabled by default. + * + */ + public static final String KEY_VONR_ON_BY_DEFAULT_BOOL = "vonr_on_by_default_bool"; + + /** * Determine whether unthrottle data retry when tracking area code (TAC/LAC) from cell changes * * @hide @@ -9520,6 +9529,7 @@ public class CarrierConfigManager { sDefaults.putBoolean(KEY_UNTHROTTLE_DATA_RETRY_WHEN_TAC_CHANGES_BOOL, false); sDefaults.putBoolean(KEY_VONR_SETTING_VISIBILITY_BOOL, true); sDefaults.putBoolean(KEY_VONR_ENABLED_BOOL, false); + sDefaults.putBoolean(KEY_VONR_ON_BY_DEFAULT_BOOL, true); sDefaults.putIntArray(KEY_SUPPORTED_PREMIUM_CAPABILITIES_INT_ARRAY, new int[] {}); sDefaults.putLong(KEY_PREMIUM_CAPABILITY_NOTIFICATION_DISPLAY_TIMEOUT_MILLIS_LONG, TimeUnit.MINUTES.toMillis(30)); diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index e099e69e3edd..541573cad3ec 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -14957,21 +14957,132 @@ public class TelephonyManager { * @return a Pair of (major version, minor version) or (-1,-1) if unknown. * * @hide + * + * @deprecated Use {@link #getHalVersion} instead. */ + @Deprecated @UnsupportedAppUsage @TestApi public Pair<Integer, Integer> getRadioHalVersion() { + return getHalVersion(HAL_SERVICE_RADIO); + } + + /** @hide */ + public static final int HAL_SERVICE_RADIO = 0; + + /** + * HAL service type that supports the HAL APIs implementation of IRadioData + * {@link RadioDataProxy} + * @hide + */ + @TestApi + public static final int HAL_SERVICE_DATA = 1; + + /** + * HAL service type that supports the HAL APIs implementation of IRadioMessaging + * {@link RadioMessagingProxy} + * @hide + */ + @TestApi + public static final int HAL_SERVICE_MESSAGING = 2; + + /** + * HAL service type that supports the HAL APIs implementation of IRadioModem + * {@link RadioModemProxy} + * @hide + */ + @TestApi + public static final int HAL_SERVICE_MODEM = 3; + + /** + * HAL service type that supports the HAL APIs implementation of IRadioNetwork + * {@link RadioNetworkProxy} + * @hide + */ + @TestApi + public static final int HAL_SERVICE_NETWORK = 4; + + /** + * HAL service type that supports the HAL APIs implementation of IRadioSim + * {@link RadioSimProxy} + * @hide + */ + @TestApi + public static final int HAL_SERVICE_SIM = 5; + + /** + * HAL service type that supports the HAL APIs implementation of IRadioVoice + * {@link RadioVoiceProxy} + * @hide + */ + @TestApi + public static final int HAL_SERVICE_VOICE = 6; + + /** + * HAL service type that supports the HAL APIs implementation of IRadioIms + * {@link RadioImsProxy} + * @hide + */ + @TestApi + public static final int HAL_SERVICE_IMS = 7; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = {"HAL_SERVICE_"}, + value = { + HAL_SERVICE_RADIO, + HAL_SERVICE_DATA, + HAL_SERVICE_MESSAGING, + HAL_SERVICE_MODEM, + HAL_SERVICE_NETWORK, + HAL_SERVICE_SIM, + HAL_SERVICE_VOICE, + HAL_SERVICE_IMS, + }) + public @interface HalService {} + + /** + * The HAL Version indicating that the version is unknown or invalid. + * @hide + */ + @TestApi + public static final Pair HAL_VERSION_UNKNOWN = new Pair(-1, -1); + + /** + * The HAL Version indicating that the version is unsupported. + * @hide + */ + @TestApi + public static final Pair HAL_VERSION_UNSUPPORTED = new Pair(-2, -2); + + /** + * Retrieve the HAL Version of a specific service for this device. + * + * Get the HAL version for a specific HAL interface for test purposes. + * + * @param halService the service id to query. + * @return a Pair of (major version, minor version), HAL_VERSION_UNKNOWN if unknown + * or HAL_VERSION_UNSUPPORTED if unsupported. + * + * @hide + */ + @TestApi + public @NonNull Pair<Integer, Integer> getHalVersion(@HalService int halService) { try { ITelephony service = getITelephony(); if (service != null) { - int version = service.getRadioHalVersion(); - if (version == -1) return new Pair<Integer, Integer>(-1, -1); - return new Pair<Integer, Integer>(version / 100, version % 100); + int version = service.getHalVersion(halService); + if (version != -1) { + return new Pair<Integer, Integer>(version / 100, version % 100); + } + } else { + throw new IllegalStateException("telephony service is null."); } } catch (RemoteException e) { - Log.e(TAG, "getRadioHalVersion() RemoteException", e); + Log.e(TAG, "getHalVersion() RemoteException", e); + e.rethrowAsRuntimeException(); } - return new Pair<Integer, Integer>(-1, -1); + return HAL_VERSION_UNKNOWN; } /** @@ -17339,7 +17450,7 @@ public class TelephonyManager { * 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 +17480,7 @@ public class TelephonyManager { 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 +17520,8 @@ public class TelephonyManager { 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 ba2b62d14bec..8e2707735f63 100644 --- a/telephony/java/android/telephony/data/IQualifiedNetworksService.aidl +++ b/telephony/java/android/telephony/data/IQualifiedNetworksService.aidl @@ -27,4 +27,5 @@ interface IQualifiedNetworksService 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/QosBearerFilter.java b/telephony/java/android/telephony/data/QosBearerFilter.java index 0ab7b61bd73d..a0d9c1bdf1ed 100644 --- a/telephony/java/android/telephony/data/QosBearerFilter.java +++ b/telephony/java/android/telephony/data/QosBearerFilter.java @@ -130,6 +130,10 @@ public final class QosBearerFilter implements Parcelable { return precedence; } + public int getProtocol() { + return protocol; + } + public static class PortRange implements Parcelable { int start; int end; diff --git a/telephony/java/android/telephony/data/QualifiedNetworksService.java b/telephony/java/android/telephony/data/QualifiedNetworksService.java index fb973361e398..56f0f9f13772 100644 --- a/telephony/java/android/telephony/data/QualifiedNetworksService.java +++ b/telephony/java/android/telephony/data/QualifiedNetworksService.java @@ -68,6 +68,7 @@ public abstract class QualifiedNetworksService extends Service { 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 @@ public abstract class QualifiedNetworksService extends Service { } /** + * 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 @@ public abstract class QualifiedNetworksService extends Service { } 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 @@ public abstract class QualifiedNetworksService extends Service { 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/telephony/java/android/telephony/emergency/EmergencyNumber.java b/telephony/java/android/telephony/emergency/EmergencyNumber.java index d9d5c14735ea..e78a1e107afe 100644 --- a/telephony/java/android/telephony/emergency/EmergencyNumber.java +++ b/telephony/java/android/telephony/emergency/EmergencyNumber.java @@ -660,9 +660,6 @@ public final class EmergencyNumber implements Parcelable, Comparable<EmergencyNu if (!first.getEmergencyUrns().equals(second.getEmergencyUrns())) { return false; } - if (first.getEmergencyCallRouting() != second.getEmergencyCallRouting()) { - return false; - } // Never merge two numbers if one of them is from test mode but the other one is not; // This supports to remove a number from the test mode. if (first.isFromSources(EMERGENCY_NUMBER_SOURCE_TEST) @@ -685,12 +682,18 @@ public final class EmergencyNumber implements Parcelable, Comparable<EmergencyNu public static EmergencyNumber mergeSameEmergencyNumbers(@NonNull EmergencyNumber first, @NonNull EmergencyNumber second) { if (areSameEmergencyNumbers(first, second)) { + int routing = first.getEmergencyCallRouting(); + + if (second.isFromSources(EMERGENCY_NUMBER_SOURCE_DATABASE)) { + routing = second.getEmergencyCallRouting(); + } + return new EmergencyNumber(first.getNumber(), first.getCountryIso(), first.getMnc(), first.getEmergencyServiceCategoryBitmask(), first.getEmergencyUrns(), first.getEmergencyNumberSourceBitmask() | second.getEmergencyNumberSourceBitmask(), - first.getEmergencyCallRouting()); + routing); } return null; } diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl index 648866b6662d..abf4cde62323 100644 --- a/telephony/java/com/android/internal/telephony/ITelephony.aidl +++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl @@ -2159,6 +2159,12 @@ interface ITelephony { int getRadioHalVersion(); /** + * Get the HAL Version of a specific service + * encoded as 100 * MAJOR_VERSION + MINOR_VERSION or -1 if unknown + */ + int getHalVersion(int service); + + /** * Get the current calling package name. */ String getCurrentPackageName(); diff --git a/tests/FlickerTests/AndroidTest.xml b/tests/FlickerTests/AndroidTest.xml index d91aa1ebe8f5..a7d6a018c9c7 100644 --- a/tests/FlickerTests/AndroidTest.xml +++ b/tests/FlickerTests/AndroidTest.xml @@ -15,6 +15,8 @@ <option name="run-command" value="cmd window tracing frame" /> <!-- ensure lock screen mode is swipe --> <option name="run-command" value="locksettings set-disabled false" /> + <!-- disable betterbug as it's log collection dialogues cause flakes in e2e tests --> + <option name="run-command" value="pm disable com.google.android.internal.betterbug" /> <!-- restart launcher to activate TAPL --> <option name="run-command" value="setprop ro.test_harness 1 ; am force-stop com.google.android.apps.nexuslauncher" /> </target_preparer> diff --git a/tests/TouchLatency/Android.bp b/tests/TouchLatency/Android.bp index 3a9e240d9746..4ef1ead7d9c9 100644 --- a/tests/TouchLatency/Android.bp +++ b/tests/TouchLatency/Android.bp @@ -12,6 +12,7 @@ android_test { 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 f5ae6f4b4ffc..129baab5529d 100644 --- a/tests/TouchLatency/app/build.gradle +++ b/tests/TouchLatency/app/build.gradle @@ -6,7 +6,7 @@ android { defaultConfig { applicationId "com.prefabulated.touchlatency" - minSdkVersion 28 + minSdkVersion 30 targetSdkVersion 33 versionCode 1 versionName "1.0" @@ -17,4 +17,9 @@ android { 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 6ab3b3e6c037..2e93c878ceac 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.MenuItem; 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 @@ public class TouchLatencyActivity extends Activity { 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 @@ public class TouchLatencyActivity extends Activity { 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 @@ public class TouchLatencyActivity extends Activity { 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 @@ public class TouchLatencyActivity extends Activity { 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 000000000000..bb9ce609c56f --- /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 abc7fd5d6bb2..7169021b6653 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 5ee86d8bd8bf..cad2df78ffcd 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 22da7c1d050b..b23a87e57754 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 1d3591c8a4c9..ccd5dda1d6fa 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 3da8b460df13..133c1767c9b4 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 @@ public class BroadcastInterceptingContext extends ContextWrapper { @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 2a450ba45aeb..1d7fd1d17dcd 100644 --- a/tools/aapt2/Resources.proto +++ b/tools/aapt2/Resources.proto @@ -46,6 +46,13 @@ message ToolFingerprint { 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 @@ message ResourceTable { // 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 116dcd641bc1..a8d229956b73 100644 --- a/tools/aapt2/cmd/Link.cpp +++ b/tools/aapt2/cmd/Link.cpp @@ -1085,6 +1085,10 @@ class Linker { 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 254f3a546f99..28fcc1a4800e 100644 --- a/tools/aapt2/cmd/Link_test.cpp +++ b/tools/aapt2/cmd/Link_test.cpp @@ -840,6 +840,43 @@ TEST_F(LinkTest, LocaleConfigVerification) { 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 6a1e8c1bb24c..e39f327cee9f 100644 --- a/tools/aapt2/format/proto/ProtoDeserialize.cpp +++ b/tools/aapt2/format/proto/ProtoDeserialize.cpp @@ -562,6 +562,11 @@ bool DeserializeTableFromPb(const pb::ResourceTable& pb_table, io::IFileCollecti } } + 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 163a60a9e40e..a6d58fd38f09 100644 --- a/tools/aapt2/format/proto/ProtoSerialize.cpp +++ b/tools/aapt2/format/proto/ProtoSerialize.cpp @@ -345,7 +345,11 @@ void SerializeTableToPb(const ResourceTable& table, pb::ResourceTable* out_table 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 692fa4247ae9..5adc5e639830 100644 --- a/tools/aapt2/format/proto/ProtoSerialize_test.cpp +++ b/tools/aapt2/format/proto/ProtoSerialize_test.cpp @@ -1024,4 +1024,28 @@ TEST(ProtoSerializeTest, CustomResourceTypes) { 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 |