diff options
392 files changed, 11472 insertions, 2986 deletions
diff --git a/apct-tests/perftests/multiuser/AndroidManifest.xml b/apct-tests/perftests/multiuser/AndroidManifest.xml index 63e5983401d7..5befa1f1d4d4 100644 --- a/apct-tests/perftests/multiuser/AndroidManifest.xml +++ b/apct-tests/perftests/multiuser/AndroidManifest.xml @@ -35,4 +35,8 @@ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" android:targetPackage="com.android.perftests.multiuser"/> + <queries> + <package android:name="perftests.multiuser.apps.dummyapp" /> + </queries> + </manifest> 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 d9fb318c9335..d9fe30da2cb7 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -177,7 +177,7 @@ public class JobSchedulerService extends com.android.server.SystemService @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.TIRAMISU) private static final long REQUIRE_NETWORK_CONSTRAINT_FOR_NETWORK_JOB_WORK_ITEMS = 241104082L; - @VisibleForTesting + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) public static Clock sSystemClock = Clock.systemUTC(); private abstract static class MySimpleClock extends Clock { @@ -454,6 +454,10 @@ public class JobSchedulerService extends com.android.server.SystemService runtimeUpdated = true; } break; + case Constants.KEY_PERSIST_IN_SPLIT_FILES: + mConstants.updatePersistingConstantsLocked(); + mJobs.setUseSplitFiles(mConstants.PERSIST_IN_SPLIT_FILES); + break; default: if (name.startsWith(JobConcurrencyManager.CONFIG_KEY_PREFIX_CONCURRENCY) && !concurrencyUpdated) { @@ -537,6 +541,8 @@ public class JobSchedulerService extends com.android.server.SystemService private static final String KEY_RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS = "runtime_min_high_priority_guarantee_ms"; + private static final String KEY_PERSIST_IN_SPLIT_FILES = "persist_in_split_files"; + private static final int DEFAULT_MIN_READY_NON_ACTIVE_JOBS_COUNT = 5; private static final long DEFAULT_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS = 31 * MINUTE_IN_MILLIS; private static final float DEFAULT_HEAVY_USE_FACTOR = .9f; @@ -563,6 +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; private static final boolean DEFAULT_USE_TARE_POLICY = false; /** @@ -678,6 +685,12 @@ public class JobSchedulerService extends com.android.server.SystemService DEFAULT_RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS; /** + * Whether to persist jobs in split files (by UID). If false, all persisted jobs will be + * saved in a single file. + */ + public boolean PERSIST_IN_SPLIT_FILES = DEFAULT_PERSIST_IN_SPLIT_FILES; + + /** * If true, use TARE policy for job limiting. If false, use quotas. */ public boolean USE_TARE_POLICY = DEFAULT_USE_TARE_POLICY; @@ -735,6 +748,11 @@ public class JobSchedulerService extends com.android.server.SystemService DEFAULT_CONN_LOW_SIGNAL_STRENGTH_RELAX_FRAC); } + private void updatePersistingConstantsLocked() { + PERSIST_IN_SPLIT_FILES = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_JOB_SCHEDULER, + KEY_PERSIST_IN_SPLIT_FILES, DEFAULT_PERSIST_IN_SPLIT_FILES); + } + private void updatePrefetchConstantsLocked() { PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS = DeviceConfig.getLong( DeviceConfig.NAMESPACE_JOB_SCHEDULER, @@ -835,6 +853,8 @@ public class JobSchedulerService extends com.android.server.SystemService pw.print(KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, RUNTIME_FREE_QUOTA_MAX_LIMIT_MS) .println(); + pw.print(KEY_PERSIST_IN_SPLIT_FILES, PERSIST_IN_SPLIT_FILES).println(); + pw.print(Settings.Global.ENABLE_TARE, USE_TARE_POLICY).println(); pw.decreaseIndent(); @@ -1290,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 @@ -1489,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 68cb049af758..c2602f246ce4 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java @@ -40,6 +40,7 @@ import android.util.AtomicFile; import android.util.Pair; import android.util.Slog; import android.util.SparseArray; +import android.util.SparseBooleanArray; import android.util.SystemConfigFileCommitEventLogger; import android.util.Xml; @@ -47,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; @@ -89,6 +91,8 @@ public final class JobStore { /** Threshold to adjust how often we want to write to the db. */ private static final long JOB_PERSIST_DELAY = 2000L; + private static final String JOB_FILE_SPLIT_PREFIX = "jobs_"; + private static final int ALL_UIDS = -1; final Object mLock; final Object mWriteScheduleLock; // used solely for invariants around write scheduling @@ -105,13 +109,20 @@ public final class JobStore { @GuardedBy("mWriteScheduleLock") private boolean mWriteInProgress; + @GuardedBy("mWriteScheduleLock") + private boolean mSplitFileMigrationNeeded; + private static final Object sSingletonLock = new Object(); private final SystemConfigFileCommitEventLogger mEventLogger; private final AtomicFile mJobsFile; + private final File mJobFileDirectory; + private final SparseBooleanArray mPendingJobWriteUids = new SparseBooleanArray(); /** Handler backed by IoThread for writing to disk. */ private final Handler mIoHandler = IoThread.getHandler(); private static JobStore sSingleton; + private boolean mUseSplitFiles = JobSchedulerService.Constants.DEFAULT_PERSIST_IN_SPLIT_FILES; + private JobStorePersistStats mPersistInfo = new JobStorePersistStats(); /** Used by the {@link JobSchedulerService} to instantiate the JobStore. */ @@ -144,10 +155,10 @@ public final class JobStore { mContext = context; File systemDir = new File(dataDir, "system"); - File jobDir = new File(systemDir, "job"); - jobDir.mkdirs(); + mJobFileDirectory = new File(systemDir, "job"); + mJobFileDirectory.mkdirs(); mEventLogger = new SystemConfigFileCommitEventLogger("jobs"); - mJobsFile = new AtomicFile(new File(jobDir, "jobs.xml"), mEventLogger); + mJobsFile = createJobFile(new File(mJobFileDirectory, "jobs.xml")); mJobSet = new JobSet(); @@ -162,12 +173,21 @@ public final class JobStore { // an incorrect historical timestamp. That's fine; at worst we'll reboot with // a *correct* timestamp, see a bunch of overdue jobs, and run them; then // settle into normal operation. - mXmlTimestamp = mJobsFile.getLastModifiedTime(); + mXmlTimestamp = mJobsFile.exists() + ? mJobsFile.getLastModifiedTime() : mJobFileDirectory.lastModified(); mRtcGood = (sSystemClock.millis() > mXmlTimestamp); readJobMapFromDisk(mJobSet, mRtcGood); } + private AtomicFile createJobFile(String baseName) { + return createJobFile(new File(mJobFileDirectory, baseName + ".xml")); + } + + private AtomicFile createJobFile(File file) { + return new AtomicFile(file, mEventLogger); + } + public boolean jobTimesInflatedValid() { return mRtcGood; } @@ -211,6 +231,7 @@ public final class JobStore { public void add(JobStatus jobStatus) { mJobSet.add(jobStatus); if (jobStatus.isPersisted()) { + mPendingJobWriteUids.put(jobStatus.getUid(), true); maybeWriteStatusToDiskAsync(); } if (DEBUG) { @@ -224,6 +245,9 @@ public final class JobStore { @VisibleForTesting public void addForTesting(JobStatus jobStatus) { mJobSet.add(jobStatus); + if (jobStatus.isPersisted()) { + mPendingJobWriteUids.put(jobStatus.getUid(), true); + } } boolean containsJob(JobStatus jobStatus) { @@ -257,12 +281,24 @@ public final class JobStore { return false; } if (removeFromPersisted && jobStatus.isPersisted()) { + mPendingJobWriteUids.put(jobStatus.getUid(), true); maybeWriteStatusToDiskAsync(); } return removed; } /** + * Like {@link #remove(JobStatus, boolean)}, but doesn't schedule a disk write. + */ + @VisibleForTesting + public void removeForTesting(JobStatus jobStatus) { + mJobSet.remove(jobStatus); + if (jobStatus.isPersisted()) { + mPendingJobWriteUids.put(jobStatus.getUid(), true); + } + } + + /** * Remove the jobs of users not specified in the keepUserIds. * @param keepUserIds Array of User IDs whose jobs should be kept and not removed. */ @@ -273,6 +309,7 @@ public final class JobStore { @VisibleForTesting public void clear() { mJobSet.clear(); + mPendingJobWriteUids.put(ALL_UIDS, true); maybeWriteStatusToDiskAsync(); } @@ -282,6 +319,36 @@ public final class JobStore { @VisibleForTesting public void clearForTesting() { mJobSet.clear(); + mPendingJobWriteUids.put(ALL_UIDS, true); + } + + void setUseSplitFiles(boolean useSplitFiles) { + synchronized (mLock) { + if (mUseSplitFiles != useSplitFiles) { + mUseSplitFiles = useSplitFiles; + migrateJobFilesAsync(); + } + } + } + + /** + * The same as above but does not schedule writing. This makes perf benchmarks more stable. + */ + @VisibleForTesting + public void setUseSplitFilesForTesting(boolean useSplitFiles) { + final boolean changed; + synchronized (mLock) { + changed = mUseSplitFiles != useSplitFiles; + if (changed) { + mUseSplitFiles = useSplitFiles; + mPendingJobWriteUids.put(ALL_UIDS, true); + } + } + if (changed) { + synchronized (mWriteScheduleLock) { + mSplitFileMigrationNeeded = true; + } + } } /** @@ -352,6 +419,16 @@ public final class JobStore { private static final String XML_TAG_ONEOFF = "one-off"; private static final String XML_TAG_EXTRAS = "extras"; + private void migrateJobFilesAsync() { + synchronized (mLock) { + mPendingJobWriteUids.put(ALL_UIDS, true); + } + synchronized (mWriteScheduleLock) { + mSplitFileMigrationNeeded = true; + maybeWriteStatusToDiskAsync(); + } + } + /** * Every time the state changes we write all the jobs in one swath, instead of trying to * track incremental changes. @@ -449,10 +526,38 @@ public final class JobStore { * NOTE: This Runnable locks on mLock */ private final Runnable mWriteRunnable = new Runnable() { + private final SparseArray<AtomicFile> mJobFiles = new SparseArray<>(); + private final CopyConsumer mPersistedJobCopier = new CopyConsumer(); + + class CopyConsumer implements Consumer<JobStatus> { + private final SparseArray<List<JobStatus>> mJobStoreCopy = new SparseArray<>(); + private boolean mCopyAllJobs; + + private void prepare() { + mCopyAllJobs = !mUseSplitFiles || mPendingJobWriteUids.get(ALL_UIDS); + } + + @Override + public void accept(JobStatus jobStatus) { + final int uid = mUseSplitFiles ? jobStatus.getUid() : ALL_UIDS; + if (jobStatus.isPersisted() && (mCopyAllJobs || mPendingJobWriteUids.get(uid))) { + List<JobStatus> uidJobList = mJobStoreCopy.get(uid); + if (uidJobList == null) { + uidJobList = new ArrayList<>(); + mJobStoreCopy.put(uid, uidJobList); + } + uidJobList.add(new JobStatus(jobStatus)); + } + } + + private void reset() { + mJobStoreCopy.clear(); + } + } + @Override public void run() { final long startElapsed = sElapsedRealtimeClock.millis(); - final List<JobStatus> storeCopy = new ArrayList<JobStatus>(); // Intentionally allow new scheduling of a write operation *before* we clone // the job set. If we reset it to false after cloning, there's a window in // which no new write will be scheduled but mLock is not held, i.e. a new @@ -469,31 +574,73 @@ public final class JobStore { } mWriteInProgress = true; } + final boolean useSplitFiles; synchronized (mLock) { // Clone the jobs so we can release the lock before writing. - mJobSet.forEachJob(null, (job) -> { - if (job.isPersisted()) { - storeCopy.add(new JobStatus(job)); + useSplitFiles = mUseSplitFiles; + mPersistedJobCopier.prepare(); + mJobSet.forEachJob(null, mPersistedJobCopier); + mPendingJobWriteUids.clear(); + } + mPersistInfo.countAllJobsSaved = 0; + mPersistInfo.countSystemServerJobsSaved = 0; + mPersistInfo.countSystemSyncManagerJobsSaved = 0; + for (int i = mPersistedJobCopier.mJobStoreCopy.size() - 1; i >= 0; --i) { + AtomicFile file; + if (useSplitFiles) { + final int uid = mPersistedJobCopier.mJobStoreCopy.keyAt(i); + file = mJobFiles.get(uid); + if (file == null) { + file = createJobFile(JOB_FILE_SPLIT_PREFIX + uid); + mJobFiles.put(uid, file); } - }); + } else { + file = mJobsFile; + } + if (DEBUG) { + Slog.d(TAG, "Writing for " + mPersistedJobCopier.mJobStoreCopy.keyAt(i) + + " to " + file.getBaseFile().getName() + ": " + + mPersistedJobCopier.mJobStoreCopy.valueAt(i).size() + " jobs"); + } + writeJobsMapImpl(file, mPersistedJobCopier.mJobStoreCopy.valueAt(i)); } - writeJobsMapImpl(storeCopy); if (DEBUG) { Slog.v(TAG, "Finished writing, took " + (sElapsedRealtimeClock.millis() - startElapsed) + "ms"); } + mPersistedJobCopier.reset(); + if (!useSplitFiles) { + mJobFiles.clear(); + } + // Update the last modified time of the directory to aid in RTC time verification + // (see the JobStore constructor). + mJobFileDirectory.setLastModified(sSystemClock.millis()); synchronized (mWriteScheduleLock) { + if (mSplitFileMigrationNeeded) { + final File[] files = mJobFileDirectory.listFiles(); + for (File file : files) { + if (useSplitFiles) { + if (!file.getName().startsWith(JOB_FILE_SPLIT_PREFIX)) { + // Delete the now unused file so there's no confusion in the future. + file.delete(); + } + } else if (file.getName().startsWith(JOB_FILE_SPLIT_PREFIX)) { + // Delete the now unused file so there's no confusion in the future. + file.delete(); + } + } + } mWriteInProgress = false; mWriteScheduleLock.notifyAll(); } } - private void writeJobsMapImpl(List<JobStatus> jobList) { + private void writeJobsMapImpl(@NonNull AtomicFile file, @NonNull List<JobStatus> jobList) { int numJobs = 0; int numSystemJobs = 0; int numSyncJobs = 0; mEventLogger.setStartTime(SystemClock.uptimeMillis()); - try (FileOutputStream fos = mJobsFile.startWrite()) { + try (FileOutputStream fos = file.startWrite()) { TypedXmlSerializer out = Xml.resolveSerializer(fos); out.startDocument(null, true); out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); @@ -523,7 +670,7 @@ public final class JobStore { out.endTag(null, "job-info"); out.endDocument(); - mJobsFile.finishWrite(fos); + file.finishWrite(fos); } catch (IOException e) { if (DEBUG) { Slog.v(TAG, "Error writing out job data.", e); @@ -533,9 +680,9 @@ public final class JobStore { Slog.d(TAG, "Error persisting bundle.", e); } } finally { - mPersistInfo.countAllJobsSaved = numJobs; - mPersistInfo.countSystemServerJobsSaved = numSystemJobs; - mPersistInfo.countSystemSyncManagerJobsSaved = numSyncJobs; + mPersistInfo.countAllJobsSaved += numJobs; + mPersistInfo.countSystemServerJobsSaved += numSystemJobs; + mPersistInfo.countSystemSyncManagerJobsSaved += numSyncJobs; } } @@ -600,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())); @@ -610,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)); @@ -720,54 +881,87 @@ public final class JobStore { @Override public void run() { + if (!mJobFileDirectory.isDirectory()) { + Slog.wtf(TAG, "jobs directory isn't a directory O.O"); + mJobFileDirectory.mkdirs(); + return; + } + int numJobs = 0; int numSystemJobs = 0; int numSyncJobs = 0; List<JobStatus> jobs; - try (FileInputStream fis = mJobsFile.openRead()) { - synchronized (mLock) { - jobs = readJobMapImpl(fis, rtcGood); - if (jobs != null) { - long now = sElapsedRealtimeClock.millis(); - for (int i=0; i<jobs.size(); i++) { - JobStatus js = jobs.get(i); - js.prepareLocked(); - js.enqueueTime = now; - this.jobSet.add(js); - - numJobs++; - if (js.getUid() == Process.SYSTEM_UID) { - numSystemJobs++; - if (isSyncJob(js)) { - numSyncJobs++; + final File[] files; + try { + files = mJobFileDirectory.listFiles(); + } catch (SecurityException e) { + Slog.wtf(TAG, "Not allowed to read job file directory", e); + return; + } + if (files == null) { + Slog.wtfStack(TAG, "Couldn't get job file list"); + return; + } + boolean needFileMigration = false; + long now = sElapsedRealtimeClock.millis(); + for (File file : files) { + final AtomicFile aFile = createJobFile(file); + try (FileInputStream fis = aFile.openRead()) { + synchronized (mLock) { + jobs = readJobMapImpl(fis, rtcGood); + if (jobs != null) { + for (int i = 0; i < jobs.size(); i++) { + JobStatus js = jobs.get(i); + js.prepareLocked(); + js.enqueueTime = now; + this.jobSet.add(js); + + numJobs++; + if (js.getUid() == Process.SYSTEM_UID) { + numSystemJobs++; + if (isSyncJob(js)) { + numSyncJobs++; + } } } } } + } catch (FileNotFoundException e) { + // mJobFileDirectory.listFiles() gave us this file...why can't we find it??? + Slog.e(TAG, "Could not find jobs file: " + file.getName()); + } catch (XmlPullParserException | IOException e) { + Slog.wtf(TAG, "Error in " + file.getName(), e); + } catch (Exception e) { + // Crashing at this point would result in a boot loop, so live with a general + // Exception for system stability's sake. + Slog.wtf(TAG, "Unexpected exception", e); } - } catch (FileNotFoundException e) { - if (DEBUG) { - Slog.d(TAG, "Could not find jobs file, probably there was nothing to load."); - } - } catch (XmlPullParserException | IOException e) { - Slog.wtf(TAG, "Error jobstore xml.", e); - } catch (Exception e) { - // Crashing at this point would result in a boot loop, so live with a general - // Exception for system stability's sake. - Slog.wtf(TAG, "Unexpected exception", e); - } finally { - if (mPersistInfo.countAllJobsLoaded < 0) { // Only set them once. - mPersistInfo.countAllJobsLoaded = numJobs; - mPersistInfo.countSystemServerJobsLoaded = numSystemJobs; - mPersistInfo.countSystemSyncManagerJobsLoaded = numSyncJobs; + if (mUseSplitFiles) { + if (!file.getName().startsWith(JOB_FILE_SPLIT_PREFIX)) { + // We're supposed to be using the split file architecture, but we still have + // the old job file around. Fully migrate and remove the old file. + needFileMigration = true; + } + } else if (file.getName().startsWith(JOB_FILE_SPLIT_PREFIX)) { + // We're supposed to be using the legacy single file architecture, but we still + // have some job split files around. Fully migrate and remove the split files. + needFileMigration = true; } } + if (mPersistInfo.countAllJobsLoaded < 0) { // Only set them once. + mPersistInfo.countAllJobsLoaded = numJobs; + mPersistInfo.countSystemServerJobsLoaded = numSystemJobs; + mPersistInfo.countSystemSyncManagerJobsLoaded = numSyncJobs; + } Slog.i(TAG, "Read " + numJobs + " jobs"); + if (needFileMigration) { + migrateJobFilesAsync(); + } } 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 && @@ -827,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; @@ -1073,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; @@ -1110,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() @@ -1138,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/core/api/current.txt b/core/api/current.txt index bba189f229d9..3a0788dcba06 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -7669,6 +7669,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 +8449,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 { @@ -16585,6 +16598,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 +32183,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 { @@ -32542,7 +32561,7 @@ package android.os { method @NonNull @RequiresPermission(anyOf={"android.permission.MANAGE_USERS", "android.permission.QUERY_USERS", "android.permission.INTERACT_ACROSS_USERS"}, conditional=true) public android.content.pm.UserProperties getUserProperties(@NonNull android.os.UserHandle); method public android.os.Bundle getUserRestrictions(); method @RequiresPermission(anyOf={"android.permission.MANAGE_USERS", "android.permission.INTERACT_ACROSS_USERS"}, conditional=true) public android.os.Bundle getUserRestrictions(android.os.UserHandle); - method @NonNull @RequiresPermission(anyOf={"android.permission.MANAGE_USERS", "android.permission.INTERACT_ACROSS_USERS"}) public java.util.List<android.os.UserHandle> getVisibleUsers(); + method @NonNull @RequiresPermission(anyOf={"android.permission.MANAGE_USERS", "android.permission.INTERACT_ACROSS_USERS"}) public java.util.Set<android.os.UserHandle> getVisibleUsers(); method public boolean hasUserRestriction(String); method public boolean isDemoUser(); method public static boolean isHeadlessSystemUserMode(); @@ -32619,6 +32638,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 +35821,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"; @@ -44095,10 +44116,10 @@ package android.telephony { field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_PURCHASED = 3; // 0x3 field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_CARRIER_DISABLED = 7; // 0x7 field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_CARRIER_ERROR = 8; // 0x8 + 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_CONGESTED = 13; // 0xd field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_NOT_AVAILABLE = 12; // 0xc - field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA_SUB = 14; // 0xe + field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA_SUBSCRIPTION = 14; // 0xe field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_OVERRIDDEN = 5; // 0x5 field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_PENDING_NETWORK_SETUP = 15; // 0xf field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_REQUEST_FAILED = 11; // 0xb @@ -51906,6 +51927,7 @@ 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 +52376,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 +52464,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 +52544,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/system-current.txt b/core/api/system-current.txt index ae6e58c7ed48..b5b87c1353e7 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -14155,6 +14155,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..d8419a87edc2 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -2945,6 +2945,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/java/android/app/OWNERS b/core/java/android/app/OWNERS index 7103806fa751..8ec313ecdd09 100644 --- a/core/java/android/app/OWNERS +++ b/core/java/android/app/OWNERS @@ -30,7 +30,7 @@ per-file ProfilerInfo* = file:/services/core/java/com/android/server/am/OWNERS per-file Service* = file:/services/core/java/com/android/server/am/OWNERS per-file SystemServiceRegistry.java = file:/services/core/java/com/android/server/am/OWNERS per-file *UserSwitchObserver* = file:/services/core/java/com/android/server/am/OWNERS -per-file UiAutomation* = file:/services/accessibility/OWNERS +per-file *UiAutomation* = file:/services/accessibility/OWNERS per-file GameManager* = file:/GAME_MANAGER_OWNERS per-file GameMode* = file:/GAME_MANAGER_OWNERS per-file GameState* = file:/GAME_MANAGER_OWNERS 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/servertransaction/ClientTransaction.java b/core/java/android/app/servertransaction/ClientTransaction.java index 30a6c311bd1e..ee14708c38a8 100644 --- a/core/java/android/app/servertransaction/ClientTransaction.java +++ b/core/java/android/app/servertransaction/ClientTransaction.java @@ -176,7 +176,6 @@ public class ClientTransaction implements Parcelable, ObjectPoolItem { /** Write to Parcel. */ @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeStrongBinder(mClient.asBinder()); final boolean writeActivityToken = mActivityToken != null; dest.writeBoolean(writeActivityToken); if (writeActivityToken) { @@ -192,7 +191,6 @@ public class ClientTransaction implements Parcelable, ObjectPoolItem { /** Read from Parcel. */ private ClientTransaction(Parcel in) { - mClient = (IApplicationThread) in.readStrongBinder(); final boolean readActivityToken = in.readBoolean(); if (readActivityToken) { mActivityToken = in.readStrongBinder(); 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/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/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/BinderProxy.java b/core/java/android/os/BinderProxy.java index 63306612fdaf..1929a4d562d4 100644 --- a/core/java/android/os/BinderProxy.java +++ b/core/java/android/os/BinderProxy.java @@ -536,8 +536,8 @@ public final class BinderProxy implements IBinder { mWarnOnBlocking = false; warnOnBlocking = false; - if (Build.IS_USERDEBUG) { - // Log this as a WTF on userdebug builds. + if (Build.IS_USERDEBUG || Build.IS_ENG) { + // Log this as a WTF on userdebug and eng builds. Log.wtf(Binder.TAG, "Outgoing transactions from this process must be FLAG_ONEWAY", new Throwable()); 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/IUserManager.aidl b/core/java/android/os/IUserManager.aidl index 8eaa5ad7fe94..a887f2a6ef29 100644 --- a/core/java/android/os/IUserManager.aidl +++ b/core/java/android/os/IUserManager.aidl @@ -130,7 +130,7 @@ interface IUserManager { boolean isUserRunning(int userId); boolean isUserForeground(int userId); boolean isUserVisible(int userId); - List<UserHandle> getVisibleUsers(); + int[] getVisibleUsers(); boolean isUserNameSet(int userId); boolean hasRestrictedProfiles(int userId); boolean requestQuietModeEnabled(String callingPackage, boolean enableQuietMode, int userId, in IntentSender target, int flags); 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/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/UserManager.java b/core/java/android/os/UserManager.java index 3d20d6373ae2..bda2a357fb03 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 {} @@ -2904,12 +2929,19 @@ public class UserManager { */ @RequiresPermission(anyOf = {Manifest.permission.MANAGE_USERS, Manifest.permission.INTERACT_ACROSS_USERS}) - public @NonNull List<UserHandle> getVisibleUsers() { + public @NonNull Set<UserHandle> getVisibleUsers() { + ArraySet<UserHandle> result = new ArraySet<>(); try { - return mService.getVisibleUsers(); + int[] visibleUserIds = mService.getVisibleUsers(); + if (visibleUserIds != null) { + for (int userId : visibleUserIds) { + result.add(UserHandle.of(userId)); + } + } } catch (RemoteException re) { throw re.rethrowFromSystemServer(); } + return result; } /** diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index fab6f7b97790..336f18342321 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> @@ -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/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index 37fc9f288c3e..104570d03b42 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -575,6 +575,7 @@ public abstract class WallpaperService extends Service { */ public void reportEngineShown(boolean waitForEngineShown) { if (mIWallpaperEngine.mShownReported) return; + Trace.beginSection("WPMS.reportEngineShown-" + waitForEngineShown); Log.d(TAG, "reportEngineShown: shouldWait=" + waitForEngineShown); if (!waitForEngineShown) { Message message = mCaller.obtainMessage(MSG_REPORT_SHOWN); @@ -587,6 +588,7 @@ public abstract class WallpaperService extends Service { mCaller.sendMessageDelayed(message, TimeUnit.SECONDS.toMillis(5)); } } + Trace.endSection(); } /** @@ -1259,7 +1261,9 @@ public abstract class WallpaperService extends Service { didSurface = true; if (DEBUG) Log.v(TAG, "onSurfaceCreated(" + mSurfaceHolder + "): " + this); + Trace.beginSection("WPMS.Engine.onSurfaceCreated"); onSurfaceCreated(mSurfaceHolder); + Trace.endSection(); SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks(); if (callbacks != null) { for (SurfaceHolder.Callback c : callbacks) { @@ -1285,8 +1289,10 @@ public abstract class WallpaperService extends Service { + ", " + mCurWidth + ", " + mCurHeight + "): " + this); didSurface = true; + Trace.beginSection("WPMS.Engine.onSurfaceChanged"); onSurfaceChanged(mSurfaceHolder, mFormat, mCurWidth, mCurHeight); + Trace.endSection(); SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks(); if (callbacks != null) { for (SurfaceHolder.Callback c : callbacks) { @@ -1303,11 +1309,15 @@ public abstract class WallpaperService extends Service { if (DEBUG) { Log.v(TAG, "dispatching insets=" + windowInsets); } + Trace.beginSection("WPMS.Engine.onApplyWindowInsets"); onApplyWindowInsets(windowInsets); + Trace.endSection(); } if (redrawNeeded) { + Trace.beginSection("WPMS.Engine.onSurfaceRedrawNeeded"); onSurfaceRedrawNeeded(mSurfaceHolder); + Trace.endSection(); SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks(); if (callbacks != null) { for (SurfaceHolder.Callback c : callbacks) { @@ -1332,11 +1342,15 @@ public abstract class WallpaperService extends Service { // the state to get them to notice. if (DEBUG) Log.v(TAG, "onVisibilityChanged(true) at surface: " + this); + Trace.beginSection("WPMS.Engine.onVisibilityChanged-true"); onVisibilityChanged(true); + Trace.endSection(); } if (DEBUG) Log.v(TAG, "onVisibilityChanged(false) at surface: " + this); + Trace.beginSection("WPMS.Engine.onVisibilityChanged-false"); onVisibilityChanged(false); + Trace.endSection(); } } finally { mIsCreating = false; @@ -1421,12 +1435,16 @@ public abstract class WallpaperService extends Service { mDisplayState = mDisplay.getState(); if (DEBUG) Log.v(TAG, "onCreate(): " + this); + Trace.beginSection("WPMS.Engine.onCreate"); onCreate(mSurfaceHolder); + Trace.endSection(); mInitializing = false; mReportedVisible = false; + Trace.beginSection("WPMS.Engine.updateSurface"); updateSurface(false, false, false); + Trace.endSection(); } /** @@ -2236,14 +2254,15 @@ public abstract class WallpaperService extends Service { public void reportShown() { if (!mShownReported) { mShownReported = true; + Trace.beginSection("WPMS.mConnection.engineShown"); try { mConnection.engineShown(this); Log.d(TAG, "Wallpaper has updated the surface:" + mWallpaperManager.getWallpaperInfo()); } catch (RemoteException e) { Log.w(TAG, "Wallpaper host disappeared", e); - return; } + Trace.endSection(); } } @@ -2285,6 +2304,27 @@ public abstract class WallpaperService extends Service { return mEngine == null ? null : SurfaceControl.mirrorSurface(mEngine.mSurfaceControl); } + private void doAttachEngine() { + Trace.beginSection("WPMS.onCreateEngine"); + Engine engine = onCreateEngine(); + Trace.endSection(); + mEngine = engine; + Trace.beginSection("WPMS.mConnection.attachEngine-" + mDisplayId); + try { + mConnection.attachEngine(this, mDisplayId); + } catch (RemoteException e) { + engine.detach(); + Log.w(TAG, "Wallpaper host disappeared", e); + return; + } finally { + Trace.endSection(); + } + mActiveEngines.add(engine); + Trace.beginSection("WPMS.engine.attach"); + engine.attach(this); + Trace.endSection(); + } + private void doDetachEngine() { mActiveEngines.remove(mEngine); mEngine.detach(); @@ -2310,21 +2350,15 @@ public abstract class WallpaperService extends Service { } switch (message.what) { case DO_ATTACH: { - Engine engine = onCreateEngine(); - mEngine = engine; - try { - mConnection.attachEngine(this, mDisplayId); - } catch (RemoteException e) { - engine.detach(); - Log.w(TAG, "Wallpaper host disappeared", e); - return; - } - mActiveEngines.add(engine); - engine.attach(this); + Trace.beginSection("WPMS.DO_ATTACH"); + doAttachEngine(); + Trace.endSection(); return; } case DO_DETACH: { + Trace.beginSection("WPMS.DO_DETACH"); doDetachEngine(); + Trace.endSection(); return; } case DO_SET_DESIRED_SIZE: { @@ -2405,7 +2439,9 @@ public abstract class WallpaperService extends Service { } } break; case MSG_REPORT_SHOWN: { + Trace.beginSection("WPMS.MSG_REPORT_SHOWN"); reportShown(); + Trace.endSection(); } break; default : Log.w(TAG, "Unknown message type " + message.what); @@ -2429,8 +2465,10 @@ public abstract class WallpaperService extends Service { public void attach(IWallpaperConnection conn, IBinder windowToken, int windowType, boolean isPreview, int reqWidth, int reqHeight, Rect padding, int displayId, @SetWallpaperFlags int which) { + Trace.beginSection("WPMS.ServiceWrapper.attach"); mEngineWrapper = new IWallpaperEngineWrapper(mTarget, conn, windowToken, windowType, isPreview, reqWidth, reqHeight, padding, displayId); + Trace.endSection(); } @Override @@ -2441,16 +2479,20 @@ public abstract class WallpaperService extends Service { @Override public void onCreate() { + Trace.beginSection("WPMS.onCreate"); super.onCreate(); + Trace.endSection(); } @Override public void onDestroy() { + Trace.beginSection("WPMS.onDestroy"); super.onDestroy(); for (int i=0; i<mActiveEngines.size(); i++) { mActiveEngines.get(i).detach(); } mActiveEngines.clear(); + Trace.endSection(); } /** 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/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/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 b088514c6989..636023938b9d 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -712,7 +712,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(); @@ -1106,11 +1106,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; } @@ -1242,11 +1242,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; } @@ -1256,8 +1256,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; @@ -1788,24 +1788,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. @@ -8232,7 +8232,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); 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/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/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 8aba526e6d3d..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); } @@ -3406,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; } 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/com/android/internal/widget/RemeasuringLinearLayout.java b/core/java/com/android/internal/widget/RemeasuringLinearLayout.java index 7b154a54fc85..80d14574955d 100644 --- a/core/java/com/android/internal/widget/RemeasuringLinearLayout.java +++ b/core/java/com/android/internal/widget/RemeasuringLinearLayout.java @@ -18,6 +18,7 @@ package com.android.internal.widget; import android.annotation.Nullable; import android.content.Context; +import android.os.Trace; import android.util.AttributeSet; import android.view.View; import android.widget.LinearLayout; @@ -54,6 +55,7 @@ public class RemeasuringLinearLayout extends LinearLayout { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + Trace.beginSection("RemeasuringLinearLayout#onMeasure"); super.onMeasure(widthMeasureSpec, heightMeasureSpec); int count = getChildCount(); int height = 0; @@ -86,5 +88,6 @@ public class RemeasuringLinearLayout extends LinearLayout { } mMatchParentViews.clear(); setMeasuredDimension(getMeasuredWidth(), height); + Trace.endSection(); } } diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp index d9ca16ea8748..0798110134f8 100644 --- a/core/jni/AndroidRuntime.cpp +++ b/core/jni/AndroidRuntime.cpp @@ -19,6 +19,7 @@ #define LOG_NDEBUG 1 #include <android-base/macros.h> +#include <android-base/parsebool.h> #include <android-base/properties.h> #include <android/graphics/jni_runtime.h> #include <android_runtime/AndroidRuntime.h> @@ -52,6 +53,8 @@ using namespace android; using android::base::GetBoolProperty; using android::base::GetProperty; +using android::base::ParseBool; +using android::base::ParseBoolResult; extern int register_android_os_Binder(JNIEnv* env); extern int register_android_os_Process(JNIEnv* env); @@ -703,17 +706,24 @@ int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, bool zygote, bool p // Read if we are using the profile configuration, do this at the start since the last ART args // take precedence. - property_get("dalvik.vm.profilebootclasspath", propBuf, ""); - std::string profile_boot_class_path_flag = propBuf; - // Empty means the property is unset and we should default to the phenotype property. - // The possible values are {"true", "false", ""} - if (profile_boot_class_path_flag.empty()) { - profile_boot_class_path_flag = server_configurable_flags::GetServerConfigurableFlag( - RUNTIME_NATIVE_BOOT_NAMESPACE, - PROFILE_BOOT_CLASS_PATH, - /*default_value=*/ ""); + std::string profile_boot_class_path_flag = + server_configurable_flags::GetServerConfigurableFlag(RUNTIME_NATIVE_BOOT_NAMESPACE, + PROFILE_BOOT_CLASS_PATH, + /*default_value=*/""); + bool profile_boot_class_path; + switch (ParseBool(profile_boot_class_path_flag)) { + case ParseBoolResult::kError: + // Default to the system property. + profile_boot_class_path = + GetBoolProperty("dalvik.vm.profilebootclasspath", /*default_value=*/false); + break; + case ParseBoolResult::kTrue: + profile_boot_class_path = true; + break; + case ParseBoolResult::kFalse: + profile_boot_class_path = false; + break; } - const bool profile_boot_class_path = (profile_boot_class_path_flag == "true"); if (profile_boot_class_path) { addOption("-Xcompiler-option"); addOption("--count-hotness-in-compiled-code"); 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/res/values/config.xml b/core/res/res/values/config.xml index 0218215b9946..2157270d81af 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 diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 48484c7d41f9..216975d2d2e6 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -6331,6 +6331,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..5eb62919214c 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3480,6 +3480,9 @@ <!-- Captive Portal Login --> <java-symbol type="string" name="config_defaultCaptivePortalLoginPackageName" /> + <!-- Dock Manager --> + <java-symbol type="string" name="config_defaultDockManagerPackageName" /> + <!-- Optional IPsec algorithms --> <java-symbol type="array" name="config_optionalIpSecAlgorithms" /> @@ -4858,6 +4861,7 @@ <!-- For VirtualDeviceManager --> <java-symbol type="string" name="vdm_camera_access_denied" /> <java-symbol type="string" name="vdm_secure_window" /> + <java-symbol type="string" name="vdm_pip_blocked" /> <java-symbol type="color" name="camera_privacy_light_day"/> <java-symbol type="color" name="camera_privacy_light_night"/> diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceAidlImplTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceAidlImplTest.java index 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 06d7cddf3148..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]; @@ -210,7 +205,7 @@ public final class TunerSessionTest { mTunerSessions[0].setMuted(/* mute= */ false); - assertWithMessage("Session mute state after setting muted %s", false) + assertWithMessage("Session mute state after setting unmuted") .that(mTunerSessions[0].isMuted()).isFalse(); } @@ -220,7 +215,7 @@ public final class TunerSessionTest { mTunerSessions[0].setMuted(/* mute= */ true); - assertWithMessage("Session mute state after setting muted %s", true) + assertWithMessage("Session mute state after setting muted") .that(mTunerSessions[0].isMuted()).isTrue(); } @@ -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 392e140f24e0..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 @@ -22,6 +22,7 @@ import android.hardware.broadcastradio.V2_0.VendorKeyValue; import android.hardware.radio.ProgramSelector; import android.hardware.radio.RadioManager; import android.hardware.radio.RadioMetadata; +import android.util.ArrayMap; import java.util.ArrayList; import java.util.HashMap; @@ -32,14 +33,43 @@ 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, + /* infoFlags= */ 0, signalQuality, + new RadioMetadata.Builder().build(), new ArrayMap<>()); + } + static RadioManager.ProgramInfo makeProgramInfo(int programType, ProgramSelector.Identifier identifier, int signalQuality) { // Note: If you set new fields, check if programInfoToHal() needs to be updated as well. - return new RadioManager.ProgramInfo(new ProgramSelector(programType, identifier, null, - null), null, null, null, 0, signalQuality, new RadioMetadata.Builder().build(), + return new RadioManager.ProgramInfo(makeProgramSelector(programType, identifier), null, + null, null, 0, signalQuality, new RadioMetadata.Builder().build(), new HashMap<String, String>()); } + static ProgramSelector makeFmSelector(long freq) { + return makeProgramSelector(ProgramSelector.PROGRAM_TYPE_FM, + new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, + freq)); + } + + static ProgramSelector makeProgramSelector(int programType, + ProgramSelector.Identifier identifier) { + return new ProgramSelector(programType, identifier, /* secondaryIds= */ null, + /* vendorIds= */ null); + } + static ProgramInfo programInfoToHal(RadioManager.ProgramInfo info) { // Note that because Convert does not by design provide functions for all conversions, this // function only copies fields that are set by makeProgramInfo(). @@ -57,10 +87,22 @@ final class TestUtils { android.hardware.broadcastradio.V2_0.ProgramSelector halSelector = new android.hardware.broadcastradio.V2_0.ProgramSelector(); halSelector.primaryId = halId; - halSelector.secondaryIds = new ArrayList<>(); + halSelector.secondaryIds = new ArrayList<ProgramIdentifier>(); return halSelector; } + static ProgramInfo makeHalProgramInfo( + android.hardware.broadcastradio.V2_0.ProgramSelector hwSel, int hwSignalQuality) { + ProgramInfo hwInfo = new ProgramInfo(); + hwInfo.selector = hwSel; + hwInfo.logicallyTunedTo = hwSel.primaryId; + hwInfo.physicallyTunedTo = hwSel.primaryId; + hwInfo.signalQuality = hwSignalQuality; + hwInfo.relatedContent = new ArrayList<>(); + hwInfo.metadata = new ArrayList<>(); + return hwInfo; + } + static VendorKeyValue makeVendorKeyValue(String vendorKey, String vendorValue) { VendorKeyValue vendorKeyValue = new VendorKeyValue(); vendorKeyValue.key = vendorKey; diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TunerSessionHidlTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TunerSessionHidlTest.java new file mode 100644 index 000000000000..936e606fcb76 --- /dev/null +++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TunerSessionHidlTest.java @@ -0,0 +1,622 @@ +/* + * 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.anyBoolean; +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; +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.ITunerCallback; +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; +import org.mockito.Mock; +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. + */ +@RunWith(MockitoJUnitRunner.class) +public final class TunerSessionHidlTest { + + private static final VerificationWithTimeout CALLBACK_TIMEOUT = + timeout(/* millis= */ 200); + private static final int SIGNAL_QUALITY = 1; + private static final long AM_FM_FREQUENCY_SPACING = 500; + private static final long[] AM_FM_FREQUENCY_LIST = {97500, 98100, 99100}; + private static final RadioManager.FmBandDescriptor FM_BAND_DESCRIPTOR = + new RadioManager.FmBandDescriptor(RadioManager.REGION_ITU_1, RadioManager.BAND_FM, + /* lowerLimit= */ 87500, /* upperLimit= */ 108000, /* spacing= */ 100, + /* stereo= */ false, /* rds= */ false, /* ta= */ false, /* af= */ false, + /* ea= */ false); + private static final RadioManager.BandConfig FM_BAND_CONFIG = + new RadioManager.FmBandConfig(FM_BAND_DESCRIPTOR); + 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; + private TunerSession[] mTunerSessions; + + @Mock private IBroadcastRadio mBroadcastRadioMock; + @Mock ITunerSession mHalTunerSessionMock; + private android.hardware.radio.ITunerCallback[] mAidlTunerCallbackMocks; + + @Before + public void setup() throws Exception { + mRadioModule = new RadioModule(mBroadcastRadioMock, + TestUtils.makeDefaultModuleProperties(), mLock); + + doAnswer(invocation -> { + mHalTunerCallback = (ITunerCallback) invocation.getArguments()[0]; + IBroadcastRadio.openSessionCallback cb = (IBroadcastRadio.openSessionCallback) + invocation.getArguments()[1]; + cb.onValues(Result.OK, mHalTunerSessionMock); + return null; + }).when(mBroadcastRadioMock).openSession(any(), any()); + + doAnswer(invocation -> { + android.hardware.broadcastradio.V2_0.ProgramSelector halSel = + (android.hardware.broadcastradio.V2_0.ProgramSelector) + invocation.getArguments()[0]; + mHalCurrentInfo = TestUtils.makeHalProgramInfo(halSel, SIGNAL_QUALITY); + if (halSel.primaryId.type != IdentifierType.AMFM_FREQUENCY) { + return Result.NOT_SUPPORTED; + } + mHalTunerCallback.onCurrentProgramInfoChanged(mHalCurrentInfo); + return Result.OK; + }).when(mHalTunerSessionMock).tune(any()); + + doAnswer(invocation -> { + if ((boolean) invocation.getArguments()[0]) { + mHalCurrentInfo.selector.primaryId.value += AM_FM_FREQUENCY_SPACING; + } else { + mHalCurrentInfo.selector.primaryId.value -= AM_FM_FREQUENCY_SPACING; + } + mHalCurrentInfo.logicallyTunedTo = mHalCurrentInfo.selector.primaryId; + mHalCurrentInfo.physicallyTunedTo = mHalCurrentInfo.selector.primaryId; + mHalTunerCallback.onCurrentProgramInfoChanged(mHalCurrentInfo); + return Result.OK; + }).when(mHalTunerSessionMock).step(anyBoolean()); + + doAnswer(invocation -> { + if (mHalCurrentInfo == null) { + android.hardware.broadcastradio.V2_0.ProgramSelector placeHolderSelector = + TestUtils.makeHalFmSelector(/* freq= */ 97300); + + mHalTunerCallback.onTuneFailed(Result.TIMEOUT, placeHolderSelector); + return Result.OK; + } + mHalCurrentInfo.selector.primaryId.value = getSeekFrequency( + mHalCurrentInfo.selector.primaryId.value, + !(boolean) invocation.getArguments()[0]); + mHalCurrentInfo.logicallyTunedTo = mHalCurrentInfo.selector.primaryId; + mHalCurrentInfo.physicallyTunedTo = mHalCurrentInfo.selector.primaryId; + mHalTunerCallback.onCurrentProgramInfoChanged(mHalCurrentInfo); + return Result.OK; + }).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 + public void openSession_withMultipleSessions() throws Exception { + int numSessions = 3; + + openAidlClients(numSessions); + + for (int index = 0; index < numSessions; index++) { + assertWithMessage("Session of index %s close state", index) + .that(mTunerSessions[index].isClosed()).isFalse(); + } + } + + @Test + public void setConfiguration() throws Exception { + openAidlClients(/* numClients= */ 1); + + mTunerSessions[0].setConfiguration(FM_BAND_CONFIG); + + verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onConfigurationChanged(FM_BAND_CONFIG); + } + + @Test + public void getConfiguration() throws Exception { + openAidlClients(/* numClients= */ 1); + mTunerSessions[0].setConfiguration(FM_BAND_CONFIG); + + RadioManager.BandConfig config = mTunerSessions[0].getConfiguration(); + + assertWithMessage("Session configuration").that(config) + .isEqualTo(FM_BAND_CONFIG); + } + + @Test + public void setMuted_withUnmuted() throws Exception { + openAidlClients(/* numClients= */ 1); + + mTunerSessions[0].setMuted(/* mute= */ false); + + assertWithMessage("Session mute state after setting unmuted") + .that(mTunerSessions[0].isMuted()).isFalse(); + } + + @Test + public void setMuted_withMuted() throws Exception { + openAidlClients(/* numClients= */ 1); + + mTunerSessions[0].setMuted(/* mute= */ true); + + assertWithMessage("Session mute state after setting muted") + .that(mTunerSessions[0].isMuted()).isTrue(); + } + + @Test + public void close_withOneSession() throws Exception { + openAidlClients(/* numClients= */ 1); + + mTunerSessions[0].close(); + + assertWithMessage("Close state of broadcast radio service session") + .that(mTunerSessions[0].isClosed()).isTrue(); + } + + @Test + public void close_withOnlyOneSession_withMultipleSessions() throws Exception { + int numSessions = 3; + openAidlClients(numSessions); + int closeIdx = 0; + + mTunerSessions[closeIdx].close(); + + for (int index = 0; index < numSessions; index++) { + if (index == closeIdx) { + assertWithMessage( + "Close state of broadcast radio service session of index %s", index) + .that(mTunerSessions[index].isClosed()).isTrue(); + } else { + assertWithMessage( + "Close state of broadcast radio service session of index %s", index) + .that(mTunerSessions[index].isClosed()).isFalse(); + } + } + } + + @Test + public void close_withOneSession_withError() throws Exception { + openAidlClients(/* numClients= */ 1); + int errorCode = RadioTuner.ERROR_SERVER_DIED; + + mTunerSessions[0].close(errorCode); + + verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onError(errorCode); + assertWithMessage("Close state of broadcast radio service session") + .that(mTunerSessions[0].isClosed()).isTrue(); + } + + @Test + public void closeSessions_withMultipleSessions_withError() throws Exception { + int numSessions = 3; + openAidlClients(numSessions); + + int errorCode = RadioTuner.ERROR_SERVER_DIED; + mRadioModule.closeSessions(errorCode); + + for (int index = 0; index < numSessions; index++) { + verify(mAidlTunerCallbackMocks[index], CALLBACK_TIMEOUT).onError(errorCode); + assertWithMessage("Close state of broadcast radio service session of index %s", index) + .that(mTunerSessions[index].isClosed()).isTrue(); + } + } + + @Test + public void tune_withOneSession() throws Exception { + openAidlClients(/* numClients= */ 1); + ProgramSelector initialSel = TestUtils.makeFmSelector(AM_FM_FREQUENCY_LIST[1]); + RadioManager.ProgramInfo tuneInfo = + TestUtils.makeProgramInfo(initialSel, SIGNAL_QUALITY); + + mTunerSessions[0].tune(initialSel); + + verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onCurrentProgramInfoChanged(tuneInfo); + } + + @Test + public void tune_withMultipleSessions() throws Exception { + int numSessions = 3; + openAidlClients(numSessions); + ProgramSelector initialSel = TestUtils.makeFmSelector(AM_FM_FREQUENCY_LIST[1]); + RadioManager.ProgramInfo tuneInfo = + TestUtils.makeProgramInfo(initialSel, SIGNAL_QUALITY); + + mTunerSessions[0].tune(initialSel); + + for (int index = 0; index < numSessions; index++) { + verify(mAidlTunerCallbackMocks[index], CALLBACK_TIMEOUT) + .onCurrentProgramInfoChanged(tuneInfo); + } + } + + @Test + public void tune_withUnsupportedSelector_throwsException() throws Exception { + openAidlClients(/* numClients= */ 1); + ProgramSelector unsupportedSelector = TestUtils.makeProgramSelector( + ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY, new ProgramSelector.Identifier( + ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY, /* value= */ 300)); + + UnsupportedOperationException thrown = assertThrows(UnsupportedOperationException.class, + () -> mTunerSessions[0].tune(unsupportedSelector)); + + assertWithMessage("Exception for tuning on unsupported program selector") + .that(thrown).hasMessageThat().contains("tune: NOT_SUPPORTED"); + } + + @Test + public void step_withDirectionUp() throws Exception { + long initFreq = AM_FM_FREQUENCY_LIST[1]; + ProgramSelector initialSel = TestUtils.makeFmSelector(initFreq); + RadioManager.ProgramInfo stepUpInfo = TestUtils.makeProgramInfo( + TestUtils.makeFmSelector(initFreq + AM_FM_FREQUENCY_SPACING), SIGNAL_QUALITY); + openAidlClients(/* numClients= */ 1); + mHalCurrentInfo = TestUtils.makeHalProgramInfo( + Convert.programSelectorToHal(initialSel), SIGNAL_QUALITY); + + mTunerSessions[0].step(/* directionDown= */ false, /* skipSubChannel= */ false); + + verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT) + .onCurrentProgramInfoChanged(stepUpInfo); + } + + @Test + public void step_withDirectionDown() throws Exception { + long initFreq = AM_FM_FREQUENCY_LIST[1]; + ProgramSelector initialSel = TestUtils.makeFmSelector(initFreq); + RadioManager.ProgramInfo stepDownInfo = TestUtils.makeProgramInfo( + TestUtils.makeFmSelector(initFreq - AM_FM_FREQUENCY_SPACING), + SIGNAL_QUALITY); + openAidlClients(/* numClients= */ 1); + mHalCurrentInfo = TestUtils.makeHalProgramInfo( + Convert.programSelectorToHal(initialSel), SIGNAL_QUALITY); + + mTunerSessions[0].step(/* directionDown= */ true, /* skipSubChannel= */ false); + + verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT) + .onCurrentProgramInfoChanged(stepDownInfo); + } + + @Test + public void scan_withDirectionUp() throws Exception { + long initFreq = AM_FM_FREQUENCY_LIST[2]; + ProgramSelector initialSel = TestUtils.makeFmSelector(initFreq); + RadioManager.ProgramInfo scanUpInfo = TestUtils.makeProgramInfo( + TestUtils.makeFmSelector(getSeekFrequency(initFreq, /* seekDown= */ false)), + SIGNAL_QUALITY); + openAidlClients(/* numClients= */ 1); + mHalCurrentInfo = TestUtils.makeHalProgramInfo( + Convert.programSelectorToHal(initialSel), SIGNAL_QUALITY); + + mTunerSessions[0].scan(/* directionDown= */ false, /* skipSubChannel= */ false); + + verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT) + .onCurrentProgramInfoChanged(scanUpInfo); + } + + @Test + public void scan_callsOnTuneFailedWhenTimeout() throws Exception { + int numSessions = 2; + openAidlClients(numSessions); + + mTunerSessions[0].scan(/* directionDown= */ false, /* skipSubChannel= */ false); + + for (int index = 0; index < numSessions; index++) { + verify(mAidlTunerCallbackMocks[index], CALLBACK_TIMEOUT) + .onTuneFailed(eq(Result.TIMEOUT), any()); + } + } + + @Test + public void scan_withDirectionDown() throws Exception { + long initFreq = AM_FM_FREQUENCY_LIST[2]; + ProgramSelector initialSel = TestUtils.makeFmSelector(initFreq); + RadioManager.ProgramInfo scanUpInfo = TestUtils.makeProgramInfo( + TestUtils.makeFmSelector(getSeekFrequency(initFreq, /* seekDown= */ true)), + SIGNAL_QUALITY); + openAidlClients(/* numClients= */ 1); + mHalCurrentInfo = TestUtils.makeHalProgramInfo( + Convert.programSelectorToHal(initialSel), SIGNAL_QUALITY); + + mTunerSessions[0].scan(/* directionDown= */ true, /* skipSubChannel= */ false); + verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT) + .onCurrentProgramInfoChanged(scanUpInfo); + } + + @Test + public void cancel() throws Exception { + openAidlClients(/* numClients= */ 1); + ProgramSelector initialSel = TestUtils.makeFmSelector(AM_FM_FREQUENCY_LIST[1]); + mTunerSessions[0].tune(initialSel); + + mTunerSessions[0].cancel(); + + verify(mHalTunerSessionMock).cancel(); + } + + @Test + public void getImage_withInvalidId_throwsIllegalArgumentException() throws Exception { + openAidlClients(/* numClients= */ 1); + int imageId = Constants.INVALID_IMAGE; + + IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> { + mTunerSessions[0].getImage(imageId); + }); + + assertWithMessage("Get image exception") + .that(thrown).hasMessageThat().contains("Image ID is missing"); + } + + @Test + public void getImage_withValidId() throws Exception { + openAidlClients(/* numClients= */ 1); + int imageId = 1; + + Bitmap imageTest = mTunerSessions[0].getImage(imageId); + + assertWithMessage("Null image").that(imageTest).isEqualTo(null); + } + + @Test + public void startBackgroundScan() throws Exception { + openAidlClients(/* numClients= */ 1); + + mTunerSessions[0].startBackgroundScan(); + + verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onBackgroundScanComplete(); + } + + @Test + public void stopProgramListUpdates() throws Exception { + openAidlClients(/* numClients= */ 1); + ProgramList.Filter aidlFilter = new ProgramList.Filter(new ArraySet<>(), new ArraySet<>(), + /* includeCategories= */ true, /* excludeModifications= */ false); + mTunerSessions[0].startProgramListUpdates(aidlFilter); + + mTunerSessions[0].stopProgramListUpdates(); + + 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]; + for (int index = 0; index < numClients; index++) { + mAidlTunerCallbackMocks[index] = mock(android.hardware.radio.ITunerCallback.class); + mTunerSessions[index] = mRadioModule.openSession(mAidlTunerCallbackMocks[index]); + } + } + + private long getSeekFrequency(long currentFrequency, boolean seekDown) { + long seekFrequency; + if (seekDown) { + seekFrequency = AM_FM_FREQUENCY_LIST[AM_FM_FREQUENCY_LIST.length - 1]; + for (int i = AM_FM_FREQUENCY_LIST.length - 1; i >= 0; i--) { + if (AM_FM_FREQUENCY_LIST[i] < currentFrequency) { + seekFrequency = AM_FM_FREQUENCY_LIST[i]; + break; + } + } + } else { + seekFrequency = AM_FM_FREQUENCY_LIST[0]; + for (int index = 0; index < AM_FM_FREQUENCY_LIST.length; index++) { + if (AM_FM_FREQUENCY_LIST[index] > currentFrequency) { + seekFrequency = AM_FM_FREQUENCY_LIST[index]; + break; + } + } + } + return seekFrequency; + } +} diff --git a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java index b292d7dfafe2..a0ed0266d24b 100644 --- a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java +++ b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java @@ -294,10 +294,9 @@ public class TransactionParcelTests { StopActivityItem lifecycleRequest = StopActivityItem.obtain(78 /* configChanges */); - IApplicationThread appThread = new StubAppThread(); Binder activityToken = new Binder(); - ClientTransaction transaction = ClientTransaction.obtain(appThread, activityToken); + ClientTransaction transaction = ClientTransaction.obtain(null, activityToken); transaction.addCallback(callback1); transaction.addCallback(callback2); transaction.setLifecycleStateRequest(lifecycleRequest); @@ -318,10 +317,9 @@ public class TransactionParcelTests { ActivityConfigurationChangeItem callback2 = ActivityConfigurationChangeItem.obtain( config()); - IApplicationThread appThread = new StubAppThread(); Binder activityToken = new Binder(); - ClientTransaction transaction = ClientTransaction.obtain(appThread, activityToken); + ClientTransaction transaction = ClientTransaction.obtain(null, activityToken); transaction.addCallback(callback1); transaction.addCallback(callback2); @@ -339,10 +337,9 @@ public class TransactionParcelTests { // Write to parcel StopActivityItem lifecycleRequest = StopActivityItem.obtain(78 /* configChanges */); - IApplicationThread appThread = new StubAppThread(); Binder activityToken = new Binder(); - ClientTransaction transaction = ClientTransaction.obtain(appThread, activityToken); + ClientTransaction transaction = ClientTransaction.obtain(null, activityToken); transaction.setLifecycleStateRequest(lifecycleRequest); writeAndPrepareForReading(transaction); @@ -400,286 +397,4 @@ public class TransactionParcelTests { } }; } - - /** Stub implementation of IApplicationThread that can be presented as {@link Binder}. */ - class StubAppThread extends android.app.IApplicationThread.Stub { - - @Override - public void scheduleTransaction(ClientTransaction transaction) throws RemoteException { - } - - @Override - public void scheduleReceiver(Intent intent, ActivityInfo activityInfo, - CompatibilityInfo compatibilityInfo, int i, String s, Bundle bundle, boolean b, - int i1, int i2) throws RemoteException { - } - - @Override - public void scheduleCreateService(IBinder iBinder, ServiceInfo serviceInfo, - CompatibilityInfo compatibilityInfo, int i) throws RemoteException { - } - - @Override - public void scheduleStopService(IBinder iBinder) throws RemoteException { - } - - @Override - public void bindApplication(String s, ApplicationInfo applicationInfo, - String sdkSandboxClientAppVolumeUuid, String sdkSandboxClientAppPackage, - ProviderInfoList list, ComponentName componentName, ProfilerInfo profilerInfo, - Bundle bundle, IInstrumentationWatcher iInstrumentationWatcher, - IUiAutomationConnection iUiAutomationConnection, int i, boolean b, boolean b1, - boolean b2, boolean b3, Configuration configuration, - CompatibilityInfo compatibilityInfo, Map map, Bundle bundle1, String s1, - AutofillOptions ao, ContentCaptureOptions co, long[] disableCompatChanges, - SharedMemory serializedSystemFontMap, - long startRequestedElapsedTime, long startRequestedUptime) - throws RemoteException { - } - - @Override - public void scheduleExit() throws RemoteException { - } - - @Override - public void scheduleServiceArgs(IBinder iBinder, ParceledListSlice parceledListSlice) - throws RemoteException { - } - - @Override - public void updateTimeZone() throws RemoteException { - } - - @Override - public void processInBackground() throws RemoteException { - } - - @Override - public void scheduleBindService(IBinder iBinder, Intent intent, boolean b, int i) - throws RemoteException { - } - - @Override - public void scheduleUnbindService(IBinder iBinder, Intent intent) throws RemoteException { - } - - @Override - public void dumpService(ParcelFileDescriptor parcelFileDescriptor, IBinder iBinder, - String[] strings) throws RemoteException { - } - - @Override - public void scheduleRegisteredReceiver(IIntentReceiver iIntentReceiver, Intent intent, - int i, String s, Bundle bundle, boolean b, boolean b1, int i1, int i2) - throws RemoteException { - } - - @Override - public void scheduleLowMemory() throws RemoteException { - } - - @Override - public void profilerControl(boolean b, ProfilerInfo profilerInfo, int i) - throws RemoteException { - } - - @Override - public void setSchedulingGroup(int i) throws RemoteException { - } - - @Override - public void scheduleCreateBackupAgent(ApplicationInfo applicationInfo, - int i, int userId, int operatioType) - throws RemoteException { - } - - @Override - public void scheduleDestroyBackupAgent(ApplicationInfo applicationInfo, - int userId) throws RemoteException { - } - - @Override - public void scheduleOnNewActivityOptions(IBinder iBinder, Bundle bundle) - throws RemoteException { - } - - @Override - public void scheduleSuicide() throws RemoteException { - } - - @Override - public void dispatchPackageBroadcast(int i, String[] strings) throws RemoteException { - } - - @Override - public void scheduleCrash(String s, int i, Bundle extras) throws RemoteException { - } - - @Override - public void dumpActivity(ParcelFileDescriptor parcelFileDescriptor, IBinder iBinder, - String s, String[] strings) throws RemoteException { - } - - @Override - public void clearDnsCache() throws RemoteException { - } - - @Override - public void updateHttpProxy() throws RemoteException { - } - - @Override - public void setCoreSettings(Bundle bundle) throws RemoteException { - } - - @Override - public void updatePackageCompatibilityInfo(String s, CompatibilityInfo compatibilityInfo) - throws RemoteException { - } - - @Override - public void scheduleTrimMemory(int i) throws RemoteException { - } - - @Override - public void dumpMemInfo(ParcelFileDescriptor parcelFileDescriptor, - Debug.MemoryInfo memoryInfo, boolean b, boolean b1, boolean b2, boolean b3, - boolean b4, String[] strings) throws RemoteException { - } - - @Override - public void dumpMemInfoProto(ParcelFileDescriptor parcelFileDescriptor, - Debug.MemoryInfo memoryInfo, boolean b, boolean b1, boolean b2, - boolean b3, String[] strings) throws RemoteException { - } - - @Override - public void dumpGfxInfo(ParcelFileDescriptor parcelFileDescriptor, String[] strings) - throws RemoteException { - } - - @Override - public void dumpCacheInfo(ParcelFileDescriptor parcelFileDescriptor, String[] strings) - throws RemoteException { - } - - @Override - public void dumpProvider(ParcelFileDescriptor parcelFileDescriptor, IBinder iBinder, - String[] strings) throws RemoteException { - } - - @Override - public void dumpDbInfo(ParcelFileDescriptor parcelFileDescriptor, String[] strings) - throws RemoteException { - } - - @Override - public void unstableProviderDied(IBinder iBinder) throws RemoteException { - } - - @Override - public void requestAssistContextExtras(IBinder iBinder, IBinder iBinder1, int i, int i1, - int i2) throws RemoteException { - } - - @Override - public void scheduleTranslucentConversionComplete(IBinder iBinder, boolean b) - throws RemoteException { - } - - @Override - public void setProcessState(int i) throws RemoteException { - } - - @Override - public void scheduleInstallProvider(ProviderInfo providerInfo) throws RemoteException { - } - - @Override - public void updateTimePrefs(int i) throws RemoteException { - } - - @Override - public void scheduleEnterAnimationComplete(IBinder iBinder) throws RemoteException { - } - - @Override - public void notifyCleartextNetwork(byte[] bytes) throws RemoteException { - } - - @Override - public void startBinderTracking() throws RemoteException { - } - - @Override - public void stopBinderTrackingAndDump(ParcelFileDescriptor parcelFileDescriptor) - throws RemoteException { - } - - @Override - public void scheduleLocalVoiceInteractionStarted(IBinder iBinder, - IVoiceInteractor iVoiceInteractor) throws RemoteException { - } - - @Override - public void handleTrustStorageUpdate() throws RemoteException { - } - - @Override - public void attachAgent(String s) throws RemoteException { - } - - @Override - public void attachStartupAgents(String s) throws RemoteException { - } - - @Override - public void scheduleApplicationInfoChanged(ApplicationInfo applicationInfo) - throws RemoteException { - } - - @Override - public void setNetworkBlockSeq(long l) throws RemoteException { - } - - @Override - public void dumpHeap(boolean managed, boolean mallocInfo, boolean runGc, String path, - ParcelFileDescriptor fd, RemoteCallback finishCallback) { - } - - @Override - public void dumpResources(ParcelFileDescriptor fd, RemoteCallback finishCallback) { - } - - @Override - public final void runIsolatedEntryPoint(String entryPoint, String[] entryPointArgs) { - } - - @Override - public void requestDirectActions(IBinder activityToken, IVoiceInteractor interactor, - RemoteCallback cancellationCallback, RemoteCallback resultCallback) { - } - - @Override - public void performDirectAction(IBinder activityToken, String actionId, Bundle arguments, - RemoteCallback cancellationCallback, RemoteCallback resultCallback) { - } - - @Override - public void notifyContentProviderPublishStatus(ContentProviderHolder holder, String auth, - int userId, boolean published) { - } - - @Override - public void instrumentWithoutRestart(ComponentName instrumentationName, - Bundle instrumentationArgs, IInstrumentationWatcher instrumentationWatcher, - IUiAutomationConnection instrumentationUiConnection, ApplicationInfo targetInfo) { - } - - @Override - public void updateUiTranslationState(IBinder activityToken, int state, - TranslationSpec sourceSpec, TranslationSpec targetSpec, List<AutofillId> viewIds, - UiTranslationSpec uiTranslationSpec) { - } - } } 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/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/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/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/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index 16760e26b3f1..a7fa2d936940 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -96,7 +96,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen ActivityEmbeddingComponent { static final String TAG = "SplitController"; static final boolean ENABLE_SHELL_TRANSITIONS = - SystemProperties.getBoolean("persist.wm.debug.shell_transit", false); + SystemProperties.getBoolean("persist.wm.debug.shell_transit", true); @VisibleForTesting @GuardedBy("mLock") diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml index 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/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/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/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/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index 56d51bda762f..c6f31c23ff25 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -80,7 +80,7 @@ public class Transitions implements RemoteCallable<Transitions> { /** Set to {@code true} to enable shell transitions. */ public static final boolean ENABLE_SHELL_TRANSITIONS = - SystemProperties.getBoolean("persist.wm.debug.shell_transit", false); + SystemProperties.getBoolean("persist.wm.debug.shell_transit", true); public static final boolean SHELL_TRANSITIONS_ROTATION = ENABLE_SHELL_TRANSITIONS && SystemProperties.getBoolean("persist.wm.debug.shell_transit_rotate", false); diff --git a/libs/WindowManager/Shell/tests/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/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/splitscreen/StageTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java index 5ee8bf3006a3..1a1bebd28aef 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java @@ -61,7 +61,7 @@ import org.mockito.MockitoAnnotations; @RunWith(AndroidJUnit4.class) public final class StageTaskListenerTests extends ShellTestCase { private static final boolean ENABLE_SHELL_TRANSITIONS = - SystemProperties.getBoolean("persist.wm.debug.shell_transit", false); + SystemProperties.getBoolean("persist.wm.debug.shell_transit", true); @Mock private ShellTaskOrganizer mTaskOrganizer; diff --git a/libs/input/PointerController.cpp b/libs/input/PointerController.cpp index 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/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/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..584d0ba2ea8c 100644 --- a/native/android/libandroid.map.txt +++ b/native/android/libandroid.map.txt @@ -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/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/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java index e67ea7e66bc2..5d1e1644a5b4 100644 --- a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java +++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java @@ -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..c2ad2c56079b 100644 --- a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiver.java +++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiver.java @@ -173,7 +173,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 +204,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); 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 25fa34b418ab..2f6d1b4bea02 100644 --- a/packages/CredentialManager/res/values/strings.xml +++ b/packages/CredentialManager/res/values/strings.xml @@ -21,14 +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="createOptionInfo_icon_description">CreateOptionInfo credentialType icon</string> + <string name="another_device">Another device</string> + <string name="other_password_manager">Other password manager</string> + <!-- TODO: Check the wording here. --> + <string name="confirm_default_or_use_once_description">This password manager will store your passwords and passkeys to help you easily sign in.</string> <!-- Spoken content description of an element which will close the sheet when clicked. --> <string name="close_sheet">"Close sheet"</string> <!-- Spoken content description of the back arrow button. --> @@ -51,6 +54,12 @@ <string name="get_dialog_sign_in_type_username_separator" translatable="false">" - "</string> <!-- Modal bottom sheet title for displaying all the available sign-in options. [CHAR LIMIT=80] --> <string name="get_dialog_title_sign_in_options">Sign-in options</string> - <!-- Column heading for displaying sign-ins for a specific username. [CHAR LIMIT=20] --> + <!-- Column heading for displaying sign-ins for a specific username. [CHAR LIMIT=80] --> <string name="get_dialog_heading_for_username">For <xliff:g id="username" example="becket@gmail.com">%1$s</xliff:g></string> + <!-- Column heading for displaying locked (that is, the user needs to first authenticate via pin, fingerprint, faceId, etc.) sign-ins. [CHAR LIMIT=80] --> + <string name="get_dialog_heading_locked_password_managers">Locked password managers</string> + <!-- Explanatory sub/body text for an option entry to use a locked (that is, the user needs to first authenticate via pin, fingerprint, faceId, etc.) sign-in. [CHAR LIMIT=120] --> + <string name="locked_credential_entry_label_subtext">Tap to unlock</string> + <!-- Column heading for displaying action chips for managing sign-ins from each credential provider. [CHAR LIMIT=80] --> + <string name="get_dialog_heading_manage_sign_ins">Manage sign-ins</string> </resources>
\ No newline at end of file diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt index 2099a235a3e8..8bd7cf03008b 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt @@ -28,6 +28,7 @@ import android.credentials.ui.Constants import android.credentials.ui.Entry import android.credentials.ui.CreateCredentialProviderData import android.credentials.ui.GetCredentialProviderData +import android.credentials.ui.DisabledProviderData import android.credentials.ui.ProviderData import android.credentials.ui.RequestInfo import android.credentials.ui.BaseDialogResult @@ -39,7 +40,7 @@ import android.os.ResultReceiver import com.android.credentialmanager.createflow.ActiveEntry import com.android.credentialmanager.createflow.CreatePasskeyUiState import com.android.credentialmanager.createflow.CreateScreenState -import com.android.credentialmanager.createflow.ProviderInfo +import com.android.credentialmanager.createflow.EnabledProviderInfo import com.android.credentialmanager.createflow.RequestDisplayInfo import com.android.credentialmanager.getflow.GetCredentialUiState import com.android.credentialmanager.getflow.GetScreenState @@ -51,7 +52,8 @@ class CredentialManagerRepo( intent: Intent, ) { private val requestInfo: RequestInfo - private val providerList: List<ProviderData> + private val providerEnabledList: List<ProviderData> + private val providerDisabledList: List<DisabledProviderData> // TODO: require non-null. val resultReceiver: ResultReceiver? @@ -61,16 +63,16 @@ class CredentialManagerRepo( RequestInfo::class.java ) ?: testCreateRequestInfo() - providerList = when (requestInfo.type) { + providerEnabledList = when (requestInfo.type) { RequestInfo.TYPE_CREATE -> intent.extras?.getParcelableArrayList( ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, CreateCredentialProviderData::class.java - ) ?: testCreateCredentialProviderList() + ) ?: testCreateCredentialEnabledProviderList() RequestInfo.TYPE_GET -> intent.extras?.getParcelableArrayList( ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, - GetCredentialProviderData::class.java + DisabledProviderData::class.java ) ?: testGetCredentialProviderList() else -> { // TODO: fail gracefully @@ -78,6 +80,12 @@ class CredentialManagerRepo( } } + providerDisabledList = + intent.extras?.getParcelableArrayList( + ProviderData.EXTRA_DISABLED_PROVIDER_DATA_LIST, + DisabledProviderData::class.java + ) ?: testDisabledProviderList() + resultReceiver = intent.getParcelableExtra( Constants.EXTRA_RESULT_RECEIVER, ResultReceiver::class.java @@ -103,25 +111,28 @@ class CredentialManagerRepo( } fun getCredentialInitialUiState(): GetCredentialUiState { - val providerList = GetFlowUtils.toProviderList( + val providerEnabledList = GetFlowUtils.toProviderList( // TODO: handle runtime cast error - providerList as List<GetCredentialProviderData>, context) + providerEnabledList as List<GetCredentialProviderData>, context) // TODO: covert from real requestInfo val requestDisplayInfo = com.android.credentialmanager.getflow.RequestDisplayInfo("tribank") return GetCredentialUiState( - providerList, + providerEnabledList, GetScreenState.PRIMARY_SELECTION, requestDisplayInfo, ) } fun createPasskeyInitialUiState(): CreatePasskeyUiState { - val providerList = CreateFlowUtils.toProviderList( + val providerEnabledList = CreateFlowUtils.toEnabledProviderList( + // Handle runtime cast error + providerEnabledList as List<CreateCredentialProviderData>, context) + val providerDisabledList = CreateFlowUtils.toDisabledProviderList( // Handle runtime cast error - providerList as List<CreateCredentialProviderData>, context) + providerDisabledList as List<DisabledProviderData>, context) var hasDefault = false - var defaultProvider: ProviderInfo = providerList.first() - providerList.forEach{providerInfo -> providerInfo.createOptions = + var defaultProvider: EnabledProviderInfo = providerEnabledList.first() + providerEnabledList.forEach{providerInfo -> providerInfo.createOptions = providerInfo.createOptions.sortedWith(compareBy { it.lastUsedTimeMillis }).reversed() if (providerInfo.isDefault) {hasDefault = true; defaultProvider = providerInfo} } // TODO: covert from real requestInfo @@ -131,7 +142,8 @@ class CredentialManagerRepo( TYPE_PUBLIC_KEY_CREDENTIAL, "tribank") return CreatePasskeyUiState( - providers = providerList, + enabledProviders = providerEnabledList, + disabledProviders = providerDisabledList, if (hasDefault) {CreateScreenState.CREATION_OPTION_SELECTION} else {CreateScreenState.PASSKEY_INTRO}, requestDisplayInfo, @@ -157,10 +169,10 @@ class CredentialManagerRepo( } // TODO: below are prototype functionalities. To be removed for productionization. - private fun testCreateCredentialProviderList(): List<CreateCredentialProviderData> { + 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", @@ -169,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", @@ -185,9 +200,16 @@ class CredentialManagerRepo( ) } + private fun testDisabledProviderList(): List<DisabledProviderData> { + return listOf( + 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( @@ -203,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( @@ -216,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, @@ -289,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 5c7956423469..33fb154f44f0 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt @@ -17,14 +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 @@ -36,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), ) @@ -82,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( @@ -94,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(), + ) + } } } } @@ -103,18 +140,42 @@ class GetFlowUtils { class CreateFlowUtils { companion object { - fun toProviderList( + fun toEnabledProviderList( providerDataList: List<CreateCredentialProviderData>, context: Context, - ): List<com.android.credentialmanager.createflow.ProviderInfo> { + ): List<com.android.credentialmanager.createflow.EnabledProviderInfo> { + // TODO: get from the actual service info + val packageManager = context.packageManager return providerDataList.map { - com.android.credentialmanager.createflow.ProviderInfo( - // TODO: replace to extract from the service data structure when available - icon = context.getDrawable(R.drawable.ic_passkey)!!, + val pkgInfo = packageManager + .getPackageInfo(it.providerFlattenedComponentName, + PackageManager.PackageInfoFlags.of(0)) + com.android.credentialmanager.createflow.EnabledProviderInfo( + // TODO: decide what to do when failed to load a provider icon + icon = pkgInfo.applicationInfo.loadIcon(packageManager)!!, name = it.providerFlattenedComponentName, - displayName = it.providerFlattenedComponentName, + displayName = pkgInfo.applicationInfo.loadLabel(packageManager).toString(), createOptions = toCreationOptionInfoList(it.saveEntries, context), isDefault = it.isDefaultProvider, + remoteEntry = toRemoteInfo(it.remoteEntry), + ) + } + } + + fun toDisabledProviderList( + 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( + icon = pkgInfo.applicationInfo.loadIcon(packageManager)!!, + name = it.providerFlattenedComponentName, + displayName = pkgInfo.applicationInfo.loadLabel(packageManager).toString(), ) } } @@ -142,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 21190e7dc8c4..123c3d454905 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt @@ -18,13 +18,26 @@ package com.android.credentialmanager.createflow import android.graphics.drawable.Drawable -data class ProviderInfo( +open class ProviderInfo( val icon: Drawable, val name: String, val displayName: String, +) + +class EnabledProviderInfo( + icon: Drawable, + name: String, + displayName: String, var createOptions: List<CreateOptionInfo>, val isDefault: Boolean, -) + var remoteEntry: RemoteInfo?, +) : ProviderInfo(icon, name, displayName) + +class DisabledProviderInfo( + icon: Drawable, + name: String, + displayName: String, +) : ProviderInfo(icon, name, displayName) open class EntryInfo ( val entryKey: String, @@ -43,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, @@ -55,7 +73,7 @@ data class RequestDisplayInfo( * user selects a different entry on the more option page. */ data class ActiveEntry ( - val activeProvider: ProviderInfo, + val activeProvider: EnabledProviderInfo, val activeEntryInfo: EntryInfo, ) diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt index 437e7b213620..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,8 @@ 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 import androidx.compose.ui.Alignment @@ -59,7 +61,7 @@ fun CreatePasskeyScreen( onCancel = viewModel::onCancel, ) CreateScreenState.PROVIDER_SELECTION -> ProviderSelectionCard( - providerList = uiState.providers, + enabledProviderList = uiState.enabledProviders, onCancel = viewModel::onCancel, onProviderSelected = viewModel::onProviderSelected ) @@ -70,14 +72,17 @@ fun CreatePasskeyScreen( onOptionSelected = viewModel::onPrimaryCreateOptionInfoSelected, onConfirm = viewModel::onPrimaryCreateOptionInfoSelected, onCancel = viewModel::onCancel, - multiProvider = uiState.providers.size > 1, + multiProvider = uiState.enabledProviders.size > 1, onMoreOptionsSelected = viewModel::onMoreOptionsSelected ) CreateScreenState.MORE_OPTIONS_SELECTION -> MoreOptionsSelectionCard( requestDisplayInfo = uiState.requestDisplayInfo, - providerList = uiState.providers, + enabledProviderList = uiState.enabledProviders, + disabledProviderList = uiState.disabledProviders, onBackButtonSelected = viewModel::onBackButtonSelected, - onOptionSelected = viewModel::onMoreOptionsRowSelected + onOptionSelected = viewModel::onMoreOptionsRowSelected, + onDisabledPasswordManagerSelected = viewModel::onDisabledPasswordManagerSelected, + onRemoteEntrySelected = viewModel::onRemoteEntrySelected ) CreateScreenState.MORE_OPTIONS_ROW_INTRO -> MoreOptionsRowIntroCard( providerInfo = uiState.activeEntry?.activeProvider!!, @@ -153,7 +158,7 @@ fun ConfirmationCard( @OptIn(ExperimentalMaterial3Api::class) @Composable fun ProviderSelectionCard( - providerList: List<ProviderInfo>, + enabledProviderList: List<EnabledProviderInfo>, onProviderSelected: (String) -> Unit, onCancel: () -> Unit ) { @@ -182,7 +187,7 @@ fun ProviderSelectionCard( LazyColumn( verticalArrangement = Arrangement.spacedBy(2.dp) ) { - providerList.forEach { + enabledProviderList.forEach { item { ProviderRow(providerInfo = it, onProviderSelected = onProviderSelected) } @@ -212,9 +217,12 @@ fun ProviderSelectionCard( @Composable fun MoreOptionsSelectionCard( requestDisplayInfo: RequestDisplayInfo, - providerList: List<ProviderInfo>, + enabledProviderList: List<EnabledProviderInfo>, + disabledProviderList: List<DisabledProviderInfo>?, onBackButtonSelected: () -> Unit, - onOptionSelected: (ActiveEntry) -> Unit + onOptionSelected: (ActiveEntry) -> Unit, + onDisabledPasswordManagerSelected: () -> Unit, + onRemoteEntrySelected: () -> Unit, ) { Card() { Column() { @@ -250,18 +258,39 @@ fun MoreOptionsSelectionCard( LazyColumn( verticalArrangement = Arrangement.spacedBy(2.dp) ) { - providerList.forEach { providerInfo -> - providerInfo.createOptions.forEach { createOptionInfo -> + enabledProviderList.forEach { enabledProviderInfo -> + enabledProviderInfo.createOptions.forEach { createOptionInfo -> item { MoreOptionsInfoRow( - providerInfo = providerInfo, + providerInfo = enabledProviderInfo, createOptionInfo = createOptionInfo, onOptionSelected = { - onOptionSelected(ActiveEntry(providerInfo, createOptionInfo)) + onOptionSelected(ActiveEntry(enabledProviderInfo, createOptionInfo)) }) } } } + 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, + ) + } + } } } Divider( @@ -276,14 +305,26 @@ fun MoreOptionsSelectionCard( @OptIn(ExperimentalMaterial3Api::class) @Composable fun MoreOptionsRowIntroCard( - providerInfo: ProviderInfo, + providerInfo: EnabledProviderInfo, onDefaultOrNotSelected: () -> Unit, ) { 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( @@ -347,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) @@ -366,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, @@ -460,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 = { @@ -483,7 +520,7 @@ fun PrimaryCreateOptionRow( @OptIn(ExperimentalMaterial3Api::class) @Composable fun MoreOptionsInfoRow( - providerInfo: ProviderInfo, + providerInfo: EnabledProviderInfo, createOptionInfo: CreateOptionInfo, onOptionSelected: () -> Unit ) { @@ -491,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 = { @@ -501,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( @@ -517,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( @@ -527,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( @@ -537,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 @@ -546,4 +585,68 @@ fun MoreOptionsInfoRow( } } ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MoreOptionsDisabledProvidersRow( + disabledProviders: List<ProviderInfo>, + onDisabledPasswordManagerSelected: () -> Unit, +) { + SuggestionChip( + modifier = Modifier.fillMaxWidth(), + onClick = onDisabledPasswordManagerSelected, + icon = { + Icon( + Icons.Filled.Add, + contentDescription = null, + modifier = Modifier.padding(start = 16.dp) + ) + }, + shape = MaterialTheme.shapes.large, + label = { + Column() { + Text( + text = stringResource(R.string.other_password_manager), + style = MaterialTheme.typography.titleLarge, + 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, 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) + ) + } + } + ) }
\ No newline at end of file diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt index 2e9758aece33..af74b8ea4de1 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt @@ -28,7 +28,8 @@ import com.android.credentialmanager.common.DialogResult import com.android.credentialmanager.common.ResultState data class CreatePasskeyUiState( - val providers: List<ProviderInfo>, + val enabledProviders: List<EnabledProviderInfo>, + val disabledProviders: List<DisabledProviderInfo>? = null, val currentScreenState: CreateScreenState, val requestDisplayInfo: RequestDisplayInfo, val activeEntry: ActiveEntry? = null, @@ -50,15 +51,15 @@ class CreatePasskeyViewModel( } fun onConfirmIntro() { - if (uiState.providers.size > 1) { + if (uiState.enabledProviders.size > 1) { uiState = uiState.copy( currentScreenState = CreateScreenState.PROVIDER_SELECTION ) - } else if (uiState.providers.size == 1){ + } else if (uiState.enabledProviders.size == 1){ uiState = uiState.copy( currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION, - activeEntry = ActiveEntry(uiState.providers.first(), - uiState.providers.first().createOptions.first()) + activeEntry = ActiveEntry(uiState.enabledProviders.first(), + uiState.enabledProviders.first().createOptions.first()) ) } else { throw java.lang.IllegalStateException("Empty provider list.") @@ -73,8 +74,8 @@ class CreatePasskeyViewModel( ) } - fun getProviderInfoByName(providerName: String): ProviderInfo { - return uiState.providers.single { + fun getProviderInfoByName(providerName: String): EnabledProviderInfo { + return uiState.enabledProviders.single { it.name.equals(providerName) } } @@ -98,6 +99,14 @@ class CreatePasskeyViewModel( ) } + fun onDisabledPasswordManagerSelected() { + // 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/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 f1a24aff4319..50cab84e0d9d 100644 --- a/packages/SettingsLib/Spa/gallery/AndroidManifest.xml +++ b/packages/SettingsLib/Spa/gallery/AndroidManifest.xml @@ -35,8 +35,8 @@ </activity> <provider - android:name=".GalleryEntryProvider" - android:authorities="com.android.spa.gallery.provider" + android:name="com.android.settingslib.spa.framework.SpaSearchProvider" + android:authorities="com.android.spa.gallery.search.provider" android:enabled="true" android:exported="false"> </provider> @@ -51,7 +51,7 @@ </activity> <provider android:name="com.android.settingslib.spa.framework.debug.DebugProvider" - android:authorities="com.android.spa.gallery.debug" + android:authorities="com.android.spa.gallery.debug.provider" android:enabled="true" android:exported="false"> </provider> 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 742e2712e41a..016b27f0c82c 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 @@ -80,7 +80,7 @@ class GallerySpaEnvironment(context: Context) : SpaEnvironment(context) { override val browseActivityClass = GalleryMainActivity::class.java - override val entryProviderAuthorities = "com.android.spa.gallery.provider" + override val searchProviderAuthorities = "com.android.spa.gallery.search.provider" override val logger = LocalLogger() } diff --git a/packages/SettingsLib/Spa/spa/build.gradle b/packages/SettingsLib/Spa/spa/build.gradle index 7a20c747249b..84988b0539cf 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 { diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/EntryProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSearchProvider.kt index 38f41bc44a89..35b9c0fbd16b 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/EntryProvider.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSearchProvider.kt @@ -33,7 +33,7 @@ import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory import com.android.settingslib.spa.framework.common.addUri import com.android.settingslib.spa.framework.common.getColumns -private const val TAG = "EntryProvider" +private const val TAG = "SpaSearchProvider" /** * The content provider to return entry related data, which can be used for search and hierarchy. @@ -47,7 +47,7 @@ private const val TAG = "EntryProvider" * $ adb shell content query --uri content://<AuthorityPath>/search_mutable_status * $ adb shell content query --uri content://<AuthorityPath>/search_immutable_status */ -open class EntryProvider : ContentProvider() { +class SpaSearchProvider : ContentProvider() { private val spaEnvironment get() = SpaEnvironmentFactory.instance private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH) 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 b83104360260..a9cb041cc198 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 @@ -64,7 +64,7 @@ abstract class SpaEnvironment(context: Context) { open val browseActivityClass: Class<out Activity>? = null - open val entryProviderAuthorities: String? = null + open val searchProviderAuthorities: String? = null open val logger: SpaLogger = object : SpaLogger {} 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/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/gallery/src/com/android/settingslib/spa/gallery/GalleryEntryProvider.kt b/packages/SettingsLib/Spa/testutils/src/SpaTest.kt index 817c209fbcdc..a397bb48103c 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryEntryProvider.kt +++ b/packages/SettingsLib/Spa/testutils/src/SpaTest.kt @@ -14,8 +14,20 @@ * limitations under the License. */ -package com.android.settingslib.spa.gallery +package com.android.settingslib.spa.testutils -import com.android.settingslib.spa.framework.EntryProvider +import java.util.concurrent.TimeoutException -class GalleryEntryProvider : EntryProvider() +/** + * 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/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/template/app/AppList.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt index 9d6b311a33cb..7f5fe9f75d51 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt @@ -16,6 +16,9 @@ package com.android.settingslib.spaprivileged.template.app +import android.content.Intent +import android.content.IntentFilter +import android.os.UserHandle import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.LazyColumn @@ -33,6 +36,7 @@ import com.android.settingslib.spa.framework.compose.rememberLazyListStateAndHid import com.android.settingslib.spa.framework.compose.toState import com.android.settingslib.spa.widget.ui.PlaceholderTitle import com.android.settingslib.spaprivileged.R +import com.android.settingslib.spaprivileged.framework.compose.DisposableBroadcastReceiverAsUser import com.android.settingslib.spaprivileged.model.app.AppListConfig import com.android.settingslib.spaprivileged.model.app.AppListData import com.android.settingslib.spaprivileged.model.app.AppListModel @@ -120,5 +124,15 @@ 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/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/res/drawable/ic_5g_plus_mobiledata_default.xml b/packages/SettingsLib/res/drawable/ic_5g_plus_mobiledata_default.xml new file mode 100644 index 000000000000..46abff81d38f --- /dev/null +++ b/packages/SettingsLib/res/drawable/ic_5g_plus_mobiledata_default.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:viewportWidth="22" + android:viewportHeight="17" + android:width="22dp" + android:height="17dp"> + <group> + <group> + <path android:fillColor="#FF000000" + android:pathData="M1.03 8.47l0.43-4.96h4.33v1.17H2.48L2.25 7.39C2.66 7.1 3.1 6.96 3.57 6.96c0.77 0 1.38 0.3 1.83 0.9 s0.66 1.41 0.66 2.43c0 1.03-0.24 1.84-0.72 2.43S4.2 13.6 3.36 13.6c-0.75 0-1.36-0.24-1.83-0.73s-0.74-1.16-0.81-2.02h1.13 c0.07 0.57 0.23 1 0.49 1.29s0.59 0.43 1.01 0.43c0.47 0 0.84-0.2 1.1-0.61c0.26-0.41 0.4-0.96 0.4-1.65 c0-0.65-0.14-1.18-0.43-1.59S3.76 8.09 3.28 8.09c-0.4 0-0.72 0.1-0.96 0.31L1.99 8.73L1.03 8.47z"/> + </group> + <group> + <path android:fillColor="#FF000000" + android:pathData="M 18.93,5.74 L 18.93,3.39 L 17.63,3.39 L 17.63,5.74 L 15.28,5.74 L 15.28,7.04 L 17.63,7.04 L 17.63,9.39 L 18.93,9.39 L 18.93,7.04 L 21.28,7.04 L 21.28,5.74 z"/> + </group> + <path android:fillColor="#FF000000" + android:pathData="M13.78 12.24l-0.22 0.27c-0.63 0.73-1.55 1.1-2.76 1.1c-1.08 0-1.92-0.36-2.53-1.07s-0.93-1.72-0.94-3.02V7.56 c0-1.39 0.28-2.44 0.84-3.13s1.39-1.04 2.51-1.04c0.95 0 1.69 0.26 2.23 0.79s0.83 1.28 0.89 2.26h-1.25 c-0.05-0.62-0.22-1.1-0.52-1.45s-0.74-0.52-1.34-0.52c-0.72 0-1.24 0.23-1.57 0.7S8.6 6.37 8.59 7.4v2.03c0 1 0.19 1.77 0.57 2.31 c0.38 0.54 0.93 0.8 1.65 0.8c0.67 0 1.19-0.16 1.54-0.49l0.18-0.17V9.59h-1.82V8.52h3.07V12.24z"/> + </group> +</vector> diff --git a/packages/SettingsLib/res/values/carrierid_icon_overrides.xml b/packages/SettingsLib/res/values/carrierid_icon_overrides.xml new file mode 100644 index 000000000000..d2ae52d8347a --- /dev/null +++ b/packages/SettingsLib/res/values/carrierid_icon_overrides.xml @@ -0,0 +1,32 @@ +<?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. + --> +<!-- + ~ This resource file exists to enumerate all network type icon overrides on a + ~ per-carrierId basis +--> +<resources> + <!-- + Network type (RAT) icon overrides can be configured here on a per-carrierId basis. + 1. Add a new TypedArray here, using the naming scheme below + 2. The entries are (NetworkType, drawable ID) pairs + 3. Add this array's ID to the MAPPING field of MobileIconCarrierIdOverrides.kt + --> + <array name="carrierId_2032_iconOverrides"> + <item>5G_PLUS</item> + <item>@drawable/ic_5g_plus_mobiledata_default</item> + </array> +</resources>
\ No newline at end of file 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/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/src/com/android/settingslib/mobile/MobileIconCarrierIdOverrides.kt b/packages/SettingsLib/src/com/android/settingslib/mobile/MobileIconCarrierIdOverrides.kt new file mode 100644 index 000000000000..a0395b559291 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/mobile/MobileIconCarrierIdOverrides.kt @@ -0,0 +1,142 @@ +/* + * 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.mobile + +import android.annotation.DrawableRes +import android.content.res.Resources +import android.content.res.TypedArray +import android.util.Log +import androidx.annotation.VisibleForTesting +import com.android.settingslib.R +import com.android.settingslib.SignalIcon.MobileIconGroup + +/** + * This class defines a network type (3G, 4G, etc.) override mechanism on a per-carrierId basis. + * + * Traditionally, carrier-customized network type iconography was achieved using the `MCC/MNC` + * resource qualifiers, and swapping out the drawable resource by name. It would look like this: + * + * res/ + * drawable/ + * 3g_mobiledata_icon.xml + * drawable-MCC-MNC/ + * 3g_mobiledata_icon.xml + * + * This would mean that, provided a context created with this MCC/MNC configuration set, loading + * the network type icon through [MobileIconGroup] would provide a carrier-defined network type + * icon rather than the AOSP-defined default. + * + * The MCC/MNC mechanism no longer can fully define carrier-specific network type icons, because + * there is no longer a 1:1 mapping between MCC/MNC and carrier. With the advent of MVNOs, multiple + * carriers can have the same MCC/MNC value, but wish to differentiate based on their carrier ID. + * CarrierId is a newer concept than MCC/MNC, and provides more granularity when it comes to + * determining the carrier (e.g. MVNOs can share MCC/MNC values with the network owner), therefore + * it can fit all of the same use cases currently handled by `MCC/MNC`, without the need to apply a + * configuration context in order to get the proper UI for a given SIM icon. + * + * NOTE: CarrierId icon overrides will always take precedence over those defined using `MCC/MNC` + * resource qualifiers. + * + * [MAPPING] encodes the relationship between CarrierId and the corresponding override array + * that exists in the config.xml. An alternative approach could be to generate the resource name + * by string concatenation at run-time: + * + * val resName = "carrierId_$carrierId_iconOverrides" + * val override = resources.getResourceIdentifier(resName) + * + * However, that's going to be far less efficient until MAPPING grows to a sufficient size. For now, + * given a relatively small number of entries, we should just maintain the mapping here. + */ +interface MobileIconCarrierIdOverrides { + @DrawableRes + fun getOverrideFor(carrierId: Int, networkType: String, resources: Resources): Int + fun carrierIdEntryExists(carrierId: Int): Boolean +} + +class MobileIconCarrierIdOverridesImpl : MobileIconCarrierIdOverrides { + @DrawableRes + override fun getOverrideFor(carrierId: Int, networkType: String, resources: Resources): Int { + val resId = MAPPING[carrierId] ?: return 0 + val ta = resources.obtainTypedArray(resId) + val map = parseNetworkIconOverrideTypedArray(ta) + ta.recycle() + return map[networkType] ?: 0 + } + + override fun carrierIdEntryExists(carrierId: Int) = + overrideExists(carrierId, MAPPING) + + companion object { + private const val TAG = "MobileIconOverrides" + /** + * This map maintains the lookup from the canonical carrier ID (see below link) to the + * corresponding overlay resource. New overrides should add an entry below in order to + * change the network type icon resources based on carrier ID + * + * Refer to the link below for the canonical mapping maintained in AOSP: + * https://android.googlesource.com/platform/packages/providers/TelephonyProvider/+/master/assets/latest_carrier_id/carrier_list.textpb + */ + private val MAPPING = mapOf( + // 2032 == Xfinity Mobile + 2032 to R.array.carrierId_2032_iconOverrides, + ) + + /** + * Parse `carrierId_XXXX_iconOverrides` for a particular network type. The resource file + * "carrierid_icon_overrides.xml" defines a TypedArray format for overriding specific + * network type icons (a.k.a. RAT icons) for a particular carrier ID. The format is defined + * as an array of (network type name, drawable) pairs: + * <array name="carrierId_XXXX_iconOverrides> + * <item>NET_TYPE_1</item> + * <item>@drawable/net_type_1_override</item> + * <item>NET_TYPE_2</item> + * <item>@drawable/net_type_2_override</item> + * </array> + * + * @param ta the [TypedArray] defined in carrierid_icon_overrides.xml + * @return the overridden drawable resource ID if it exists, or 0 if it does not + */ + @VisibleForTesting + @JvmStatic + fun parseNetworkIconOverrideTypedArray(ta: TypedArray): Map<String, Int> { + if (ta.length() % 2 != 0) { + Log.w(TAG, + "override must contain an even number of (key, value) entries. skipping") + + return mapOf() + } + + val result = mutableMapOf<String, Int>() + // The array is defined as Pair(String, resourceId), so walk by 2 + for (i in 0 until ta.length() step 2) { + val key = ta.getString(i) + val override = ta.getResourceId(i + 1, 0) + if (key == null || override == 0) { + Log.w(TAG, "Invalid override found. Skipping") + continue + } + result[key] = override + } + + return result + } + + @JvmStatic + private fun overrideExists(carrierId: Int, mapping: Map<Int, Int>): Boolean = + mapping.containsKey(carrierId) + } +} 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/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/SettingsLib/tests/robotests/src/com/android/settingslib/mobile/MobileIconCarrierIdOverridesTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/mobile/MobileIconCarrierIdOverridesTest.java new file mode 100644 index 000000000000..740261d3bac2 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/mobile/MobileIconCarrierIdOverridesTest.java @@ -0,0 +1,140 @@ +/* + * 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.mobile; + +import static com.android.settingslib.mobile.MobileIconCarrierIdOverridesImpl.parseNetworkIconOverrideTypedArray; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.content.res.TypedArray; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import java.util.Map; + +@RunWith(RobolectricTestRunner.class) +public final class MobileIconCarrierIdOverridesTest { + private static final String OVERRIDE_ICON_1_NAME = "name_1"; + private static final int OVERRIDE_ICON_1_RES = 1; + + private static final String OVERRIDE_ICON_2_NAME = "name_2"; + private static final int OVERRIDE_ICON_2_RES = 2; + + NetworkOverrideTypedArrayMock mResourceMock; + + @Before + public void setUp() { + mResourceMock = new NetworkOverrideTypedArrayMock( + new String[] { OVERRIDE_ICON_1_NAME, OVERRIDE_ICON_2_NAME }, + new int[] { OVERRIDE_ICON_1_RES, OVERRIDE_ICON_2_RES } + ); + } + + @Test + public void testParse_singleOverride() { + mResourceMock.setOverrides( + new String[] { OVERRIDE_ICON_1_NAME }, + new int[] { OVERRIDE_ICON_1_RES } + ); + + Map<String, Integer> parsed = parseNetworkIconOverrideTypedArray(mResourceMock.getMock()); + + assertThat(parsed.get(OVERRIDE_ICON_1_NAME)).isEqualTo(OVERRIDE_ICON_1_RES); + } + + @Test + public void testParse_multipleOverrides() { + mResourceMock.setOverrides( + new String[] { OVERRIDE_ICON_1_NAME, OVERRIDE_ICON_2_NAME }, + new int[] { OVERRIDE_ICON_1_RES, OVERRIDE_ICON_2_RES } + ); + + Map<String, Integer> parsed = parseNetworkIconOverrideTypedArray(mResourceMock.getMock()); + + assertThat(parsed.get(OVERRIDE_ICON_2_NAME)).isEqualTo(OVERRIDE_ICON_2_RES); + assertThat(parsed.get(OVERRIDE_ICON_1_NAME)).isEqualTo(OVERRIDE_ICON_1_RES); + } + + @Test + public void testParse_nonexistentKey_isNull() { + mResourceMock.setOverrides( + new String[] { OVERRIDE_ICON_1_NAME }, + new int[] { OVERRIDE_ICON_1_RES } + ); + + Map<String, Integer> parsed = parseNetworkIconOverrideTypedArray(mResourceMock.getMock()); + + assertThat(parsed.get(OVERRIDE_ICON_2_NAME)).isNull(); + } + + static class NetworkOverrideTypedArrayMock { + private Object[] mInterleaved; + + private final TypedArray mMockTypedArray = mock(TypedArray.class); + + NetworkOverrideTypedArrayMock( + String[] networkTypes, + int[] iconOverrides) { + + mInterleaved = interleaveTypes(networkTypes, iconOverrides); + + doAnswer(invocation -> { + return mInterleaved[(int) invocation.getArgument(0)]; + }).when(mMockTypedArray).getString(/* index */ anyInt()); + + doAnswer(invocation -> { + return mInterleaved[(int) invocation.getArgument(0)]; + }).when(mMockTypedArray).getResourceId(/* index */ anyInt(), /* default */ anyInt()); + + when(mMockTypedArray.length()).thenAnswer(invocation -> { + return mInterleaved.length; + }); + } + + TypedArray getMock() { + return mMockTypedArray; + } + + void setOverrides(String[] types, int[] resIds) { + mInterleaved = interleaveTypes(types, resIds); + } + + private Object[] interleaveTypes(String[] strs, int[] ints) { + assertThat(strs.length).isEqualTo(ints.length); + + Object[] ret = new Object[strs.length * 2]; + + // Keep track of where we are in the interleaved array, but iterate the overrides + int interleavedIndex = 0; + for (int i = 0; i < strs.length; i++) { + ret[interleavedIndex] = strs[i]; + interleavedIndex += 1; + ret[interleavedIndex] = ints[i]; + interleavedIndex += 1; + } + return ret; + } + } +} 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/res-keyguard/values-land/dimens.xml b/packages/SystemUI/res-keyguard/values-land/dimens.xml index a4e7a5f12db4..f1aa54412b3b 100644 --- a/packages/SystemUI/res-keyguard/values-land/dimens.xml +++ b/packages/SystemUI/res-keyguard/values-land/dimens.xml @@ -27,4 +27,6 @@ <integer name="scaled_password_text_size">26</integer> <dimen name="bouncer_user_switcher_y_trans">@dimen/status_bar_height</dimen> + <dimen name="bouncer_user_switcher_view_mode_user_switcher_bottom_margin">0dp</dimen> + <dimen name="bouncer_user_switcher_view_mode_view_flipper_bottom_margin">0dp</dimen> </resources> diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml index 0a55cf779683..3861d983b309 100644 --- a/packages/SystemUI/res-keyguard/values/dimens.xml +++ b/packages/SystemUI/res-keyguard/values/dimens.xml @@ -124,6 +124,8 @@ <dimen name="bouncer_user_switcher_item_padding_horizontal">12dp</dimen> <dimen name="bouncer_user_switcher_header_padding_end">44dp</dimen> <dimen name="bouncer_user_switcher_y_trans">0dp</dimen> + <dimen name="bouncer_user_switcher_view_mode_user_switcher_bottom_margin">0dp</dimen> + <dimen name="bouncer_user_switcher_view_mode_view_flipper_bottom_margin">0dp</dimen> <!-- 2 * the margin + size should equal the plus_margin --> <dimen name="user_switcher_icon_large_margin">16dp</dimen> diff --git a/packages/SystemUI/res/layout/screen_record_options.xml b/packages/SystemUI/res/layout/screen_record_options.xml index a93691434bc0..d6c9e98d8b4d 100644 --- a/packages/SystemUI/res/layout/screen_record_options.xml +++ b/packages/SystemUI/res/layout/screen_record_options.xml @@ -50,6 +50,7 @@ android:importantForAccessibility="yes"/> </LinearLayout> <LinearLayout + android:id="@+id/show_taps" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" diff --git a/packages/SystemUI/res/values-sw600dp-port/dimens.xml b/packages/SystemUI/res/values-sw600dp-port/dimens.xml index d9df3373bef1..707bc9e535ff 100644 --- a/packages/SystemUI/res/values-sw600dp-port/dimens.xml +++ b/packages/SystemUI/res/values-sw600dp-port/dimens.xml @@ -17,7 +17,6 @@ <resources> <dimen name="notification_panel_margin_horizontal">48dp</dimen> <dimen name="status_view_margin_horizontal">62dp</dimen> - <dimen name="bouncer_user_switcher_y_trans">20dp</dimen> <!-- qs_tiles_page_horizontal_margin should be margin / 2, otherwise full space between two pages is margin * 2, and that makes tiles page not appear immediately after user swipes to diff --git a/packages/SystemUI/res/values-sw720dp-h1000dp/dimens.xml b/packages/SystemUI/res/values-sw720dp-h1000dp/dimens.xml index 97ead01669a9..b98165fb08f0 100644 --- a/packages/SystemUI/res/values-sw720dp-h1000dp/dimens.xml +++ b/packages/SystemUI/res/values-sw720dp-h1000dp/dimens.xml @@ -21,4 +21,6 @@ <!-- Space between status view and notification shelf --> <dimen name="keyguard_status_view_bottom_margin">70dp</dimen> <dimen name="keyguard_clock_top_margin">80dp</dimen> + <dimen name="bouncer_user_switcher_view_mode_user_switcher_bottom_margin">186dp</dimen> + <dimen name="bouncer_user_switcher_view_mode_view_flipper_bottom_margin">110dp</dimen> </resources> diff --git a/packages/SystemUI/res/values-sw720dp-port/dimens.xml b/packages/SystemUI/res/values-sw720dp-port/dimens.xml index 17f82b50d7be..8b41a44b9ba3 100644 --- a/packages/SystemUI/res/values-sw720dp-port/dimens.xml +++ b/packages/SystemUI/res/values-sw720dp-port/dimens.xml @@ -21,7 +21,6 @@ for different hardware and product builds. --> <resources> <dimen name="status_view_margin_horizontal">124dp</dimen> - <dimen name="bouncer_user_switcher_y_trans">200dp</dimen> <dimen name="large_screen_shade_header_left_padding">24dp</dimen> <dimen name="qqs_layout_padding_bottom">40dp</dimen> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index ad45471419a8..f9f2195984f4 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1496,10 +1496,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 0e48760b7e27..f49105ca913b 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,7 +405,7 @@ <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=25] --> + <!-- 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> <!-- Content description of the bluetooth icon when connected for accessibility (not shown on the screen). [CHAR LIMIT=NONE] --> diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderClient.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderClient.kt new file mode 100644 index 000000000000..8612b3a2c587 --- /dev/null +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderClient.kt @@ -0,0 +1,326 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.shared.keyguard.data.content + +import android.annotation.SuppressLint +import android.content.ContentValues +import android.content.Context +import android.database.ContentObserver +import android.graphics.drawable.Drawable +import android.net.Uri +import android.os.UserHandle +import androidx.annotation.DrawableRes +import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract as Contract +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.withContext + +/** Collection of utility functions for using a content provider implementing the [Contract]. */ +object KeyguardQuickAffordanceProviderClient { + + /** + * Selects an affordance with the given ID for a slot on the lock screen with the given ID. + * + * Note that the maximum number of selected affordances on this slot is automatically enforced. + * Selecting a slot that is already full (e.g. already has a number of selected affordances at + * its maximum capacity) will automatically remove the oldest selected affordance before adding + * the one passed in this call. Additionally, selecting an affordance that's already one of the + * selected affordances on the slot will move the selected affordance to the newest location in + * the slot. + */ + suspend fun insertSelection( + context: Context, + slotId: String, + affordanceId: String, + dispatcher: CoroutineDispatcher = Dispatchers.IO, + ) { + withContext(dispatcher) { + context.contentResolver.insert( + Contract.SelectionTable.URI, + ContentValues().apply { + put(Contract.SelectionTable.Columns.SLOT_ID, slotId) + put(Contract.SelectionTable.Columns.AFFORDANCE_ID, affordanceId) + } + ) + } + } + + /** Returns all available slots supported by the device. */ + suspend fun querySlots( + context: Context, + dispatcher: CoroutineDispatcher = Dispatchers.IO, + ): List<Slot> { + return withContext(dispatcher) { + context.contentResolver + .query( + Contract.SlotTable.URI, + null, + null, + null, + null, + ) + ?.use { cursor -> + buildList { + val idColumnIndex = cursor.getColumnIndex(Contract.SlotTable.Columns.ID) + val capacityColumnIndex = + cursor.getColumnIndex(Contract.SlotTable.Columns.CAPACITY) + if (idColumnIndex == -1 || capacityColumnIndex == -1) { + return@buildList + } + + while (cursor.moveToNext()) { + add( + Slot( + id = cursor.getString(idColumnIndex), + capacity = cursor.getInt(capacityColumnIndex), + ) + ) + } + } + } + } + ?: emptyList() + } + + /** + * Returns [Flow] for observing the collection of slots. + * + * @see [querySlots] + */ + fun observeSlots( + context: Context, + dispatcher: CoroutineDispatcher = Dispatchers.IO, + ): Flow<List<Slot>> { + return observeUri( + context, + Contract.SlotTable.URI, + ) + .map { querySlots(context, dispatcher) } + } + + /** + * Returns all available affordances supported by the device, regardless of current slot + * placement. + */ + suspend fun queryAffordances( + context: Context, + dispatcher: CoroutineDispatcher = Dispatchers.IO, + ): List<Affordance> { + return withContext(dispatcher) { + context.contentResolver + .query( + Contract.AffordanceTable.URI, + null, + null, + null, + null, + ) + ?.use { cursor -> + buildList { + val idColumnIndex = + cursor.getColumnIndex(Contract.AffordanceTable.Columns.ID) + val nameColumnIndex = + cursor.getColumnIndex(Contract.AffordanceTable.Columns.NAME) + val iconColumnIndex = + cursor.getColumnIndex(Contract.AffordanceTable.Columns.ICON) + if (idColumnIndex == -1 || nameColumnIndex == -1 || iconColumnIndex == -1) { + return@buildList + } + + while (cursor.moveToNext()) { + add( + Affordance( + id = cursor.getString(idColumnIndex), + name = cursor.getString(nameColumnIndex), + iconResourceId = cursor.getInt(iconColumnIndex), + ) + ) + } + } + } + } + ?: emptyList() + } + + /** + * Returns [Flow] for observing the collection of affordances. + * + * @see [queryAffordances] + */ + fun observeAffordances( + context: Context, + dispatcher: CoroutineDispatcher = Dispatchers.IO, + ): Flow<List<Affordance>> { + return observeUri( + context, + Contract.AffordanceTable.URI, + ) + .map { queryAffordances(context, dispatcher) } + } + + /** Returns the current slot-affordance selections. */ + suspend fun querySelections( + context: Context, + dispatcher: CoroutineDispatcher = Dispatchers.IO, + ): List<Selection> { + return withContext(dispatcher) { + context.contentResolver + .query( + Contract.SelectionTable.URI, + null, + null, + null, + null, + ) + ?.use { cursor -> + buildList { + val slotIdColumnIndex = + cursor.getColumnIndex(Contract.SelectionTable.Columns.SLOT_ID) + val affordanceIdColumnIndex = + cursor.getColumnIndex(Contract.SelectionTable.Columns.AFFORDANCE_ID) + if (slotIdColumnIndex == -1 || affordanceIdColumnIndex == -1) { + return@buildList + } + + while (cursor.moveToNext()) { + add( + Selection( + slotId = cursor.getString(slotIdColumnIndex), + affordanceId = cursor.getString(affordanceIdColumnIndex), + ) + ) + } + } + } + } + ?: emptyList() + } + + /** + * Returns [Flow] for observing the collection of selections. + * + * @see [querySelections] + */ + fun observeSelections( + context: Context, + dispatcher: CoroutineDispatcher = Dispatchers.IO, + ): Flow<List<Selection>> { + return observeUri( + context, + Contract.SelectionTable.URI, + ) + .map { querySelections(context, dispatcher) } + } + + /** Unselects an affordance with the given ID from the slot with the given ID. */ + suspend fun deleteSelection( + context: Context, + slotId: String, + affordanceId: String, + dispatcher: CoroutineDispatcher = Dispatchers.IO, + ) { + withContext(dispatcher) { + context.contentResolver.delete( + Contract.SelectionTable.URI, + "${Contract.SelectionTable.Columns.SLOT_ID} = ? AND" + + " ${Contract.SelectionTable.Columns.AFFORDANCE_ID} = ?", + arrayOf( + slotId, + affordanceId, + ), + ) + } + } + + /** Unselects all affordances from the slot with the given ID. */ + suspend fun deleteAllSelections( + context: Context, + slotId: String, + dispatcher: CoroutineDispatcher = Dispatchers.IO, + ) { + withContext(dispatcher) { + context.contentResolver.delete( + Contract.SelectionTable.URI, + "${Contract.SelectionTable.Columns.SLOT_ID}", + arrayOf( + slotId, + ), + ) + } + } + + private fun observeUri( + context: Context, + uri: Uri, + ): Flow<Unit> { + return callbackFlow { + val observer = + object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + trySend(Unit) + } + } + + context.contentResolver.registerContentObserver( + uri, + /* notifyForDescendants= */ true, + observer, + UserHandle.USER_CURRENT, + ) + + awaitClose { context.contentResolver.unregisterContentObserver(observer) } + } + .onStart { emit(Unit) } + } + + @SuppressLint("UseCompatLoadingForDrawables") + suspend fun getAffordanceIcon( + context: Context, + @DrawableRes iconResourceId: Int, + dispatcher: CoroutineDispatcher = Dispatchers.IO, + ): Drawable { + return withContext(dispatcher) { + context.packageManager + .getResourcesForApplication(SYSTEM_UI_PACKAGE_NAME) + .getDrawable(iconResourceId) + } + } + + data class Slot( + val id: String, + val capacity: Int, + ) + + data class Affordance( + val id: String, + val name: String, + val iconResourceId: Int, + ) + + data class Selection( + val slotId: String, + val affordanceId: String, + ) + + private const val SYSTEM_UI_PACKAGE_NAME = "com.android.systemui" +} diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderContract.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderContract.kt new file mode 100644 index 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/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java index 40c8774d4f34..a790d89ac1ae 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java @@ -61,26 +61,42 @@ public class PreviewPositionHelper { * Updates the matrix based on the provided parameters */ public void updateThumbnailMatrix(Rect thumbnailBounds, ThumbnailData thumbnailData, - int canvasWidth, int canvasHeight, int screenWidthPx, int taskbarSize, boolean isTablet, + int canvasWidth, int canvasHeight, int screenWidthPx, int screenHeightPx, + int taskbarSize, boolean isTablet, int currentRotation, boolean isRtl) { boolean isRotated = false; boolean isOrientationDifferent; - float fullscreenTaskWidth = screenWidthPx; - if (mSplitBounds != null && !mSplitBounds.appsStackedVertically) { - // For landscape, scale the width - float taskPercent = mDesiredStagePosition == STAGE_POSITION_TOP_OR_LEFT - ? mSplitBounds.leftTaskPercent - : (1 - (mSplitBounds.leftTaskPercent + mSplitBounds.dividerWidthPercent)); - // Scale landscape width to that of actual screen - fullscreenTaskWidth = screenWidthPx * taskPercent; - } int thumbnailRotation = thumbnailData.rotation; int deltaRotate = getRotationDelta(currentRotation, thumbnailRotation); RectF thumbnailClipHint = new RectF(); - float canvasScreenRatio = canvasWidth / fullscreenTaskWidth; - float scaledTaskbarSize = taskbarSize * canvasScreenRatio; - thumbnailClipHint.bottom = isTablet ? scaledTaskbarSize : 0; + + float scaledTaskbarSize = 0; + if (mSplitBounds != null) { + float fullscreenTaskWidth; + float fullscreenTaskHeight; + float canvasScreenRatio; + + float taskPercent; + if (!mSplitBounds.appsStackedVertically) { + // For landscape, scale the width + taskPercent = mDesiredStagePosition == STAGE_POSITION_TOP_OR_LEFT + ? mSplitBounds.leftTaskPercent + : (1 - (mSplitBounds.leftTaskPercent + mSplitBounds.dividerWidthPercent)); + // Scale landscape width to that of actual screen + fullscreenTaskWidth = screenWidthPx * taskPercent; + canvasScreenRatio = canvasWidth / fullscreenTaskWidth; + } else { + taskPercent = mDesiredStagePosition != STAGE_POSITION_TOP_OR_LEFT + ? mSplitBounds.leftTaskPercent + : (1 - (mSplitBounds.leftTaskPercent + mSplitBounds.dividerWidthPercent)); + // Scale landscape width to that of actual screen + fullscreenTaskHeight = screenHeightPx * taskPercent; + canvasScreenRatio = canvasHeight / fullscreenTaskHeight; + } + scaledTaskbarSize = taskbarSize * canvasScreenRatio; + thumbnailClipHint.bottom = isTablet ? scaledTaskbarSize : 0; + } float scale = thumbnailData.scale; final float thumbnailScale; 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/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java index 73229c321079..faaba63938bf 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java @@ -21,6 +21,7 @@ import android.annotation.Nullable; import android.content.res.ColorStateList; import android.content.res.Resources; import android.telephony.TelephonyManager; +import android.text.TextUtils; import android.util.Log; import android.view.inputmethod.InputMethodManager; @@ -152,7 +153,9 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> } public void startAppearAnimation() { - mMessageAreaController.setMessage(getInitialMessageResId()); + if (TextUtils.isEmpty(mMessageAreaController.getMessage())) { + mMessageAreaController.setMessage(getInitialMessageResId()); + } mView.startAppearAnimation(); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java index 2bd3ca59b740..db986e0a631a 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java @@ -103,6 +103,11 @@ public class KeyguardMessageAreaController<T extends KeyguardMessageArea> mView.setNextMessageColor(colorState); } + /** Returns the message of the underlying TextView. */ + public CharSequence getMessage() { + return mView.getText(); + } + /** * Reload colors from resources. **/ diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java index 2bb3a5f437f5..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); } @@ -1097,11 +1106,19 @@ public class KeyguardSecurityContainer extends ConstraintLayout { new KeyguardSecurityViewTransition()); } int yTrans = mResources.getDimensionPixelSize(R.dimen.bouncer_user_switcher_y_trans); + int viewFlipperBottomMargin = mResources.getDimensionPixelSize( + R.dimen.bouncer_user_switcher_view_mode_view_flipper_bottom_margin); + int userSwitcherBottomMargin = mResources.getDimensionPixelSize( + R.dimen.bouncer_user_switcher_view_mode_user_switcher_bottom_margin); if (mResources.getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) { ConstraintSet constraintSet = new ConstraintSet(); constraintSet.connect(mUserSwitcherViewGroup.getId(), TOP, PARENT_ID, TOP, yTrans); - constraintSet.connect(mViewFlipper.getId(), TOP, PARENT_ID, TOP); - constraintSet.connect(mViewFlipper.getId(), BOTTOM, PARENT_ID, BOTTOM); + constraintSet.connect(mUserSwitcherViewGroup.getId(), BOTTOM, mViewFlipper.getId(), + TOP, userSwitcherBottomMargin); + constraintSet.connect(mViewFlipper.getId(), TOP, mUserSwitcherViewGroup.getId(), + BOTTOM); + constraintSet.connect(mViewFlipper.getId(), BOTTOM, PARENT_ID, BOTTOM, + viewFlipperBottomMargin); constraintSet.centerHorizontally(mViewFlipper.getId(), PARENT_ID); constraintSet.centerHorizontally(mUserSwitcherViewGroup.getId(), PARENT_ID); constraintSet.setVerticalChainStyle(mViewFlipper.getId(), CHAIN_SPREAD); @@ -1137,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..2ac93b51b147 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -470,19 +470,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 +481,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); } } } 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/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..45f9385a2620 100644 --- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java +++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java @@ -170,6 +170,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 +385,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( @@ -1022,7 +1024,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 +1036,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/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/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/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 99dfefa4fa41..c4cc3389a4f2 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -146,8 +146,10 @@ object Flags { // TODO(b/255607168): Tracking Bug @JvmField val DOZING_MIGRATION_1 = unreleasedFlag(213, "dozing_migration_1") + // TODO(b/252897742): Tracking Bug @JvmField val NEW_ELLIPSE_DETECTION = unreleasedFlag(214, "new_ellipse_detection") + // TODO(b/252897742): Tracking Bug @JvmField val NEW_UDFPS_OVERLAY = unreleasedFlag(215, "new_udfps_overlay") /** @@ -160,6 +162,10 @@ object Flags { val CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES = unreleasedFlag(216, "customizable_lock_screen_quick_affordances", teamfood = false) + /** Shows chipbar UI whenever the device is unlocked by ActiveUnlock (watch). */ + // TODO(b/240196500): Tracking Bug + @JvmField val ACTIVE_UNLOCK_CHIPBAR = unreleasedFlag(217, "active_unlock_chipbar") + // 300 - power menu // TODO(b/254512600): Tracking Bug @JvmField val POWER_MENU_LITE = releasedFlag(300, "power_menu_lite") @@ -295,7 +301,7 @@ object Flags { @Keep @JvmField val WM_ENABLE_SHELL_TRANSITIONS = - sysPropBooleanFlag(1100, "persist.wm.debug.shell_transit", default = false) + sysPropBooleanFlag(1100, "persist.wm.debug.shell_transit", default = true) // TODO(b/254513207): Tracking Bug @Keep 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/data/repository/KeyguardBouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt index d4514c5cb7aa..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 @@ -16,16 +16,17 @@ package com.android.systemui.keyguard.data.repository -import android.hardware.biometrics.BiometricSourceType import com.android.keyguard.KeyguardUpdateMonitor -import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.keyguard.ViewMediatorCallback import com.android.systemui.dagger.SysUISingleton 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. */ @@ -70,33 +71,19 @@ constructor( private val _keyguardAuthenticated = MutableStateFlow<Boolean?>(null) /** Determines if user is already unlocked */ val keyguardAuthenticated = _keyguardAuthenticated.asStateFlow() - - var bouncerPromptReason: Int? = null - 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 + get() = viewMediatorCallback.bouncerPromptReason val bouncerErrorMessage: CharSequence? get() = viewMediatorCallback.consumeCustomMessage() - init { - val callback = - object : KeyguardUpdateMonitorCallback() { - override fun onStrongAuthStateChanged(userId: Int) { - bouncerPromptReason = viewMediatorCallback.bouncerPromptReason - } - - override fun onLockedOutStateChanged(type: BiometricSourceType) { - if (type == BiometricSourceType.FINGERPRINT) { - bouncerPromptReason = viewMediatorCallback.bouncerPromptReason - } - } - } - - keyguardUpdateMonitor.registerCallback(callback) - } - fun setPrimaryScrimmed(isScrimmed: Boolean) { _primaryBouncerScrimmed.value = isScrimmed } @@ -138,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/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt index ca25282ec2f0..9d5d8bbd4f40 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.data.repository import com.android.keyguard.KeyguardUpdateMonitor +import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.common.shared.model.Position @@ -69,6 +70,9 @@ interface KeyguardRepository { */ val isKeyguardShowing: Flow<Boolean> + /** Observable for the signal that keyguard is about to go away. */ + val isKeyguardGoingAway: Flow<Boolean> + /** Observable for whether the bouncer is showing. */ val isBouncerShowing: Flow<Boolean> @@ -85,6 +89,14 @@ interface KeyguardRepository { val isDozing: Flow<Boolean> /** + * Observable for whether the device is dreaming. + * + * Dozing/AOD is a specific type of dream, but it is also possible for other non-systemui dreams + * to be active, such as screensavers. + */ + val isDreaming: Flow<Boolean> + + /** * Observable for the amount of doze we are currently in. * * While in doze state, this amount can change - driving a cycle of animations designed to avoid @@ -136,12 +148,12 @@ interface KeyguardRepository { class KeyguardRepositoryImpl @Inject constructor( - statusBarStateController: StatusBarStateController, - dozeHost: DozeHost, - wakefulnessLifecycle: WakefulnessLifecycle, - biometricUnlockController: BiometricUnlockController, - private val keyguardStateController: KeyguardStateController, - private val keyguardUpdateMonitor: KeyguardUpdateMonitor, + statusBarStateController: StatusBarStateController, + dozeHost: DozeHost, + wakefulnessLifecycle: WakefulnessLifecycle, + biometricUnlockController: BiometricUnlockController, + private val keyguardStateController: KeyguardStateController, + private val keyguardUpdateMonitor: KeyguardUpdateMonitor, ) : KeyguardRepository { private val _animateBottomAreaDozingTransitions = MutableStateFlow(false) override val animateBottomAreaDozingTransitions = @@ -176,6 +188,29 @@ constructor( awaitClose { keyguardStateController.removeCallback(callback) } } + override val isKeyguardGoingAway: Flow<Boolean> = conflatedCallbackFlow { + val callback = + object : KeyguardStateController.Callback { + override fun onKeyguardGoingAwayChanged() { + trySendWithFailureLogging( + keyguardStateController.isKeyguardGoingAway, + TAG, + "updated isKeyguardGoingAway" + ) + } + } + + keyguardStateController.addCallback(callback) + // Adding the callback does not send an initial update. + trySendWithFailureLogging( + keyguardStateController.isKeyguardGoingAway, + TAG, + "initial isKeyguardGoingAway" + ) + + awaitClose { keyguardStateController.removeCallback(callback) } + } + override val isBouncerShowing: Flow<Boolean> = conflatedCallbackFlow { val callback = object : KeyguardStateController.Callback { @@ -218,6 +253,25 @@ constructor( } .distinctUntilChanged() + override val isDreaming: Flow<Boolean> = + conflatedCallbackFlow { + val callback = + object : KeyguardUpdateMonitorCallback() { + override fun onDreamingStateChanged(isDreaming: Boolean) { + trySendWithFailureLogging(isDreaming, TAG, "updated isDreaming") + } + } + keyguardUpdateMonitor.registerCallback(callback) + trySendWithFailureLogging( + keyguardUpdateMonitor.isDreaming, + TAG, + "initial isDreaming", + ) + + awaitClose { keyguardUpdateMonitor.removeCallback(callback) } + } + .distinctUntilChanged() + override val dozeAmount: Flow<Float> = conflatedCallbackFlow { val callback = object : StatusBarStateController.StateListener { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt index e3d1a27dad2b..bce7d92cd8fb 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt @@ -94,11 +94,13 @@ class KeyguardTransitionRepositoryImpl @Inject constructor() : KeyguardTransitio */ private val _transitions = MutableSharedFlow<TransitionStep>( + replay = 2, extraBufferCapacity = 10, - onBufferOverflow = BufferOverflow.DROP_OLDEST + onBufferOverflow = BufferOverflow.DROP_OLDEST, ) override val transitions = _transitions.asSharedFlow().distinctUntilChanged() private var lastStep: TransitionStep = TransitionStep() + private var lastAnimator: ValueAnimator? = null /* * When manual control of the transition is requested, a unique [UUID] is used as the handle @@ -106,19 +108,39 @@ class KeyguardTransitionRepositoryImpl @Inject constructor() : KeyguardTransitio */ private var updateTransitionId: UUID? = null + init { + // Seed with transitions signaling a boot into lockscreen state + emitTransition( + TransitionStep( + KeyguardState.OFF, + KeyguardState.LOCKSCREEN, + 0f, + TransitionState.STARTED, + ) + ) + emitTransition( + TransitionStep( + KeyguardState.OFF, + KeyguardState.LOCKSCREEN, + 1f, + TransitionState.FINISHED, + ) + ) + } + override fun startTransition(info: TransitionInfo): UUID? { if (lastStep.transitionState != TransitionState.FINISHED) { - // Open questions: - // * Queue of transitions? buffer of 1? - // * Are transitions cancellable if a new one is triggered? - // * What validation does this need to do? - Log.wtf(TAG, "Transition still active: $lastStep") - return null + Log.i(TAG, "Transition still active: $lastStep, canceling") } + val startingValue = 1f - lastStep.value + lastAnimator?.cancel() + lastAnimator = info.animator + info.animator?.let { animator -> // An animator was provided, so use it to run the transition - animator.setFloatValues(0f, 1f) + animator.setFloatValues(startingValue, 1f) + animator.duration = ((1f - startingValue) * animator.duration).toLong() val updateListener = object : AnimatorUpdateListener { override fun onAnimationUpdate(animation: ValueAnimator) { @@ -134,15 +156,24 @@ class KeyguardTransitionRepositoryImpl @Inject constructor() : KeyguardTransitio val adapter = object : AnimatorListenerAdapter() { override fun onAnimationStart(animation: Animator) { - emitTransition(TransitionStep(info, 0f, TransitionState.STARTED)) + emitTransition(TransitionStep(info, startingValue, TransitionState.STARTED)) } override fun onAnimationCancel(animation: Animator) { - Log.i(TAG, "Cancelling transition: $info") + endAnimation(animation, lastStep.value, TransitionState.CANCELED) } override fun onAnimationEnd(animation: Animator) { - emitTransition(TransitionStep(info, 1f, TransitionState.FINISHED)) + endAnimation(animation, 1f, TransitionState.FINISHED) + } + + private fun endAnimation( + animation: Animator, + value: Float, + state: TransitionState + ) { + emitTransition(TransitionStep(info, value, state)) animator.removeListener(this) animator.removeUpdateListener(updateListener) + lastAnimator = null } } animator.addListener(adapter) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt index 0aeff7fc69fd..e5521c76705d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt @@ -20,10 +20,11 @@ import android.animation.ValueAnimator import com.android.systemui.animation.Interpolators import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionInfo +import com.android.systemui.keyguard.shared.model.WakefulnessModel.Companion.isSleepingOrStartingToSleep +import com.android.systemui.keyguard.shared.model.WakefulnessModel.Companion.isWakingOrStartingToWake import com.android.systemui.util.kotlin.sample import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -35,18 +36,30 @@ class AodLockscreenTransitionInteractor @Inject constructor( @Application private val scope: CoroutineScope, - private val keyguardRepository: KeyguardRepository, + private val keyguardInteractor: KeyguardInteractor, private val keyguardTransitionRepository: KeyguardTransitionRepository, private val keyguardTransitionInteractor: KeyguardTransitionInteractor, ) : TransitionInteractor("AOD<->LOCKSCREEN") { override fun start() { scope.launch { - keyguardRepository.isDozing - .sample(keyguardTransitionInteractor.finishedKeyguardState, { a, b -> Pair(a, b) }) + /* + * Listening to the startedKeyguardTransitionStep (last started step) allows this code + * to interrupt an active transition, as long as they were either going to LOCKSCREEN or + * AOD state. One example is when the user presses the power button in the middle of an + * active transition. + */ + keyguardInteractor.wakefulnessState + .sample( + keyguardTransitionInteractor.startedKeyguardTransitionStep, + { a, b -> Pair(a, b) } + ) .collect { pair -> - val (isDozing, keyguardState) = pair - if (isDozing && keyguardState == KeyguardState.LOCKSCREEN) { + val (wakefulnessState, lastStartedStep) = pair + if ( + isSleepingOrStartingToSleep(wakefulnessState) && + lastStartedStep.to == KeyguardState.LOCKSCREEN + ) { keyguardTransitionRepository.startTransition( TransitionInfo( name, @@ -55,7 +68,10 @@ constructor( getAnimator(), ) ) - } else if (!isDozing && keyguardState == KeyguardState.AOD) { + } else if ( + isWakingOrStartingToWake(wakefulnessState) && + lastStartedStep.to == KeyguardState.AOD + ) { keyguardTransitionRepository.startTransition( TransitionInfo( name, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerToGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerToGoneTransitionInteractor.kt new file mode 100644 index 000000000000..dd2967334307 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerToGoneTransitionInteractor.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.keyguard.domain.interactor + +import android.animation.ValueAnimator +import com.android.systemui.animation.Interpolators +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionInfo +import com.android.systemui.shade.data.repository.ShadeRepository +import com.android.systemui.util.kotlin.sample +import java.util.UUID +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +@SysUISingleton +class BouncerToGoneTransitionInteractor +@Inject +constructor( + @Application private val scope: CoroutineScope, + private val keyguardInteractor: KeyguardInteractor, + private val shadeRepository: ShadeRepository, + private val keyguardTransitionRepository: KeyguardTransitionRepository, + private val keyguardTransitionInteractor: KeyguardTransitionInteractor +) : TransitionInteractor("BOUNCER->GONE") { + + private var transitionId: UUID? = null + + override fun start() { + listenForKeyguardGoingAway() + } + + private fun listenForKeyguardGoingAway() { + scope.launch { + keyguardInteractor.isKeyguardGoingAway + .sample(keyguardTransitionInteractor.finishedKeyguardState, { a, b -> Pair(a, b) }) + .collect { pair -> + val (isKeyguardGoingAway, keyguardState) = pair + if (isKeyguardGoingAway && keyguardState == KeyguardState.BOUNCER) { + keyguardTransitionRepository.startTransition( + TransitionInfo( + ownerName = name, + from = KeyguardState.BOUNCER, + to = KeyguardState.GONE, + animator = getAnimator(), + ) + ) + } + } + } + } + + private fun getAnimator(): ValueAnimator { + return ValueAnimator().apply { + setInterpolator(Interpolators.LINEAR) + setDuration(TRANSITION_DURATION_MS) + } + } + + companion object { + private const val TRANSITION_DURATION_MS = 300L + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingLockscreenTransitionInteractor.kt new file mode 100644 index 000000000000..c44cda42c68d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingLockscreenTransitionInteractor.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.keyguard.domain.interactor + +import android.animation.ValueAnimator +import com.android.systemui.animation.Interpolators +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionInfo +import com.android.systemui.util.kotlin.sample +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +@SysUISingleton +class DreamingLockscreenTransitionInteractor +@Inject +constructor( + @Application private val scope: CoroutineScope, + private val keyguardInteractor: KeyguardInteractor, + private val keyguardTransitionRepository: KeyguardTransitionRepository, + private val keyguardTransitionInteractor: KeyguardTransitionInteractor, +) : TransitionInteractor("DREAMING<->LOCKSCREEN") { + + override fun start() { + scope.launch { + keyguardInteractor.isDreaming + .sample(keyguardTransitionInteractor.finishedKeyguardState, { a, b -> Pair(a, b) }) + .collect { pair -> + val (isDreaming, keyguardState) = pair + if (isDreaming && keyguardState == KeyguardState.LOCKSCREEN) { + keyguardTransitionRepository.startTransition( + TransitionInfo( + name, + KeyguardState.LOCKSCREEN, + KeyguardState.DREAMING, + getAnimator(), + ) + ) + } else if (!isDreaming && keyguardState == KeyguardState.DREAMING) { + keyguardTransitionRepository.startTransition( + TransitionInfo( + name, + KeyguardState.DREAMING, + KeyguardState.LOCKSCREEN, + getAnimator(), + ) + ) + } + } + } + } + + private fun getAnimator(): ValueAnimator { + return ValueAnimator().apply { + setInterpolator(Interpolators.LINEAR) + setDuration(TRANSITION_DURATION_MS) + } + } + + companion object { + private const val TRANSITION_DURATION_MS = 500L + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingToAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingToAodTransitionInteractor.kt new file mode 100644 index 000000000000..9e2b7241ade2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingToAodTransitionInteractor.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.keyguard.domain.interactor + +import android.animation.ValueAnimator +import com.android.systemui.animation.Interpolators +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionInfo +import com.android.systemui.keyguard.shared.model.WakefulnessModel.Companion.isSleepingOrStartingToSleep +import com.android.systemui.util.kotlin.sample +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +@SysUISingleton +class DreamingToAodTransitionInteractor +@Inject +constructor( + @Application private val scope: CoroutineScope, + private val keyguardInteractor: KeyguardInteractor, + private val keyguardTransitionRepository: KeyguardTransitionRepository, + private val keyguardTransitionInteractor: KeyguardTransitionInteractor, +) : TransitionInteractor("DREAMING->AOD") { + + override fun start() { + scope.launch { + keyguardInteractor.wakefulnessState + .sample(keyguardTransitionInteractor.finishedKeyguardState, { a, b -> Pair(a, b) }) + .collect { pair -> + val (wakefulnessState, keyguardState) = pair + if ( + isSleepingOrStartingToSleep(wakefulnessState) && + keyguardState == KeyguardState.DREAMING + ) { + keyguardTransitionRepository.startTransition( + TransitionInfo( + name, + KeyguardState.DREAMING, + KeyguardState.AOD, + getAnimator(), + ) + ) + } + } + } + } + + private fun getAnimator(): ValueAnimator { + return ValueAnimator().apply { + setInterpolator(Interpolators.LINEAR) + setDuration(TRANSITION_DURATION_MS) + } + } + + companion object { + private const val TRANSITION_DURATION_MS = 300L + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt index 614ff8d930d8..5a1c70264ee4 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt @@ -41,8 +41,15 @@ constructor( val dozeAmount: Flow<Float> = repository.dozeAmount /** Whether the system is in doze mode. */ val isDozing: Flow<Boolean> = repository.isDozing + /** + * Whether the system is dreaming. [isDreaming] will be always be true when [isDozing] is true, + * but not vice-versa. + */ + val isDreaming: Flow<Boolean> = repository.isDreaming /** Whether the keyguard is showing or not. */ val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing + /** Whether the keyguard is going away. */ + val isKeyguardGoingAway: Flow<Boolean> = repository.isKeyguardGoingAway /** Whether the bouncer is showing or not. */ val isBouncerShowing: Flow<Boolean> = repository.isBouncerShowing /** The device wake/sleep state */ diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt index 57fb4a114700..58a8093d49d2 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt @@ -41,12 +41,24 @@ constructor( } scope.launch { + keyguardInteractor.isBouncerShowing.collect { logger.v("Bouncer showing", it) } + } + + scope.launch { keyguardInteractor.isDozing.collect { logger.v("isDozing", it) } } + + scope.launch { interactor.finishedKeyguardTransitionStep.collect { logger.i("Finished transition", it) } } scope.launch { + interactor.canceledKeyguardTransitionStep.collect { + logger.i("Canceled transition", it) + } + } + + scope.launch { interactor.startedKeyguardTransitionStep.collect { logger.i("Started transition", it) } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt index a7c6d4450336..43dd358e4808 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt @@ -42,6 +42,9 @@ constructor( is GoneAodTransitionInteractor -> Log.d(TAG, "Started $it") is LockscreenGoneTransitionInteractor -> Log.d(TAG, "Started $it") is AodToGoneTransitionInteractor -> Log.d(TAG, "Started $it") + is BouncerToGoneTransitionInteractor -> Log.d(TAG, "Started $it") + is DreamingLockscreenTransitionInteractor -> Log.d(TAG, "Started $it") + is DreamingToAodTransitionInteractor -> Log.d(TAG, "Started $it") } it.start() } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt index 749183e241e5..54a4f493d21d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt @@ -57,6 +57,14 @@ constructor( lockscreenToAodTransition, ) + /* The last [TransitionStep] with a [TransitionState] of STARTED */ + val startedKeyguardTransitionStep: Flow<TransitionStep> = + repository.transitions.filter { step -> step.transitionState == TransitionState.STARTED } + + /* The last [TransitionStep] with a [TransitionState] of CANCELED */ + val canceledKeyguardTransitionStep: Flow<TransitionStep> = + repository.transitions.filter { step -> step.transitionState == TransitionState.CANCELED } + /* The last [TransitionStep] with a [TransitionState] of FINISHED */ val finishedKeyguardTransitionStep: Flow<TransitionStep> = repository.transitions.filter { step -> step.transitionState == TransitionState.FINISHED } @@ -64,8 +72,4 @@ constructor( /* The last completed [KeyguardState] transition */ val finishedKeyguardState: Flow<KeyguardState> = finishedKeyguardTransitionStep.map { step -> step.to } - - /* The last [TransitionStep] with a [TransitionState] of STARTED */ - val startedKeyguardTransitionStep: Flow<TransitionStep> = - repository.transitions.filter { step -> step.transitionState == TransitionState.STARTED } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt index fd4814d2bc94..cca2d566556e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt @@ -56,10 +56,20 @@ constructor( private fun listenForBouncerHiding() { scope.launch { keyguardInteractor.isBouncerShowing - .sample(keyguardInteractor.wakefulnessState, { a, b -> Pair(a, b) }) - .collect { pair -> - val (isBouncerShowing, wakefulnessState) = pair - if (!isBouncerShowing) { + .sample( + combine( + keyguardInteractor.wakefulnessState, + keyguardTransitionInteractor.startedKeyguardTransitionStep, + ) { a, b -> + Pair(a, b) + }, + { a, bc -> Triple(a, bc.first, bc.second) } + ) + .collect { triple -> + val (isBouncerShowing, wakefulnessState, lastStartedTransitionStep) = triple + if ( + !isBouncerShowing && lastStartedTransitionStep.to == KeyguardState.BOUNCER + ) { val to = if ( wakefulnessState == WakefulnessModel.STARTING_TO_SLEEP || @@ -90,10 +100,10 @@ constructor( combine( keyguardTransitionInteractor.finishedKeyguardState, keyguardInteractor.statusBarState, - ) { keyguardState, statusBarState -> - Pair(keyguardState, statusBarState) + ) { a, b -> + Pair(a, b) }, - { shadeModel, pair -> Triple(shadeModel, pair.first, pair.second) } + { a, bc -> Triple(a, bc.first, bc.second) } ) .collect { triple -> val (shadeModel, keyguardState, statusBarState) = triple @@ -116,8 +126,7 @@ constructor( ) } else { // TODO (b/251849525): Remove statusbarstate check when that state is - // integrated - // into KeyguardTransitionRepository + // integrated into KeyguardTransitionRepository if ( keyguardState == KeyguardState.LOCKSCREEN && shadeModel.isUserDragging && diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenGoneTransitionInteractor.kt index 6c1adbd68ef2..4100f7a8413a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenGoneTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenGoneTransitionInteractor.kt @@ -23,6 +23,7 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionInfo +import com.android.systemui.util.kotlin.sample import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collect @@ -34,23 +35,27 @@ class LockscreenGoneTransitionInteractor constructor( @Application private val scope: CoroutineScope, private val keyguardInteractor: KeyguardInteractor, + private val keyguardTransitionInteractor: KeyguardTransitionInteractor, private val keyguardTransitionRepository: KeyguardTransitionRepository, ) : TransitionInteractor("LOCKSCREEN->GONE") { override fun start() { scope.launch { - keyguardInteractor.isKeyguardShowing.collect { isShowing -> - if (!isShowing) { - keyguardTransitionRepository.startTransition( - TransitionInfo( - name, - KeyguardState.LOCKSCREEN, - KeyguardState.GONE, - getAnimator(), + keyguardInteractor.isKeyguardGoingAway + .sample(keyguardTransitionInteractor.finishedKeyguardState, { a, b -> Pair(a, b) }) + .collect { pair -> + val (isKeyguardGoingAway, keyguardState) = pair + if (!isKeyguardGoingAway && keyguardState == KeyguardState.LOCKSCREEN) { + keyguardTransitionRepository.startTransition( + TransitionInfo( + name, + KeyguardState.LOCKSCREEN, + KeyguardState.GONE, + getAnimator(), + ) ) - ) + } } - } } } 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 c22f4e7a2634..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) @@ -210,9 +209,12 @@ constructor( expansion == KeyguardBouncer.EXPANSION_HIDDEN && oldExpansion != KeyguardBouncer.EXPANSION_HIDDEN ) { - repository.setPrimaryVisible(false) - repository.setPrimaryShow(null) - falsingCollector.onBouncerHidden() + /* + * There are cases where #hide() was not invoked, such as when + * NotificationPanelViewController controls the hide animation. Make sure the state gets + * updated by calling #hide() directly. + */ + hide() DejankUtils.postAfterTraversal { primaryBouncerCallbackInteractor.dispatchReset() } primaryBouncerCallbackInteractor.dispatchFullyHidden() } else if ( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt index 37f33afbf53e..dbffeab436a4 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt @@ -46,5 +46,19 @@ abstract class StartKeyguardTransitionModule { @Binds @IntoSet + abstract fun bouncerGone(impl: BouncerToGoneTransitionInteractor): TransitionInteractor + + @Binds + @IntoSet abstract fun lockscreenGone(impl: LockscreenGoneTransitionInteractor): TransitionInteractor + + @Binds + @IntoSet + abstract fun dreamingLockscreen( + impl: DreamingLockscreenTransitionInteractor + ): TransitionInteractor + + @Binds + @IntoSet + abstract fun dreamingToAod(impl: DreamingToAodTransitionInteractor): TransitionInteractor } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt index 7958033ba017..dd908c420fcb 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt @@ -17,12 +17,29 @@ package com.android.systemui.keyguard.shared.model /** List of all possible states to transition to/from */ enum class KeyguardState { + /* + * The display is completely off, as well as any sensors that would trigger the device to wake + * up. + */ + OFF, /** - * For initialization as well as when the security method is set to NONE, indicating that - * the keyguard should never be shown. + * The device has entered a special low-power mode within SystemUI. Doze is technically a + * special dream service implementation. No UI is visible. In this state, a least some + * low-powered sensors such as lift to wake or tap to wake are enabled, or wake screen for + * notifications is enabled, allowing the device to quickly wake up. + */ + DOZING, + /* + * A device state after the device times out, which can be from both LOCKSCREEN or GONE states. + * DOZING is an example of special version of this state. Dreams may be implemented by third + * parties to present their own UI over keyguard, like a screensaver. + */ + DREAMING, + /** + * The device has entered a special low-power mode within SystemUI, also called the Always-on + * Display (AOD). A minimal UI is presented to show critical information. If the device is in + * low-power mode without a UI, then it is DOZING. */ - NONE, - /* Always-on Display. The device is in a low-power mode with a minimal UI visible */ AOD, /* * The security screen prompt UI, containing PIN, Password, Pattern, and all FPS @@ -34,7 +51,6 @@ enum class KeyguardState { * unlocked if SWIPE security method is used, or if face lockscreen bypass is false. */ LOCKSCREEN, - /* * Keyguard is no longer visible. In most cases the user has just authenticated and keyguard * is being removed, but there are other cases where the user is swiping away keyguard, such as diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt index 0e0465bb5207..38a93b50ea97 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt @@ -17,7 +17,12 @@ package com.android.systemui.keyguard.shared.model /** Possible states for a running transition between [State] */ enum class TransitionState { + /* Transition has begun. */ STARTED, + /* Transition is actively running. */ RUNNING, - FINISHED + /* Transition has completed successfully. */ + FINISHED, + /* Transition has been interrupted, and not completed successfully. */ + CANCELED, } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt index 732a6f7b887a..767fd58f78e9 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt @@ -17,8 +17,8 @@ package com.android.systemui.keyguard.shared.model /** This information will flow from the [KeyguardTransitionRepository] to control the UI layer */ data class TransitionStep( - val from: KeyguardState = KeyguardState.NONE, - val to: KeyguardState = KeyguardState.NONE, + val from: KeyguardState = KeyguardState.OFF, + val to: KeyguardState = KeyguardState.OFF, val value: Float = 0f, // constrained [0.0, 1.0] val transitionState: TransitionState = TransitionState.FINISHED, val ownerName: String = "", diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessModel.kt index 64f834d6c5ab..92040f4f0348 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessModel.kt @@ -24,5 +24,15 @@ enum class WakefulnessModel { /** Device is now fully awake and interactive. */ AWAKE, /** Signal that the device is now going to sleep. */ - STARTING_TO_SLEEP, + STARTING_TO_SLEEP; + + companion object { + fun isSleepingOrStartingToSleep(model: WakefulnessModel): Boolean { + return model == ASLEEP || model == STARTING_TO_SLEEP + } + + fun isWakingOrStartingToWake(model: WakefulnessModel): Boolean { + return model == AWAKE || model == STARTING_TO_WAKE + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt index a22958b74bb9..7739a456fcb7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt @@ -94,6 +94,10 @@ object KeyguardBouncerViewBinder { viewModel.setBouncerViewDelegate(delegate) launch { viewModel.show.collect { + hostViewController.showPromptReason(it.promptReason) + it.errorMessage?.let { errorMessage -> + hostViewController.showErrorMessage(errorMessage) + } hostViewController.showPrimarySecurityScreen() hostViewController.appear( SystemBarUtils.getStatusBarHeight(view.context) @@ -102,18 +106,6 @@ object KeyguardBouncerViewBinder { } launch { - viewModel.showPromptReason.collect { prompt -> - hostViewController.showPromptReason(prompt) - } - } - - launch { - viewModel.showBouncerErrorMessage.collect { errorMessage -> - hostViewController.showErrorMessage(errorMessage) - } - } - - launch { viewModel.showWithFullExpansion.collect { model -> hostViewController.resetSecurityContainer() hostViewController.showPromptReason(model.promptReason) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt index 07816001f45c..526ae741793c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt @@ -26,7 +26,6 @@ import com.android.systemui.statusbar.phone.KeyguardBouncer.EXPANSION_VISIBLE import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map /** Models UI state for the lock screen bouncer; handles user input. */ @@ -45,13 +44,6 @@ constructor( /** Observe whether bouncer is showing. */ val show: Flow<KeyguardBouncerModel> = interactor.show - /** Observe bouncer prompt when bouncer is showing. */ - val showPromptReason: Flow<Int> = interactor.show.map { it.promptReason } - - /** Observe bouncer error message when bouncer is showing. */ - val showBouncerErrorMessage: Flow<CharSequence> = - interactor.show.map { it.errorMessage }.filterNotNull() - /** Observe visible expansion when bouncer is showing. */ val showWithFullExpansion: Flow<KeyguardBouncerModel> = interactor.show.filter { it.expansionAmount == EXPANSION_VISIBLE } 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/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/view/MediaProjectionTaskView.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionTaskView.kt index b682bd172837..d4991f90a86b 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionTaskView.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionTaskView.kt @@ -148,6 +148,7 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 val currentRotation: Int = display.rotation val displayWidthPx = windowMetrics.bounds.width() + val displayHeightPx = windowMetrics.bounds.height() val isRtl = layoutDirection == LAYOUT_DIRECTION_RTL val isTablet = isTablet(context) val taskbarSize = @@ -163,6 +164,7 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 measuredWidth, measuredHeight, displayWidthPx, + displayHeightPx, taskbarSize, isTablet, currentRotation, 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/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/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/screenrecord/ScreenRecordPermissionDialog.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt index cffd28f9fc96..19bb15a5c2d8 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt @@ -24,8 +24,9 @@ import android.os.Handler import android.os.Looper import android.os.ResultReceiver import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE import android.widget.AdapterView -import android.widget.AdapterView.OnItemClickListener import android.widget.ArrayAdapter import android.widget.Spinner import android.widget.Switch @@ -47,6 +48,7 @@ class ScreenRecordPermissionDialog( private val onStartRecordingClicked: Runnable? ) : BaseScreenSharePermissionDialog(context, createOptionList(), null) { private lateinit var tapsSwitch: Switch + private lateinit var tapsView: View private lateinit var audioSwitch: Switch private lateinit var options: Spinner override fun onCreate(savedInstanceState: Bundle?) { @@ -84,16 +86,25 @@ class ScreenRecordPermissionDialog( private fun initRecordOptionsView() { audioSwitch = findViewById(R.id.screenrecord_audio_switch) tapsSwitch = findViewById(R.id.screenrecord_taps_switch) + tapsView = findViewById(R.id.show_taps) + updateTapsViewVisibility() options = findViewById(R.id.screen_recording_options) val a: ArrayAdapter<*> = ScreenRecordingAdapter(context, android.R.layout.simple_spinner_dropdown_item, MODES) a.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) options.adapter = a - options.setOnItemClickListenerInt( - OnItemClickListener { _: AdapterView<*>?, _: View?, _: Int, _: Long -> - audioSwitch.isChecked = true - } - ) + options.setOnItemClickListenerInt { _: AdapterView<*>?, _: View?, _: Int, _: Long -> + audioSwitch.isChecked = true + } + } + + override fun onItemSelected(adapterView: AdapterView<*>?, view: View, pos: Int, id: Long) { + super.onItemSelected(adapterView, view, pos, id) + updateTapsViewVisibility() + } + + private fun updateTapsViewVisibility() { + tapsView.visibility = if (selectedScreenShareOption.mode == SINGLE_APP) GONE else VISIBLE } /** @@ -103,7 +114,7 @@ class ScreenRecordPermissionDialog( */ private fun requestScreenCapture(captureTarget: MediaProjectionCaptureTarget?) { val userContext = userContextProvider.userContext - val showTaps = tapsSwitch.isChecked + val showTaps = selectedScreenShareOption.mode != SINGLE_APP && tapsSwitch.isChecked val audioMode = if (audioSwitch.isChecked) options.selectedItem as ScreenRecordingAudioSource else ScreenRecordingAudioSource.NONE 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/screenshot/ScreenshotProxyService.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt index c41e2bc14afc..4cb91e134003 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt @@ -15,12 +15,17 @@ */ package com.android.systemui.screenshot -import android.app.Service import android.content.Intent import android.os.IBinder import android.util.Log +import androidx.lifecycle.LifecycleService +import androidx.lifecycle.lifecycleScope +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.shade.ShadeExpansionStateManager import com.android.systemui.statusbar.phone.CentralSurfaces +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.util.Optional import javax.inject.Inject @@ -30,7 +35,8 @@ import javax.inject.Inject internal class ScreenshotProxyService @Inject constructor( private val mExpansionMgr: ShadeExpansionStateManager, private val mCentralSurfacesOptional: Optional<CentralSurfaces>, -) : Service() { + @Main private val mMainDispatcher: CoroutineDispatcher, +) : LifecycleService() { private val mBinder: IBinder = object : IScreenshotProxy.Stub() { /** @@ -43,20 +49,28 @@ internal class ScreenshotProxyService @Inject constructor( } override fun dismissKeyguard(callback: IOnDoneCallback) { - if (mCentralSurfacesOptional.isPresent) { - mCentralSurfacesOptional.get().executeRunnableDismissingKeyguard( - Runnable { - callback.onDone(true) - }, null, - true /* dismissShade */, true /* afterKeyguardGone */, - true /* deferred */ - ) - } else { - callback.onDone(false) + lifecycleScope.launch { + executeAfterDismissing(callback) } } } + private suspend fun executeAfterDismissing(callback: IOnDoneCallback) = + withContext(mMainDispatcher) { + mCentralSurfacesOptional.ifPresentOrElse( + { + it.executeRunnableDismissingKeyguard( + Runnable { + callback.onDone(true) + }, null, + true /* dismissShade */, true /* afterKeyguardGone */, + true /* deferred */ + ) + }, + { callback.onDone(false) } + ) + } + override fun onBind(intent: Intent): IBinder? { Log.d(TAG, "onBind: $intent") return mBinder diff --git a/packages/SystemUI/src/com/android/systemui/shade/CameraLauncher.java b/packages/SystemUI/src/com/android/systemui/shade/CameraLauncher.java new file mode 100644 index 000000000000..fc61e90ab8f7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/CameraLauncher.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.systemui.shade; + +import com.android.systemui.camera.CameraGestureHelper; +import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.statusbar.phone.KeyguardBypassController; + +import javax.inject.Inject; + +/** Handles launching camera from Shade. */ +@SysUISingleton +public class CameraLauncher { + private final CameraGestureHelper mCameraGestureHelper; + private final KeyguardBypassController mKeyguardBypassController; + + private boolean mLaunchingAffordance; + + @Inject + public CameraLauncher( + CameraGestureHelper cameraGestureHelper, + KeyguardBypassController keyguardBypassController + ) { + mCameraGestureHelper = cameraGestureHelper; + mKeyguardBypassController = keyguardBypassController; + } + + /** Launches the camera. */ + public void launchCamera(int source, boolean isShadeFullyCollapsed) { + if (!isShadeFullyCollapsed) { + setLaunchingAffordance(true); + } + + mCameraGestureHelper.launchCamera(source); + } + + /** + * Set whether we are currently launching an affordance. This is currently only set when + * launched via a camera gesture. + */ + public void setLaunchingAffordance(boolean launchingAffordance) { + mLaunchingAffordance = launchingAffordance; + mKeyguardBypassController.setLaunchingAffordance(launchingAffordance); + } + + /** + * Return true when a bottom affordance is launching an occluded activity with a splash screen. + */ + public boolean isLaunchingAffordance() { + return mLaunchingAffordance; + } + + /** + * Whether the camera application can be launched for the camera launch gesture. + */ + public boolean canCameraGestureBeLaunched(int barState) { + return mCameraGestureHelper.canCameraGestureBeLaunched(barState); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 8c1b5746f61e..ceef8c8ff31c 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -124,7 +124,6 @@ import com.android.systemui.animation.ActivityLaunchAnimator; import com.android.systemui.animation.Interpolators; import com.android.systemui.animation.LaunchAnimator; import com.android.systemui.biometrics.AuthController; -import com.android.systemui.camera.CameraGestureHelper; import com.android.systemui.classifier.Classifier; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.dagger.qualifiers.DisplayId; @@ -464,7 +463,6 @@ public final class NotificationPanelViewController implements Dumpable { private boolean mCollapsedOnDown; private boolean mClosingWithAlphaFadeOut; private boolean mHeadsUpAnimatingAway; - private boolean mLaunchingAffordance; private final FalsingManager mFalsingManager; private final FalsingCollector mFalsingCollector; @@ -575,7 +573,7 @@ public final class NotificationPanelViewController implements Dumpable { /** Whether the current animator is resetting the pulse expansion after a drag down. */ private boolean mIsPulseExpansionResetAnimator; - private final Rect mKeyguardStatusAreaClipBounds = new Rect(); + private final Rect mLastQsClipBounds = new Rect(); private final Region mQsInterceptRegion = new Region(); /** Alpha of the views which only show on the keyguard but not in shade / shade locked. */ private float mKeyguardOnlyContentAlpha = 1.0f; @@ -615,7 +613,6 @@ public final class NotificationPanelViewController implements Dumpable { private final NotificationListContainer mNotificationListContainer; private final NotificationStackSizeCalculator mNotificationStackSizeCalculator; private final NPVCDownEventState.Buffer mLastDownEvents; - private final CameraGestureHelper mCameraGestureHelper; private final KeyguardBottomAreaViewModel mKeyguardBottomAreaViewModel; private final KeyguardBottomAreaInteractor mKeyguardBottomAreaInteractor; private float mMinExpandHeight; @@ -743,7 +740,6 @@ public final class NotificationPanelViewController implements Dumpable { UnlockedScreenOffAnimationController unlockedScreenOffAnimationController, ShadeTransitionController shadeTransitionController, SystemClock systemClock, - CameraGestureHelper cameraGestureHelper, KeyguardBottomAreaViewModel keyguardBottomAreaViewModel, KeyguardBottomAreaInteractor keyguardBottomAreaInteractor, DumpManager dumpManager) { @@ -924,7 +920,6 @@ public final class NotificationPanelViewController implements Dumpable { unlockAnimationStarted(playingCannedAnimation, isWakeAndUnlock, startDelay); } }); - mCameraGestureHelper = cameraGestureHelper; mKeyguardBottomAreaInteractor = keyguardBottomAreaInteractor; dumpManager.registerDumpable(this); } @@ -2814,7 +2809,7 @@ public final class NotificationPanelViewController implements Dumpable { */ private void applyQSClippingBounds(int left, int top, int right, int bottom, boolean qsVisible) { - if (!mAnimateNextNotificationBounds || mKeyguardStatusAreaClipBounds.isEmpty()) { + if (!mAnimateNextNotificationBounds || mLastQsClipBounds.isEmpty()) { if (mQsClippingAnimation != null) { // update the end position of the animator mQsClippingAnimationEndBounds.set(left, top, right, bottom); @@ -2823,10 +2818,10 @@ public final class NotificationPanelViewController implements Dumpable { } } else { mQsClippingAnimationEndBounds.set(left, top, right, bottom); - final int startLeft = mKeyguardStatusAreaClipBounds.left; - final int startTop = mKeyguardStatusAreaClipBounds.top; - final int startRight = mKeyguardStatusAreaClipBounds.right; - final int startBottom = mKeyguardStatusAreaClipBounds.bottom; + final int startLeft = mLastQsClipBounds.left; + final int startTop = mLastQsClipBounds.top; + final int startRight = mLastQsClipBounds.right; + final int startBottom = mLastQsClipBounds.bottom; if (mQsClippingAnimation != null) { mQsClippingAnimation.cancel(); } @@ -2863,12 +2858,10 @@ public final class NotificationPanelViewController implements Dumpable { private void applyQSClippingImmediately(int left, int top, int right, int bottom, boolean qsVisible) { - // Fancy clipping for quick settings int radius = mScrimCornerRadius; boolean clipStatusView = false; + mLastQsClipBounds.set(left, top, right, bottom); if (mIsFullWidth) { - // The padding on this area is large enough that we can use a cheaper clipping strategy - mKeyguardStatusAreaClipBounds.set(left, top, right, bottom); clipStatusView = qsVisible; float screenCornerRadius = mRecordingController.isRecording() ? 0 : mScreenCornerRadius; radius = (int) MathUtils.lerp(screenCornerRadius, mScrimCornerRadius, @@ -2903,8 +2896,8 @@ public final class NotificationPanelViewController implements Dumpable { radius, qsVisible && !mSplitShadeEnabled); } - mKeyguardStatusViewController.setClipBounds( - clipStatusView ? mKeyguardStatusAreaClipBounds : null); + // The padding on this area is large enough that we can use a cheaper clipping strategy + mKeyguardStatusViewController.setClipBounds(clipStatusView ? mLastQsClipBounds : null); if (!qsVisible && mSplitShadeEnabled) { // On the lockscreen when qs isn't visible, we don't want the bounds of the shade to // be visible, otherwise you can see the bounds once swiping up to see bouncer @@ -3948,6 +3941,10 @@ public final class NotificationPanelViewController implements Dumpable { } } + public int getBarState() { + return mBarState; + } + private boolean isOnKeyguard() { return mBarState == KEYGUARD; } @@ -3993,35 +3990,6 @@ public final class NotificationPanelViewController implements Dumpable { && mBarState == StatusBarState.SHADE; } - /** Launches the camera. */ - public void launchCamera(int source) { - if (!isFullyCollapsed()) { - setLaunchingAffordance(true); - } - - mCameraGestureHelper.launchCamera(source); - } - - public void onAffordanceLaunchEnded() { - setLaunchingAffordance(false); - } - - /** Set whether we are currently launching an affordance (i.e. camera gesture). */ - private void setLaunchingAffordance(boolean launchingAffordance) { - mLaunchingAffordance = launchingAffordance; - mKeyguardBypassController.setLaunchingAffordance(launchingAffordance); - } - - /** Returns whether a bottom affordance is launching an occluded activity with splash screen. */ - public boolean isLaunchingAffordanceWithPreview() { - return mLaunchingAffordance; - } - - /** Whether the camera application can be launched by the camera launch gesture. */ - public boolean canCameraGestureBeLaunched() { - return mCameraGestureHelper.canCameraGestureBeLaunched(mBarState); - } - public boolean hideStatusBarIconsWhenExpanded() { if (mIsLaunchAnimationRunning) { return mHideIconsDuringLaunchAnimation; @@ -4360,7 +4328,6 @@ public final class NotificationPanelViewController implements Dumpable { ipw.print("mCollapsedOnDown="); ipw.println(mCollapsedOnDown); ipw.print("mClosingWithAlphaFadeOut="); ipw.println(mClosingWithAlphaFadeOut); ipw.print("mHeadsUpAnimatingAway="); ipw.println(mHeadsUpAnimatingAway); - ipw.print("mLaunchingAffordance="); ipw.println(mLaunchingAffordance); ipw.print("mShowIconsWhenExpanded="); ipw.println(mShowIconsWhenExpanded); ipw.print("mIndicationBottomPadding="); ipw.println(mIndicationBottomPadding); ipw.print("mAmbientIndicationBottomPadding="); ipw.println(mAmbientIndicationBottomPadding); 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 3670d0987a03..15f4b12ba10e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -1179,16 +1179,14 @@ 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 +1246,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; @@ -1263,7 +1270,8 @@ public class KeyguardIndicationController { showErrorMessageNowOrLater(errString, followupMessage); } else if (!mAuthController.isUdfpsFingerDown()) { // On subsequent lockouts, we show a more generic locked out message. - showBiometricMessage(mContext.getString(R.string.keyguard_face_unlock_unavailable), + showErrorMessageNowOrLater( + mContext.getString(R.string.keyguard_face_unlock_unavailable), followupMessage); } } 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/connectivity/MobileSignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalController.java index ec221b7eccc0..c523d22456f6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalController.java @@ -15,9 +15,7 @@ */ package com.android.systemui.statusbar.connectivity; -import static com.android.settingslib.mobile.MobileMappings.getDefaultIcons; -import static com.android.settingslib.mobile.MobileMappings.getIconKey; -import static com.android.settingslib.mobile.MobileMappings.mapIconSets; +import static android.telephony.TelephonyManager.UNKNOWN_CARRIER_ID; import android.content.Context; import android.content.Intent; @@ -46,6 +44,7 @@ import com.android.settingslib.mobile.MobileStatusTracker.SubscriptionDefaults; import com.android.settingslib.mobile.TelephonyIcons; import com.android.settingslib.net.SignalStrengthUtil; import com.android.systemui.R; +import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy; import com.android.systemui.util.CarrierConfigTracker; import java.io.PrintWriter; @@ -63,6 +62,7 @@ public class MobileSignalController extends SignalController<MobileState, Mobile private final TelephonyManager mPhone; private final CarrierConfigTracker mCarrierConfigTracker; private final SubscriptionDefaults mDefaults; + private final MobileMappingsProxy mMobileMappingsProxy; private final String mNetworkNameDefault; private final String mNetworkNameSeparator; private final ContentObserver mObserver; @@ -121,6 +121,7 @@ public class MobileSignalController extends SignalController<MobileState, Mobile TelephonyManager phone, CallbackHandler callbackHandler, NetworkControllerImpl networkController, + MobileMappingsProxy mobileMappingsProxy, SubscriptionInfo info, SubscriptionDefaults defaults, Looper receiverLooper, @@ -135,13 +136,14 @@ public class MobileSignalController extends SignalController<MobileState, Mobile mPhone = phone; mDefaults = defaults; mSubscriptionInfo = info; + mMobileMappingsProxy = mobileMappingsProxy; mNetworkNameSeparator = getTextIfExists( R.string.status_bar_network_name_separator).toString(); mNetworkNameDefault = getTextIfExists( com.android.internal.R.string.lockscreen_carrier_default).toString(); - mNetworkToIconLookup = mapIconSets(mConfig); - mDefaultIcons = getDefaultIcons(mConfig); + mNetworkToIconLookup = mMobileMappingsProxy.mapIconSets(mConfig); + mDefaultIcons = mMobileMappingsProxy.getDefaultIcons(mConfig); String networkName = info.getCarrierName() != null ? info.getCarrierName().toString() : mNetworkNameDefault; @@ -161,8 +163,8 @@ public class MobileSignalController extends SignalController<MobileState, Mobile void setConfiguration(Config config) { mConfig = config; updateInflateSignalStrength(); - mNetworkToIconLookup = mapIconSets(mConfig); - mDefaultIcons = getDefaultIcons(mConfig); + mNetworkToIconLookup = mMobileMappingsProxy.mapIconSets(mConfig); + mDefaultIcons = mMobileMappingsProxy.getDefaultIcons(mConfig); updateTelephony(); } @@ -271,8 +273,9 @@ public class MobileSignalController extends SignalController<MobileState, Mobile dataContentDescription = mContext.getString(R.string.data_connection_no_internet); } - final QsInfo qsInfo = getQsInfo(contentDescription, icons.dataType); - final SbInfo sbInfo = getSbInfo(contentDescription, icons.dataType); + int iconId = mCurrentState.getNetworkTypeIcon(mContext); + final QsInfo qsInfo = getQsInfo(contentDescription, iconId); + final SbInfo sbInfo = getSbInfo(contentDescription, iconId); MobileDataIndicators mobileDataIndicators = new MobileDataIndicators( sbInfo.icon, @@ -373,6 +376,10 @@ public class MobileSignalController extends SignalController<MobileState, Mobile } else if (action.equals(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)) { updateDataSim(); notifyListenersIfNecessary(); + } else if (action.equals(TelephonyManager.ACTION_SUBSCRIPTION_CARRIER_IDENTITY_CHANGED)) { + int carrierId = intent.getIntExtra( + TelephonyManager.EXTRA_CARRIER_ID, UNKNOWN_CARRIER_ID); + mCurrentState.setCarrierId(carrierId); } } @@ -477,7 +484,8 @@ public class MobileSignalController extends SignalController<MobileState, Mobile mCurrentState.level = getSignalLevel(mCurrentState.signalStrength); } - String iconKey = getIconKey(mCurrentState.telephonyDisplayInfo); + mCurrentState.setCarrierId(mPhone.getSimCarrierId()); + String iconKey = mMobileMappingsProxy.getIconKey(mCurrentState.telephonyDisplayInfo); if (mNetworkToIconLookup.get(iconKey) != null) { mCurrentState.iconGroup = mNetworkToIconLookup.get(iconKey); } else { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalControllerFactory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalControllerFactory.kt index 793817948803..a323454a7ed8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalControllerFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalControllerFactory.kt @@ -22,6 +22,7 @@ import android.telephony.TelephonyManager import com.android.settingslib.mobile.MobileMappings import com.android.settingslib.mobile.MobileStatusTracker import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy import com.android.systemui.util.CarrierConfigTracker import javax.inject.Inject @@ -33,6 +34,7 @@ internal class MobileSignalControllerFactory @Inject constructor( val context: Context, val callbackHandler: CallbackHandler, val carrierConfigTracker: CarrierConfigTracker, + val mobileMappings: MobileMappingsProxy, ) { fun createMobileSignalController( config: MobileMappings.Config, @@ -56,6 +58,7 @@ internal class MobileSignalControllerFactory @Inject constructor( phone, callbackHandler, networkController, + mobileMappings, subscriptionInfo, subscriptionDefaults, receiverLooper, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileState.kt index f20d20631c95..1fb6a982fdf3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileState.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileState.kt @@ -16,10 +16,14 @@ package com.android.systemui.statusbar.connectivity +import android.annotation.DrawableRes +import android.content.Context import android.telephony.ServiceState import android.telephony.SignalStrength import android.telephony.TelephonyDisplayInfo import android.telephony.TelephonyManager +import com.android.internal.annotations.VisibleForTesting +import com.android.settingslib.SignalIcon.MobileIconGroup import com.android.settingslib.Utils import com.android.settingslib.mobile.MobileStatusTracker.MobileStatus import com.android.settingslib.mobile.TelephonyIcons @@ -41,7 +45,7 @@ internal class MobileState( @JvmField var roaming: Boolean = false, @JvmField var dataState: Int = TelephonyManager.DATA_DISCONNECTED, // Tracks the on/off state of the defaultDataSubscription - @JvmField var defaultDataOff: Boolean = false + @JvmField var defaultDataOff: Boolean = false, ) : ConnectivityState() { @JvmField var telephonyDisplayInfo = TelephonyDisplayInfo(TelephonyManager.NETWORK_TYPE_UNKNOWN, @@ -49,6 +53,11 @@ internal class MobileState( @JvmField var serviceState: ServiceState? = null @JvmField var signalStrength: SignalStrength? = null + var carrierId = TelephonyManager.UNKNOWN_CARRIER_ID + + @VisibleForTesting + var networkTypeResIdCache: NetworkTypeResIdCache = NetworkTypeResIdCache() + /** @return true if this state is disabled or not default data */ val isDataDisabledOrNotDefault: Boolean get() = (iconGroup === TelephonyIcons.DATA_DISABLED || @@ -125,6 +134,21 @@ internal class MobileState( return serviceState != null && serviceState!!.roaming } + /** + * + * Load the (potentially customized) icon resource id for the current network type. Note that + * this operation caches the result. Note that reading the [MobileIconGroup.dataType] field + * directly will not yield correct results in cases where the carrierId has an associated + * override. This is the preferred method for getting the network type indicator. + * + * @return a drawable res id appropriate for the current (carrierId, networkType) pair + */ + @DrawableRes + fun getNetworkTypeIcon(context: Context): Int { + val icon = (iconGroup as MobileIconGroup) + return networkTypeResIdCache.get(icon, carrierId, context) + } + fun setFromMobileStatus(mobileStatus: MobileStatus) { activityIn = mobileStatus.activityIn activityOut = mobileStatus.activityOut @@ -140,6 +164,7 @@ internal class MobileState( super.toString(builder) builder.append(',') builder.append("dataSim=$dataSim,") + builder.append("carrierId=$carrierId") builder.append("networkName=$networkName,") builder.append("networkNameData=$networkNameData,") builder.append("dataConnected=$dataConnected,") @@ -157,6 +182,8 @@ internal class MobileState( builder.append("voiceServiceState=${getVoiceServiceState()},") builder.append("isInService=${isInService()},") + builder.append("networkTypeIconCache=$networkTypeResIdCache") + builder.append("serviceState=${serviceState?.minLog() ?: "(null)"},") builder.append("signalStrength=${signalStrength?.minLog() ?: "(null)"},") builder.append("displayInfo=$telephonyDisplayInfo") @@ -164,6 +191,7 @@ internal class MobileState( override fun tableColumns(): List<String> { val columns = listOf("dataSim", + "carrierId", "networkName", "networkNameData", "dataConnected", @@ -178,6 +206,7 @@ internal class MobileState( "showQuickSettingsRatIcon", "voiceServiceState", "isInService", + "networkTypeIconCache", "serviceState", "signalStrength", "displayInfo") @@ -187,6 +216,7 @@ internal class MobileState( override fun tableData(): List<String> { val columns = listOf(dataSim, + carrierId, networkName, networkNameData, dataConnected, @@ -201,6 +231,7 @@ internal class MobileState( showQuickSettingsRatIcon(), getVoiceServiceState(), isInService(), + networkTypeResIdCache, serviceState?.minLog() ?: "(null)", signalStrength?.minLog() ?: "(null)", telephonyDisplayInfo).map { it.toString() } @@ -217,6 +248,7 @@ internal class MobileState( if (networkName != other.networkName) return false if (networkNameData != other.networkNameData) return false + if (carrierId != other.carrierId) return false if (dataSim != other.dataSim) return false if (dataConnected != other.dataConnected) return false if (isEmergency != other.isEmergency) return false @@ -238,6 +270,7 @@ internal class MobileState( var result = super.hashCode() result = 31 * result + (networkName?.hashCode() ?: 0) result = 31 * result + (networkNameData?.hashCode() ?: 0) + result = 31 * result + (carrierId.hashCode()) result = 31 * result + dataSim.hashCode() result = 31 * result + dataConnected.hashCode() result = 31 * result + isEmergency.hashCode() 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 450b757295bc..73d6483e65fb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java @@ -22,6 +22,7 @@ import static android.net.wifi.WifiManager.TrafficStateCallback.DATA_ACTIVITY_IN import static android.net.wifi.WifiManager.TrafficStateCallback.DATA_ACTIVITY_INOUT; import static android.net.wifi.WifiManager.TrafficStateCallback.DATA_ACTIVITY_NONE; import static android.net.wifi.WifiManager.TrafficStateCallback.DATA_ACTIVITY_OUT; +import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID; import android.annotation.Nullable; import android.content.BroadcastReceiver; @@ -138,7 +139,7 @@ public class NetworkControllerImpl extends BroadcastReceiver private final MobileSignalControllerFactory mMobileFactory; private TelephonyCallback.ActiveDataSubscriptionIdListener mPhoneStateListener; - private int mActiveMobileDataSubscription = SubscriptionManager.INVALID_SUBSCRIPTION_ID; + private int mActiveMobileDataSubscription = INVALID_SUBSCRIPTION_ID; // Subcontrollers. @VisibleForTesting @@ -502,6 +503,7 @@ public class NetworkControllerImpl extends BroadcastReceiver filter.addAction(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED); filter.addAction(TelephonyManager.ACTION_DEFAULT_VOICE_SUBSCRIPTION_CHANGED); filter.addAction(TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED); + filter.addAction(TelephonyManager.ACTION_SUBSCRIPTION_CARRIER_IDENTITY_CHANGED); filter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION); mBroadcastDispatcher.registerReceiverWithHandler(this, filter, mReceiverHandler); mListening = true; @@ -792,6 +794,20 @@ public class NetworkControllerImpl extends BroadcastReceiver mConfig = Config.readConfig(mContext); mReceiverHandler.post(this::handleConfigurationChanged); break; + + case TelephonyManager.ACTION_SUBSCRIPTION_CARRIER_IDENTITY_CHANGED: { + // Notify the relevant MobileSignalController of the change + int subId = intent.getIntExtra( + TelephonyManager.EXTRA_SUBSCRIPTION_ID, + INVALID_SUBSCRIPTION_ID + ); + if (SubscriptionManager.isValidSubscriptionId(subId)) { + if (mMobileSignalControllers.indexOfKey(subId) >= 0) { + mMobileSignalControllers.get(subId).handleBroadcast(intent); + } + } + } + break; case Intent.ACTION_SIM_STATE_CHANGED: // Avoid rebroadcast because SysUI is direct boot aware. if (intent.getBooleanExtra(Intent.EXTRA_REBROADCAST_ON_UNLOCK, false)) { @@ -819,7 +835,7 @@ public class NetworkControllerImpl extends BroadcastReceiver break; default: int subId = intent.getIntExtra(SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX, - SubscriptionManager.INVALID_SUBSCRIPTION_ID); + INVALID_SUBSCRIPTION_ID); if (SubscriptionManager.isValidSubscriptionId(subId)) { if (mMobileSignalControllers.indexOfKey(subId) >= 0) { mMobileSignalControllers.get(subId).handleBroadcast(intent); @@ -1335,6 +1351,9 @@ public class NetworkControllerImpl extends BroadcastReceiver String slotString = args.getString("slot"); int slot = TextUtils.isEmpty(slotString) ? 0 : Integer.parseInt(slotString); slot = MathUtils.constrain(slot, 0, 8); + String carrierIdString = args.getString("carrierid"); + int carrierId = TextUtils.isEmpty(carrierIdString) ? 0 + : Integer.parseInt(carrierIdString); // Ensure we have enough sim slots List<SubscriptionInfo> subs = new ArrayList<>(); while (mMobileSignalControllers.size() <= slot) { @@ -1346,6 +1365,9 @@ public class NetworkControllerImpl extends BroadcastReceiver } // Hack to index linearly for easy use. MobileSignalController controller = mMobileSignalControllers.valueAt(slot); + if (carrierId != 0) { + controller.getState().setCarrierId(carrierId); + } controller.getState().dataSim = datatype != null; controller.getState().isDefault = datatype != null; controller.getState().dataConnected = datatype != null; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkTypeResIdCache.kt b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkTypeResIdCache.kt new file mode 100644 index 000000000000..9be7ee99cf02 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkTypeResIdCache.kt @@ -0,0 +1,78 @@ +/* + * 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.connectivity + +import android.annotation.DrawableRes +import android.content.Context +import com.android.settingslib.SignalIcon.MobileIconGroup +import com.android.settingslib.mobile.MobileIconCarrierIdOverrides +import com.android.settingslib.mobile.MobileIconCarrierIdOverridesImpl + +/** + * Cache for network type resource IDs. + * + * The default framework behavior is to have a statically defined icon per network type. See + * [MobileIconGroup] for the standard mapping. + * + * For the case of carrierId-defined overrides, we want to check [MobileIconCarrierIdOverrides] for + * an existing icon override, and cache the result of the operation + */ +class NetworkTypeResIdCache( + private val overrides: MobileIconCarrierIdOverrides = MobileIconCarrierIdOverridesImpl() +) { + @DrawableRes private var cachedResId: Int = 0 + private var lastCarrierId: Int? = null + private var lastIconGroup: MobileIconGroup? = null + private var isOverridden: Boolean = false + + @DrawableRes + fun get(iconGroup: MobileIconGroup, carrierId: Int, context: Context): Int { + if (lastCarrierId != carrierId || lastIconGroup != iconGroup) { + lastCarrierId = carrierId + lastIconGroup = iconGroup + + val maybeOverride = calculateOverriddenIcon(iconGroup, carrierId, context) + if (maybeOverride > 0) { + cachedResId = maybeOverride + isOverridden = true + } else { + cachedResId = iconGroup.dataType + isOverridden = false + } + } + + return cachedResId + } + + override fun toString(): String { + return "networkTypeResIdCache={id=$cachedResId, isOverridden=$isOverridden}" + } + + @DrawableRes + private fun calculateOverriddenIcon( + iconGroup: MobileIconGroup, + carrierId: Int, + context: Context, + ): Int { + val name = iconGroup.name + if (!overrides.carrierIdEntryExists(carrierId)) { + return 0 + } + + return overrides.getOverrideFor(carrierId, name, context.resources) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ui/MobileContextProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ui/MobileContextProvider.kt index a02dd3490341..42b874fd7156 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ui/MobileContextProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ui/MobileContextProvider.kt @@ -37,6 +37,13 @@ import javax.inject.Inject * own [Configuration] and track resources based on the full set of available mcc-mnc combinations. * * (for future reference: b/240555502 is the initiating bug for this) + * + * NOTE: MCC/MNC qualifiers are not sufficient to fully describe a network type icon qualified by + * network type + carrier ID. This class exists to keep the legacy behavior of using the MCC/MNC + * resource qualifiers working, but if a carrier-specific icon is requested, then the override + * provided by [MobileIconCarrierIdOverrides] will take precedence. + * + * TODO(b/258503704): consider removing this class in favor of the `carrierId` overrides */ @SysUISingleton class MobileContextProvider 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/CentralSurfacesCommandQueueCallbacks.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java index 41f0520a9d14..f3482f490d92 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java @@ -55,6 +55,7 @@ import com.android.systemui.dagger.qualifiers.DisplayId; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.qs.QSPanelController; +import com.android.systemui.shade.CameraLauncher; import com.android.systemui.shade.NotificationPanelViewController; import com.android.systemui.shade.ShadeController; import com.android.systemui.statusbar.CommandQueue; @@ -71,6 +72,8 @@ import java.util.Optional; import javax.inject.Inject; +import dagger.Lazy; + /** */ @CentralSurfacesComponent.CentralSurfacesScope public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callbacks { @@ -99,6 +102,7 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba private final boolean mVibrateOnOpening; private final VibrationEffect mCameraLaunchGestureVibrationEffect; private final SystemBarAttributesListener mSystemBarAttributesListener; + private final Lazy<CameraLauncher> mCameraLauncherLazy; private static final VibrationAttributes HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES = VibrationAttributes.createForUsage(VibrationAttributes.USAGE_HARDWARE_FEEDBACK); @@ -128,8 +132,8 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba Optional<Vibrator> vibratorOptional, DisableFlagsLogger disableFlagsLogger, @DisplayId int displayId, - SystemBarAttributesListener systemBarAttributesListener) { - + SystemBarAttributesListener systemBarAttributesListener, + Lazy<CameraLauncher> cameraLauncherLazy) { mCentralSurfaces = centralSurfaces; mContext = context; mShadeController = shadeController; @@ -152,6 +156,7 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba mVibratorOptional = vibratorOptional; mDisableFlagsLogger = disableFlagsLogger; mDisplayId = displayId; + mCameraLauncherLazy = cameraLauncherLazy; mVibrateOnOpening = resources.getBoolean(R.bool.config_vibrateOnIconAnimation); mCameraLaunchGestureVibrationEffect = getCameraGestureVibrationEffect( @@ -346,7 +351,8 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba mCentralSurfaces.setLaunchCameraOnFinishedGoingToSleep(true); return; } - if (!mNotificationPanelViewController.canCameraGestureBeLaunched()) { + if (!mCameraLauncherLazy.get().canCameraGestureBeLaunched( + mNotificationPanelViewController.getBarState())) { if (CentralSurfaces.DEBUG_CAMERA_LIFT) { Slog.d(CentralSurfaces.TAG, "Can't launch camera right now"); } @@ -383,7 +389,8 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba if (mStatusBarKeyguardViewManager.isBouncerShowing()) { mStatusBarKeyguardViewManager.reset(true /* hide */); } - mNotificationPanelViewController.launchCamera(source); + mCameraLauncherLazy.get().launchCamera(source, + mNotificationPanelViewController.isFullyCollapsed()); mCentralSurfaces.updateScrimController(); } else { // We need to defer the camera launch until the screen comes on, since otherwise 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 71609f887ebf..334f1aff0d7d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -175,6 +175,7 @@ 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; import com.android.systemui.shade.NotificationPanelViewController; import com.android.systemui.shade.NotificationShadeWindowView; import com.android.systemui.shade.NotificationShadeWindowViewController; @@ -468,6 +469,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { private final PluginManager mPluginManager; private final ShadeController mShadeController; private final InitController mInitController; + private final Lazy<CameraLauncher> mCameraLauncherLazy; private final PluginDependencyProvider mPluginDependencyProvider; private final KeyguardDismissUtil mKeyguardDismissUtil; @@ -600,6 +602,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { private Runnable mLaunchTransitionEndRunnable; private Runnable mLaunchTransitionCancelRunnable; + private boolean mLaunchingAffordance; private boolean mLaunchCameraWhenFinishedWaking; private boolean mLaunchCameraOnFinishedGoingToSleep; private boolean mLaunchEmergencyActionWhenFinishedWaking; @@ -744,7 +747,8 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { InteractionJankMonitor jankMonitor, DeviceStateManager deviceStateManager, WiredChargingRippleController wiredChargingRippleController, - IDreamManager dreamManager) { + IDreamManager dreamManager, + Lazy<CameraLauncher> cameraLauncherLazy) { mContext = context; mNotificationsController = notificationsController; mFragmentService = fragmentService; @@ -821,6 +825,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mMessageRouter = messageRouter; mWallpaperManager = wallpaperManager; mJankMonitor = jankMonitor; + mCameraLauncherLazy = cameraLauncherLazy; mLockscreenShadeTransitionController = lockscreenShadeTransitionController; mStartingSurfaceOptional = startingSurfaceOptional; @@ -2949,7 +2954,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { private void onLaunchTransitionFadingEnded() { mNotificationPanelViewController.resetAlpha(); - mNotificationPanelViewController.onAffordanceLaunchEnded(); + mCameraLauncherLazy.get().setLaunchingAffordance(false); releaseGestureWakeLock(); runLaunchTransitionEndRunnable(); mKeyguardStateController.setLaunchTransitionFadingAway(false); @@ -3019,7 +3024,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { private void onLaunchTransitionTimeout() { Log.w(TAG, "Launch transition: Timeout!"); - mNotificationPanelViewController.onAffordanceLaunchEnded(); + mCameraLauncherLazy.get().setLaunchingAffordance(false); releaseGestureWakeLock(); mNotificationPanelViewController.resetViews(false /* animate */); } @@ -3072,7 +3077,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { } mMessageRouter.cancelMessages(MSG_LAUNCH_TRANSITION_TIMEOUT); releaseGestureWakeLock(); - mNotificationPanelViewController.onAffordanceLaunchEnded(); + mCameraLauncherLazy.get().setLaunchingAffordance(false); mNotificationPanelViewController.resetAlpha(); mNotificationPanelViewController.resetTranslation(); mNotificationPanelViewController.resetViewGroupFade(); @@ -3230,7 +3235,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { @Override public void endAffordanceLaunch() { releaseGestureWakeLock(); - mNotificationPanelViewController.onAffordanceLaunchEnded(); + mCameraLauncherLazy.get().setLaunchingAffordance(false); } /** @@ -3503,7 +3508,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { final WakefulnessLifecycle.Observer mWakefulnessObserver = new WakefulnessLifecycle.Observer() { @Override public void onFinishedGoingToSleep() { - mNotificationPanelViewController.onAffordanceLaunchEnded(); + mCameraLauncherLazy.get().setLaunchingAffordance(false); releaseGestureWakeLock(); mLaunchCameraWhenFinishedWaking = false; mDeviceInteractive = false; @@ -3604,7 +3609,8 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { .updateSensitivenessForOccludedWakeup(); } if (mLaunchCameraWhenFinishedWaking) { - mNotificationPanelViewController.launchCamera(mLastCameraLaunchSource); + mCameraLauncherLazy.get().launchCamera(mLastCameraLaunchSource, + mNotificationPanelViewController.isFullyCollapsed()); mLaunchCameraWhenFinishedWaking = false; } if (mLaunchEmergencyActionWhenFinishedWaking) { @@ -3795,8 +3801,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mScrimController.setExpansionAffectsAlpha(!unlocking); - boolean launchingAffordanceWithPreview = - mNotificationPanelViewController.isLaunchingAffordanceWithPreview(); + boolean launchingAffordanceWithPreview = mLaunchingAffordance; mScrimController.setLaunchingAffordanceWithPreview(launchingAffordanceWithPreview); if (mStatusBarKeyguardViewManager.isShowingAlternateBouncer()) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java index d32847227c1a..aa0757e1d572 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java @@ -279,10 +279,7 @@ public class KeyguardBouncer { * @see #onFullyShown() */ private void onFullyHidden() { - cancelShowRunnable(); - setVisibility(View.INVISIBLE); - mFalsingCollector.onBouncerHidden(); - DejankUtils.postAfterTraversal(mResetRunnable); + } private void setVisibility(@View.Visibility int visibility) { @@ -459,7 +456,13 @@ public class KeyguardBouncer { onFullyShown(); dispatchFullyShown(); } else if (fraction == EXPANSION_HIDDEN && oldExpansion != EXPANSION_HIDDEN) { - onFullyHidden(); + DejankUtils.postAfterTraversal(mResetRunnable); + /* + * There are cases where #hide() was not invoked, such as when + * NotificationPanelViewController controls the hide animation. Make sure the state gets + * updated by calling #hide() directly. + */ + hide(false /* destroyView */); dispatchFullyHidden(); } else if (fraction != EXPANSION_VISIBLE && oldExpansion == EXPANSION_VISIBLE) { dispatchStartingToHide(); 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/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java index 86e27aba65f0..d54a8638f2e9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java @@ -895,7 +895,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump float stateBehind = mClipsQsScrim ? state.getNotifAlpha() : state.getBehindAlpha(); float behindAlpha; - int behindTint; + int behindTint = state.getBehindTint(); if (mDarkenWhileDragging) { behindAlpha = MathUtils.lerp(mDefaultScrimAlpha, stateBehind, interpolatedFract); @@ -903,12 +903,14 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump behindAlpha = MathUtils.lerp(0 /* start */, stateBehind, interpolatedFract); } - if (mClipsQsScrim) { - behindTint = ColorUtils.blendARGB(ScrimState.BOUNCER.getNotifTint(), + if (mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()) { + if (mClipsQsScrim) { + behindTint = ColorUtils.blendARGB(ScrimState.BOUNCER.getNotifTint(), state.getNotifTint(), interpolatedFract); - } else { - behindTint = ColorUtils.blendARGB(ScrimState.BOUNCER.getBehindTint(), + } else { + behindTint = ColorUtils.blendARGB(ScrimState.BOUNCER.getBehindTint(), state.getBehindTint(), interpolatedFract); + } } if (mQsExpansion > 0) { behindAlpha = MathUtils.lerp(behindAlpha, mDefaultScrimAlpha, mQsExpansion); 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 e9d4e55c82f2..9e075e952175 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -165,6 +165,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb @Override public void onFullyHidden() { mPrimaryBouncerAnimating = false; + updateStates(); } @Override @@ -468,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 { @@ -1183,12 +1184,16 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb updateNavigationBarVisibility(navBarVisible); } - if (primaryBouncerShowing != mLastPrimaryBouncerShowing || mFirstUpdate) { + boolean isPrimaryBouncerShowingChanged = + primaryBouncerShowing != mLastPrimaryBouncerShowing; + mLastPrimaryBouncerShowing = primaryBouncerShowing; + + if (isPrimaryBouncerShowingChanged || mFirstUpdate) { mNotificationShadeWindowController.setBouncerShowing(primaryBouncerShowing); mCentralSurfaces.setBouncerShowing(primaryBouncerShowing); } if (primaryBouncerIsOrWillBeShowing != mLastPrimaryBouncerIsOrWillBeShowing || mFirstUpdate - || primaryBouncerShowing != mLastPrimaryBouncerShowing) { + || isPrimaryBouncerShowingChanged) { mKeyguardUpdateManager.sendPrimaryBouncerChanged(primaryBouncerIsOrWillBeShowing, primaryBouncerShowing); } @@ -1197,7 +1202,6 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mLastShowing = showing; mLastGlobalActionsVisible = mGlobalActionsVisible; mLastOccluded = occluded; - mLastPrimaryBouncerShowing = primaryBouncerShowing; mLastPrimaryBouncerIsOrWillBeShowing = primaryBouncerIsOrWillBeShowing; mLastBouncerDismissible = primaryBouncerDismissible; mLastRemoteInputActive = remoteInputActive; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/MobileMappings.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/MobileMappings.kt index 60bd0383f8c7..501467f13007 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/MobileMappings.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/MobileMappings.kt @@ -32,6 +32,7 @@ import javax.inject.Inject interface MobileMappingsProxy { fun mapIconSets(config: Config): Map<String, MobileIconGroup> fun getDefaultIcons(config: Config): MobileIconGroup + fun getIconKey(displayInfo: TelephonyDisplayInfo): String fun toIconKey(@NetworkType networkType: Int): String fun toIconKeyOverride(@NetworkType networkType: Int): String } @@ -44,6 +45,9 @@ class MobileMappingsProxyImpl @Inject constructor() : MobileMappingsProxy { override fun getDefaultIcons(config: Config): MobileIconGroup = MobileMappings.getDefaultIcons(config) + override fun getIconKey(displayInfo: TelephonyDisplayInfo): String = + MobileMappings.getIconKey(displayInfo) + override fun toIconKey(@NetworkType networkType: Int): String = MobileMappings.toIconKey(networkType) diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt index 4cb41f3a977e..a9d05d11dc00 100644 --- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt @@ -65,8 +65,7 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora height = WindowManager.LayoutParams.WRAP_CONTENT type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or - WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or - WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON + WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL format = PixelFormat.TRANSLUCENT setTrustedOverlay() } @@ -95,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 @@ -103,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 @@ -114,27 +126,37 @@ 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. // So, set up all our callbacks and inflate the view. configurationController.addCallback(displayScaleListener) - // Wake the screen if necessary so the user will see the view. (Per b/239426653, we want - // the view to show over the dream state, so we should only wake up if the screen is - // completely off.) - if (!powerManager.isScreenOn) { - wakeLock = wakeLockBuilder + + wakeLock = if (!powerManager.isScreenOn) { + // If the screen is off, fully wake it so the user can see the view. + wakeLockBuilder .setTag(newInfo.windowTitle) .setLevelsAndFlags( - PowerManager.FULL_WAKE_LOCK or - PowerManager.ACQUIRE_CAUSES_WAKEUP + PowerManager.FULL_WAKE_LOCK or + PowerManager.ACQUIRE_CAUSES_WAKEUP ) .build() - wakeLock?.acquire(newInfo.wakeReason) - wakeReasonAcquired = newInfo.wakeReason + } else { + // Per b/239426653, we want the view to show over the dream state. + // If the screen is on, using screen bright level will leave screen on the dream + // state but ensure the screen will not go off before wake lock is released. + wakeLockBuilder + .setTag(newInfo.windowTitle) + .setLevelsAndFlags(PowerManager.SCREEN_BRIGHT_WAKE_LOCK) + .build() } - logger.logViewAddition(newInfo.windowTitle) + wakeLock?.acquire(newInfo.wakeReason) + wakeReasonAcquired = newInfo.wakeReason + logger.logViewAddition(id, newInfo.windowTitle) inflateAndUpdateView(newInfo) } @@ -145,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() ) } @@ -190,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 } /** @@ -252,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..3507cb7c40a4 100644 --- a/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java +++ b/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java @@ -381,7 +381,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/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/KeyguardMessageAreaControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java index 5d2b0ca4e7ea..829008403e02 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java @@ -16,8 +16,11 @@ package com.android.keyguard; +import static com.google.common.truth.Truth.assertThat; + import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import android.test.suitebuilder.annotation.SmallTest; import android.testing.AndroidTestingRunner; @@ -90,4 +93,11 @@ public class KeyguardMessageAreaControllerTest extends SysuiTestCase { mMessageAreaController.setIsVisible(true); verify(mKeyguardMessageArea).setIsVisible(true); } + + @Test + public void testGetMessage() { + String msg = "abc"; + when(mKeyguardMessageArea.getText()).thenReturn(msg); + assertThat(mMessageAreaController.getMessage()).isEqualTo(msg); + } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt index b369098cafc0..ffd95f4041f9 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt @@ -31,6 +31,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito +import org.mockito.Mockito.`when` import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @@ -118,4 +119,14 @@ class KeyguardPasswordViewControllerTest : SysuiTestCase() { keyguardPasswordViewController.startAppearAnimation() verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_password) } + + @Test + fun startAppearAnimation_withExistingMessage() { + `when`(mKeyguardMessageAreaController.message).thenReturn("Unlock to continue.") + keyguardPasswordViewController.startAppearAnimation() + verify( + mKeyguardMessageAreaController, + never() + ).setMessage(R.string.keyguard_enter_your_password) + } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt index 9eff70487c74..b3d1c8f909d8 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt @@ -33,6 +33,7 @@ import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.verify import org.mockito.Mockito.`when` +import org.mockito.Mockito.never import org.mockito.MockitoAnnotations @SmallTest @@ -112,4 +113,14 @@ class KeyguardPatternViewControllerTest : SysuiTestCase() { mKeyguardPatternViewController.startAppearAnimation() verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pattern) } + + @Test + fun startAppearAnimation_withExistingMessage() { + `when`(mKeyguardMessageAreaController.message).thenReturn("Unlock to continue.") + mKeyguardPatternViewController.startAppearAnimation() + verify( + mKeyguardMessageAreaController, + never() + ).setMessage(R.string.keyguard_enter_your_password) + } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt index d9efdeaea04c..8bcfe6f2b6f5 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt @@ -100,4 +100,12 @@ class KeyguardPinViewControllerTest : SysuiTestCase() { pinViewController.startAppearAnimation() verify(keyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pin) } + + @Test + fun startAppearAnimation_withExistingMessage() { + Mockito.`when`(keyguardMessageAreaController.message).thenReturn("Unlock to continue.") + pinViewController.startAppearAnimation() + verify(keyguardMessageAreaController, Mockito.never()) + .setMessage(R.string.keyguard_enter_your_password) + } } 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 1bd14e558fa0..36ed669e299c 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java @@ -262,9 +262,12 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase { ConstraintSet.Constraint userSwitcherConstraint = getViewConstraint(R.id.keyguard_bouncer_user_switcher); - assertThat(viewFlipperConstraint.layout.topToTop).isEqualTo(PARENT_ID); + assertThat(viewFlipperConstraint.layout.topToBottom).isEqualTo( + R.id.keyguard_bouncer_user_switcher); assertThat(viewFlipperConstraint.layout.bottomToBottom).isEqualTo(PARENT_ID); assertThat(userSwitcherConstraint.layout.topToTop).isEqualTo(PARENT_ID); + assertThat(userSwitcherConstraint.layout.bottomToTop).isEqualTo( + mSecurityViewFlipper.getId()); assertThat(userSwitcherConstraint.layout.topMargin).isEqualTo( getContext().getResources().getDimensionPixelSize( R.dimen.bouncer_user_switcher_y_trans)); @@ -308,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..27094c0b9853 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java @@ -1313,7 +1313,10 @@ 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 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/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/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/keyguard/KeyguardQuickAffordanceProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt new file mode 100644 index 000000000000..4d66a168303c --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt @@ -0,0 +1,302 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard + +import android.content.pm.PackageManager +import android.content.pm.ProviderInfo +import androidx.test.filters.SmallTest +import com.android.internal.widget.LockPatternUtils +import com.android.systemui.SystemUIAppComponentFactoryBase +import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager +import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.settings.UserTracker +import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderClient as Client +import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract as Contract +import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots +import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.util.mockito.mock +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(JUnit4::class) +class KeyguardQuickAffordanceProviderTest : SysuiTestCase() { + + @Mock private lateinit var lockPatternUtils: LockPatternUtils + @Mock private lateinit var keyguardStateController: KeyguardStateController + @Mock private lateinit var userTracker: UserTracker + @Mock private lateinit var activityStarter: ActivityStarter + + private lateinit var underTest: KeyguardQuickAffordanceProvider + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + underTest = KeyguardQuickAffordanceProvider() + val quickAffordanceRepository = + KeyguardQuickAffordanceRepository( + scope = CoroutineScope(IMMEDIATE), + backgroundDispatcher = IMMEDIATE, + selectionManager = KeyguardQuickAffordanceSelectionManager(), + configs = + setOf( + FakeKeyguardQuickAffordanceConfig( + key = AFFORDANCE_1, + pickerIconResourceId = 1, + ), + FakeKeyguardQuickAffordanceConfig( + key = AFFORDANCE_2, + pickerIconResourceId = 2, + ), + ), + ) + underTest.interactor = + KeyguardQuickAffordanceInteractor( + keyguardInteractor = + KeyguardInteractor( + repository = FakeKeyguardRepository(), + ), + registry = mock(), + lockPatternUtils = lockPatternUtils, + keyguardStateController = keyguardStateController, + userTracker = userTracker, + activityStarter = activityStarter, + featureFlags = + FakeFeatureFlags().apply { + set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, true) + }, + repository = { quickAffordanceRepository }, + ) + + underTest.attachInfoForTesting( + context, + ProviderInfo().apply { authority = Contract.AUTHORITY }, + ) + context.contentResolver.addProvider(Contract.AUTHORITY, underTest) + context.testablePermissions.setPermission( + Contract.PERMISSION, + PackageManager.PERMISSION_GRANTED, + ) + } + + @Test + fun `onAttachInfo - reportsContext`() { + val callback: SystemUIAppComponentFactoryBase.ContextAvailableCallback = mock() + underTest.setContextAvailableCallback(callback) + + underTest.attachInfo(context, null) + + verify(callback).onContextAvailable(context) + } + + @Test + fun getType() { + assertThat(underTest.getType(Contract.AffordanceTable.URI)) + .isEqualTo( + "vnd.android.cursor.dir/vnd." + + "${Contract.AUTHORITY}.${Contract.AffordanceTable.TABLE_NAME}" + ) + assertThat(underTest.getType(Contract.SlotTable.URI)) + .isEqualTo( + "vnd.android.cursor.dir/vnd.${Contract.AUTHORITY}.${Contract.SlotTable.TABLE_NAME}" + ) + assertThat(underTest.getType(Contract.SelectionTable.URI)) + .isEqualTo( + "vnd.android.cursor.dir/vnd." + + "${Contract.AUTHORITY}.${Contract.SelectionTable.TABLE_NAME}" + ) + } + + @Test + fun `insert and query selection`() = + runBlocking(IMMEDIATE) { + val slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START + val affordanceId = AFFORDANCE_2 + + Client.insertSelection( + context = context, + slotId = slotId, + affordanceId = affordanceId, + dispatcher = IMMEDIATE, + ) + + assertThat( + Client.querySelections( + context = context, + dispatcher = IMMEDIATE, + ) + ) + .isEqualTo( + listOf( + Client.Selection( + slotId = slotId, + affordanceId = affordanceId, + ) + ) + ) + } + + @Test + fun `query slots`() = + runBlocking(IMMEDIATE) { + assertThat( + Client.querySlots( + context = context, + dispatcher = IMMEDIATE, + ) + ) + .isEqualTo( + listOf( + Client.Slot( + id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, + capacity = 1, + ), + Client.Slot( + id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, + capacity = 1, + ), + ) + ) + } + + @Test + fun `query affordances`() = + runBlocking(IMMEDIATE) { + assertThat( + Client.queryAffordances( + context = context, + dispatcher = IMMEDIATE, + ) + ) + .isEqualTo( + listOf( + Client.Affordance( + id = AFFORDANCE_1, + name = AFFORDANCE_1, + iconResourceId = 1, + ), + Client.Affordance( + id = AFFORDANCE_2, + name = AFFORDANCE_2, + iconResourceId = 2, + ), + ) + ) + } + + @Test + fun `delete and query selection`() = + runBlocking(IMMEDIATE) { + Client.insertSelection( + context = context, + slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, + affordanceId = AFFORDANCE_1, + dispatcher = IMMEDIATE, + ) + Client.insertSelection( + context = context, + slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, + affordanceId = AFFORDANCE_2, + dispatcher = IMMEDIATE, + ) + + Client.deleteSelection( + context = context, + slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, + affordanceId = AFFORDANCE_2, + dispatcher = IMMEDIATE, + ) + + assertThat( + Client.querySelections( + context = context, + dispatcher = IMMEDIATE, + ) + ) + .isEqualTo( + listOf( + Client.Selection( + slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, + affordanceId = AFFORDANCE_1, + ) + ) + ) + } + + @Test + fun `delete all selections in a slot`() = + runBlocking(IMMEDIATE) { + Client.insertSelection( + context = context, + slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, + affordanceId = AFFORDANCE_1, + dispatcher = IMMEDIATE, + ) + Client.insertSelection( + context = context, + slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, + affordanceId = AFFORDANCE_2, + dispatcher = IMMEDIATE, + ) + + Client.deleteAllSelections( + context = context, + slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, + dispatcher = IMMEDIATE, + ) + + assertThat( + Client.querySelections( + context = context, + dispatcher = IMMEDIATE, + ) + ) + .isEqualTo( + listOf( + Client.Selection( + slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, + affordanceId = AFFORDANCE_1, + ) + ) + ) + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + private const val AFFORDANCE_1 = "affordance_1" + private const val AFFORDANCE_2 = "affordance_2" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt index ade83cf58e6e..6ba06344314c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyguard.data.repository import androidx.test.filters.SmallTest import com.android.keyguard.KeyguardUpdateMonitor +import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.Position import com.android.systemui.doze.DozeHost @@ -60,12 +61,12 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { underTest = KeyguardRepositoryImpl( - statusBarStateController, - dozeHost, - wakefulnessLifecycle, - biometricUnlockController, - keyguardStateController, - keyguardUpdateMonitor, + statusBarStateController, + dozeHost, + wakefulnessLifecycle, + biometricUnlockController, + keyguardStateController, + keyguardUpdateMonitor, ) } @@ -257,6 +258,48 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { } @Test + fun isKeyguardGoingAway() = runBlockingTest { + whenever(keyguardStateController.isKeyguardGoingAway).thenReturn(false) + var latest: Boolean? = null + val job = underTest.isKeyguardGoingAway.onEach { latest = it }.launchIn(this) + + assertThat(latest).isFalse() + + val captor = argumentCaptor<KeyguardStateController.Callback>() + verify(keyguardStateController).addCallback(captor.capture()) + + whenever(keyguardStateController.isKeyguardGoingAway).thenReturn(true) + captor.value.onKeyguardGoingAwayChanged() + assertThat(latest).isTrue() + + whenever(keyguardStateController.isKeyguardGoingAway).thenReturn(false) + captor.value.onKeyguardGoingAwayChanged() + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun isDreaming() = runBlockingTest { + whenever(keyguardUpdateMonitor.isDreaming()).thenReturn(false) + var latest: Boolean? = null + val job = underTest.isDreaming.onEach { latest = it }.launchIn(this) + + assertThat(latest).isFalse() + + val captor = argumentCaptor<KeyguardUpdateMonitorCallback>() + verify(keyguardUpdateMonitor).registerCallback(captor.capture()) + + captor.value.onDreamingStateChanged(true) + assertThat(latest).isTrue() + + captor.value.onDreamingStateChanged(false) + assertThat(latest).isFalse() + + job.cancel() + } + + @Test fun biometricUnlockState() = runBlockingTest { val values = mutableListOf<BiometricUnlockModel>() val job = underTest.biometricUnlockState.onEach(values::add).launchIn(this) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt index 27d5d0a98978..2b03722f9f31 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt @@ -25,8 +25,8 @@ import android.view.Choreographer.FrameCallback import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.animation.Interpolators +import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.KeyguardState.AOD -import com.android.systemui.keyguard.shared.model.KeyguardState.BOUNCER import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN import com.android.systemui.keyguard.shared.model.TransitionInfo import com.android.systemui.keyguard.shared.model.TransitionState @@ -38,7 +38,6 @@ import java.util.UUID import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.launchIn @@ -91,18 +90,51 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { } } - assertSteps(steps, listWithStep(BigDecimal(.1))) + assertSteps(steps, listWithStep(BigDecimal(.1)), AOD, LOCKSCREEN) job.cancel() provider.stop() } @Test - fun `startTransition called during another transition fails`() { - underTest.startTransition(TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, null)) - underTest.startTransition(TransitionInfo(OWNER_NAME, LOCKSCREEN, BOUNCER, null)) + fun `starting second transition will cancel the first transition`() { + runBlocking(IMMEDIATE) { + val (animator, provider) = setupAnimator(this) - assertThat(wtfHandler.failed).isTrue() + val steps = mutableListOf<TransitionStep>() + val job = underTest.transition(AOD, LOCKSCREEN).onEach { steps.add(it) }.launchIn(this) + + underTest.startTransition(TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, animator)) + // 3 yields(), alternating with the animator, results in a value 0.1, which can be + // canceled and tested against + yield() + yield() + yield() + + // Now start 2nd transition, which will interrupt the first + val job2 = underTest.transition(LOCKSCREEN, AOD).onEach { steps.add(it) }.launchIn(this) + val (animator2, provider2) = setupAnimator(this) + underTest.startTransition(TransitionInfo(OWNER_NAME, LOCKSCREEN, AOD, animator2)) + + val startTime = System.currentTimeMillis() + while (animator2.isRunning()) { + yield() + if (System.currentTimeMillis() - startTime > MAX_TEST_DURATION) { + fail("Failed test due to excessive runtime of: $MAX_TEST_DURATION") + } + } + + val firstTransitionSteps = listWithStep(step = BigDecimal(.1), stop = BigDecimal(.1)) + assertSteps(steps.subList(0, 4), firstTransitionSteps, AOD, LOCKSCREEN) + + val secondTransitionSteps = listWithStep(step = BigDecimal(.1), start = BigDecimal(.9)) + assertSteps(steps.subList(4, steps.size), secondTransitionSteps, LOCKSCREEN, AOD) + + job.cancel() + job2.cancel() + provider.stop() + provider2.stop() + } } @Test @@ -165,11 +197,15 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { assertThat(wtfHandler.failed).isTrue() } - private fun listWithStep(step: BigDecimal): List<BigDecimal> { + private fun listWithStep( + step: BigDecimal, + start: BigDecimal = BigDecimal.ZERO, + stop: BigDecimal = BigDecimal.ONE, + ): List<BigDecimal> { val steps = mutableListOf<BigDecimal>() - var i = BigDecimal.ZERO - while (i.compareTo(BigDecimal.ONE) <= 0) { + var i = start + while (i.compareTo(stop) <= 0) { steps.add(i) i = (i + step).setScale(2, RoundingMode.HALF_UP) } @@ -177,23 +213,43 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { return steps } - private fun assertSteps(steps: List<TransitionStep>, fractions: List<BigDecimal>) { + private fun assertSteps( + steps: List<TransitionStep>, + fractions: List<BigDecimal>, + from: KeyguardState, + to: KeyguardState, + ) { assertThat(steps[0]) - .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0f, TransitionState.STARTED, OWNER_NAME)) + .isEqualTo( + TransitionStep( + from, + to, + fractions[0].toFloat(), + TransitionState.STARTED, + OWNER_NAME + ) + ) fractions.forEachIndexed { index, fraction -> assertThat(steps[index + 1]) .isEqualTo( TransitionStep( - AOD, - LOCKSCREEN, + from, + to, fraction.toFloat(), TransitionState.RUNNING, OWNER_NAME ) ) } + val lastValue = fractions[fractions.size - 1].toFloat() + val status = + if (lastValue < 1f) { + TransitionState.CANCELED + } else { + TransitionState.FINISHED + } assertThat(steps[steps.size - 1]) - .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 1f, TransitionState.FINISHED, OWNER_NAME)) + .isEqualTo(TransitionStep(from, to, lastValue, status, OWNER_NAME)) assertThat(wtfHandler.failed).isFalse() } @@ -230,7 +286,7 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { scope.launch { frames.collect { // Delay is required for AnimationHandler to properly register a callback - delay(1) + yield() val (frameNumber, callback) = it callback?.doFrame(frameNumber) } @@ -243,7 +299,7 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { } override fun postFrameCallback(cb: FrameCallback) { - frames.value = Pair(++frameCount, cb) + frames.value = Pair(frameCount++, cb) } override fun postCommitCallback(runnable: Runnable) {} override fun getFrameTime() = frameCount 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 c85f7b9e6885..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) @@ -155,6 +154,7 @@ class PrimaryBouncerInteractorTest : SysuiTestCase() { mPrimaryBouncerInteractor.setPanelExpansion(EXPANSION_HIDDEN) verify(repository).setPrimaryVisible(false) verify(repository).setPrimaryShow(null) + verify(repository).setPrimaryHide(true) verify(falsingCollector).onBouncerHidden() verify(mPrimaryBouncerCallbackInteractor).dispatchReset() verify(mPrimaryBouncerCallbackInteractor).dispatchFullyHidden() 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/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/screenrecord/ScreenRecordPermissionDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogTest.kt new file mode 100644 index 000000000000..0aa36218b3a7 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogTest.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.screenrecord + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.view.View +import android.widget.Spinner +import androidx.test.filters.SmallTest +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.settings.UserContextProvider +import com.android.systemui.util.mockito.mock +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +class ScreenRecordPermissionDialogTest : SysuiTestCase() { + + @Mock private lateinit var starter: ActivityStarter + @Mock private lateinit var controller: RecordingController + @Mock private lateinit var userContextProvider: UserContextProvider + @Mock private lateinit var flags: FeatureFlags + @Mock private lateinit var dialogLaunchAnimator: DialogLaunchAnimator + @Mock private lateinit var onStartRecordingClicked: Runnable + + private lateinit var dialog: ScreenRecordPermissionDialog + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + dialog = + ScreenRecordPermissionDialog( + context, + controller, + starter, + dialogLaunchAnimator, + userContextProvider, + onStartRecordingClicked + ) + dialog.onCreate(null) + whenever(flags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING)).thenReturn(true) + } + + @After + fun teardown() { + if (::dialog.isInitialized) { + dialog.dismiss() + } + } + + @Test + fun testShowDialog_partialScreenSharingEnabled_optionsSpinnerIsVisible() { + dialog.show() + + val visibility = dialog.requireViewById<Spinner>(R.id.screen_share_mode_spinner).visibility + assertThat(visibility).isEqualTo(View.VISIBLE) + } + + @Test + fun testShowDialog_singleAppSelected_showTapsIsGone() { + dialog.show() + onSpinnerItemSelected(SINGLE_APP) + + val visibility = dialog.requireViewById<View>(R.id.show_taps).visibility + assertThat(visibility).isEqualTo(View.GONE) + } + + @Test + fun testShowDialog_entireScreenSelected_showTapsIsVisible() { + dialog.show() + onSpinnerItemSelected(ENTIRE_SCREEN) + + val visibility = dialog.requireViewById<View>(R.id.show_taps).visibility + assertThat(visibility).isEqualTo(View.VISIBLE) + } + + private fun onSpinnerItemSelected(position: Int) { + val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_spinner) + spinner.onItemSelectedListener.onItemSelected(spinner, mock(), position, /* id= */ 0) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java index 1f71e3c64ec4..7d2251e20021 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java @@ -94,7 +94,6 @@ import com.android.systemui.DejankUtils; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.biometrics.AuthController; -import com.android.systemui.camera.CameraGestureHelper; import com.android.systemui.classifier.FalsingCollectorFake; import com.android.systemui.classifier.FalsingManagerFake; import com.android.systemui.doze.DozeLog; @@ -492,7 +491,6 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { mUnlockedScreenOffAnimationController, mShadeTransitionController, systemClock, - mock(CameraGestureHelper.class), mKeyguardBottomAreaViewModel, mKeyguardBottomAreaInteractor, mDumpManager); 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 c6585931a035..fe4da473717e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java @@ -1067,7 +1067,7 @@ 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 +1091,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 +1108,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 +1125,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); @@ -1421,6 +1421,21 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { } @Test + public void onBiometricError_faceLockedOutSecondTimeOnBouncer_showsUnavailableMessage() { + createController(); + onFaceLockoutError("first lockout"); + clearInvocations(mRotateTextViewController); + when(mStatusBarKeyguardViewManager.isBouncerShowing()).thenReturn(true); + + onFaceLockoutError("second lockout"); + + verify(mStatusBarKeyguardViewManager) + .setKeyguardMessage( + eq(mContext.getString(R.string.keyguard_face_unlock_unavailable)), + any()); + } + + @Test public void onBiometricError_faceLockedOutSecondTimeButUdfpsActive_showsNoMessage() { createController(); onFaceLockoutError("first lockout"); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/MobileIconCarrierIdOverridesFake.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/MobileIconCarrierIdOverridesFake.kt new file mode 100644 index 000000000000..62b4e7b79f5e --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/MobileIconCarrierIdOverridesFake.kt @@ -0,0 +1,48 @@ +/* + * 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.connectivity + +import android.content.res.Resources +import com.android.settingslib.mobile.MobileIconCarrierIdOverrides + +typealias CarrierId = Int + +typealias NetworkType = String + +typealias ResId = Int + +class MobileIconCarrierIdOverridesFake : MobileIconCarrierIdOverrides { + /** Backing for [carrierIdEntryExists] */ + var overriddenIds = mutableSetOf<Int>() + + /** Backing for [getOverrideFor]. Map should be Map< CarrierId < NetworkType, ResId>> */ + var overridesByCarrierId = mutableMapOf<CarrierId, Map<NetworkType, ResId>>() + + override fun getOverrideFor( + carrierId: CarrierId, + networkType: NetworkType, + resources: Resources + ): ResId { + if (!overriddenIds.contains(carrierId)) return 0 + + return overridesByCarrierId[carrierId]?.get(networkType) ?: 0 + } + + override fun carrierIdEntryExists(carrierId: Int): Boolean { + return overriddenIds.contains(carrierId) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/MobileStateTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/MobileStateTest.java deleted file mode 100644 index 7ddfde370afa..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/MobileStateTest.java +++ /dev/null @@ -1,137 +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.connectivity; - -import static junit.framework.Assert.assertFalse; -import static junit.framework.Assert.assertTrue; - -import android.test.suitebuilder.annotation.SmallTest; -import android.testing.AndroidTestingRunner; - -import com.android.settingslib.mobile.TelephonyIcons; -import com.android.systemui.SysuiTestCase; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -@SmallTest -@RunWith(AndroidTestingRunner.class) -public class MobileStateTest extends SysuiTestCase { - - private final MobileState mState = new MobileState(); - - @Before - public void setUp() { - } - - @Test - public void testIsDataDisabledOrNotDefault_dataDisabled() { - mState.iconGroup = TelephonyIcons.DATA_DISABLED; - mState.userSetup = true; - - assertTrue(mState.isDataDisabledOrNotDefault()); - } - - @Test - public void testIsDataDisabledOrNotDefault_notDefaultData() { - mState.iconGroup = TelephonyIcons.NOT_DEFAULT_DATA; - mState.userSetup = true; - - assertTrue(mState.isDataDisabledOrNotDefault()); - } - - @Test - public void testIsDataDisabledOrNotDefault_notDisabled() { - mState.iconGroup = TelephonyIcons.G; - mState.userSetup = true; - - assertFalse(mState.isDataDisabledOrNotDefault()); - } - - @Test - public void testHasActivityIn_noData_noActivity() { - mState.dataConnected = false; - mState.carrierNetworkChangeMode = false; - mState.activityIn = false; - - assertFalse(mState.hasActivityIn()); - } - - @Test - public void testHasActivityIn_noData_activityIn() { - mState.dataConnected = false; - mState.carrierNetworkChangeMode = false; - mState.activityIn = true; - - assertFalse(mState.hasActivityIn()); - } - - @Test - public void testHasActivityIn_dataConnected_activityIn() { - mState.dataConnected = true; - mState.carrierNetworkChangeMode = false; - mState.activityIn = true; - - assertTrue(mState.hasActivityIn()); - } - - @Test - public void testHasActivityIn_carrierNetworkChange() { - mState.dataConnected = true; - mState.carrierNetworkChangeMode = true; - mState.activityIn = true; - - assertFalse(mState.hasActivityIn()); - } - - @Test - public void testHasActivityOut_noData_noActivity() { - mState.dataConnected = false; - mState.carrierNetworkChangeMode = false; - mState.activityOut = false; - - assertFalse(mState.hasActivityOut()); - } - - @Test - public void testHasActivityOut_noData_activityOut() { - mState.dataConnected = false; - mState.carrierNetworkChangeMode = false; - mState.activityOut = true; - - assertFalse(mState.hasActivityOut()); - } - - @Test - public void testHasActivityOut_dataConnected_activityOut() { - mState.dataConnected = true; - mState.carrierNetworkChangeMode = false; - mState.activityOut = true; - - assertTrue(mState.hasActivityOut()); - } - - @Test - public void testHasActivityOut_carrierNetworkChange() { - mState.dataConnected = true; - mState.carrierNetworkChangeMode = true; - mState.activityOut = true; - - assertFalse(mState.hasActivityOut()); - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/MobileStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/MobileStateTest.kt new file mode 100644 index 000000000000..a226ded06111 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/MobileStateTest.kt @@ -0,0 +1,119 @@ +/* + * 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.connectivity + +import android.test.suitebuilder.annotation.SmallTest +import android.testing.AndroidTestingRunner +import com.android.settingslib.mobile.TelephonyIcons +import com.android.systemui.SysuiTestCase +import junit.framework.Assert.assertFalse +import junit.framework.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class MobileStateTest : SysuiTestCase() { + + private val state = MobileState() + @Before fun setUp() {} + + @Test + fun testIsDataDisabledOrNotDefault_dataDisabled() { + state.iconGroup = TelephonyIcons.DATA_DISABLED + state.userSetup = true + assertTrue(state.isDataDisabledOrNotDefault) + } + + @Test + fun testIsDataDisabledOrNotDefault_notDefaultData() { + state.iconGroup = TelephonyIcons.NOT_DEFAULT_DATA + state.userSetup = true + assertTrue(state.isDataDisabledOrNotDefault) + } + + @Test + fun testIsDataDisabledOrNotDefault_notDisabled() { + state.iconGroup = TelephonyIcons.G + state.userSetup = true + assertFalse(state.isDataDisabledOrNotDefault) + } + + @Test + fun testHasActivityIn_noData_noActivity() { + state.dataConnected = false + state.carrierNetworkChangeMode = false + state.activityIn = false + assertFalse(state.hasActivityIn()) + } + + @Test + fun testHasActivityIn_noData_activityIn() { + state.dataConnected = false + state.carrierNetworkChangeMode = false + state.activityIn = true + assertFalse(state.hasActivityIn()) + } + + @Test + fun testHasActivityIn_dataConnected_activityIn() { + state.dataConnected = true + state.carrierNetworkChangeMode = false + state.activityIn = true + assertTrue(state.hasActivityIn()) + } + + @Test + fun testHasActivityIn_carrierNetworkChange() { + state.dataConnected = true + state.carrierNetworkChangeMode = true + state.activityIn = true + assertFalse(state.hasActivityIn()) + } + + @Test + fun testHasActivityOut_noData_noActivity() { + state.dataConnected = false + state.carrierNetworkChangeMode = false + state.activityOut = false + assertFalse(state.hasActivityOut()) + } + + @Test + fun testHasActivityOut_noData_activityOut() { + state.dataConnected = false + state.carrierNetworkChangeMode = false + state.activityOut = true + assertFalse(state.hasActivityOut()) + } + + @Test + fun testHasActivityOut_dataConnected_activityOut() { + state.dataConnected = true + state.carrierNetworkChangeMode = false + state.activityOut = true + assertTrue(state.hasActivityOut()) + } + + @Test + fun testHasActivityOut_carrierNetworkChange() { + state.dataConnected = true + state.carrierNetworkChangeMode = true + state.activityOut = true + assertFalse(state.hasActivityOut()) + } +} 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 9c65fac1af45..9c870b5aa363 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.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener; import com.android.systemui.telephony.TelephonyListenerManager; @@ -125,6 +126,8 @@ public class NetworkControllerBaseTest extends SysuiTestCase { protected CarrierConfigTracker mCarrierConfigTracker; protected FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock()); protected Handler mMainHandler; + // Use a real mobile mappings object since lots of tests rely on it + protected FakeMobileMappingsProxy mMobileMappingsProxy = new FakeMobileMappingsProxy(); protected WifiStatusTrackerFactory mWifiStatusTrackerFactory; protected MobileSignalControllerFactory mMobileFactory; @@ -219,10 +222,13 @@ public class NetworkControllerBaseTest extends SysuiTestCase { mWifiStatusTrackerFactory = new WifiStatusTrackerFactory( mContext, mMockWm, mMockNsm, mMockCm, mMainHandler); + // Most of these tests rely on the actual MobileMappings behavior + mMobileMappingsProxy.setUseRealImpl(true); mMobileFactory = new MobileSignalControllerFactory( mContext, mCallbackHandler, - mCarrierConfigTracker + mCarrierConfigTracker, + mMobileMappingsProxy ); mNetworkController = new NetworkControllerImpl(mContext, 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 4bed4a19b3d9..1d112262765e 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 @@ -18,12 +18,21 @@ package com.android.systemui.statusbar.connectivity; import static android.telephony.AccessNetworkConstants.TRANSPORT_TYPE_WWAN; import static android.telephony.NetworkRegistrationInfo.DOMAIN_PS; +import static android.telephony.TelephonyManager.ACTION_SUBSCRIPTION_CARRIER_IDENTITY_CHANGED; +import static android.telephony.TelephonyManager.EXTRA_CARRIER_ID; +import static android.telephony.TelephonyManager.EXTRA_SUBSCRIPTION_ID; +import static com.android.settingslib.mobile.TelephonyIcons.NR_5G_PLUS; + +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import android.content.Intent; import android.net.NetworkCapabilities; import android.os.Handler; import android.os.Looper; @@ -35,6 +44,7 @@ import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.testing.TestableLooper.RunWithLooper; +import com.android.settingslib.SignalIcon.MobileIconGroup; import com.android.settingslib.mobile.TelephonyIcons; import com.android.settingslib.net.DataUsageController; import com.android.systemui.dump.DumpManager; @@ -45,6 +55,8 @@ import com.android.systemui.util.CarrierConfigTracker; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.HashMap; + @SmallTest @RunWith(AndroidTestingRunner.class) @RunWithLooper @@ -329,6 +341,57 @@ public class NetworkControllerDataTest extends NetworkControllerBaseTest { assertFalse(mNetworkController.isMobileDataNetworkInService()); } + @Test + public void mobileSignalController_getsCarrierId() { + when(mMockTm.getSimCarrierId()).thenReturn(1); + setupDefaultSignal(); + + assertEquals(1, mMobileSignalController.getState().getCarrierId()); + } + + @Test + public void mobileSignalController_updatesCarrierId_onChange() { + when(mMockTm.getSimCarrierId()).thenReturn(1); + setupDefaultSignal(); + + // Updates are sent down through this broadcast, we can send the intent directly + Intent intent = new Intent(ACTION_SUBSCRIPTION_CARRIER_IDENTITY_CHANGED); + intent.putExtra(EXTRA_SUBSCRIPTION_ID, mSubId); + intent.putExtra(EXTRA_CARRIER_ID, 2); + + mMobileSignalController.handleBroadcast(intent); + + assertEquals(2, mMobileSignalController.getState().getCarrierId()); + } + + @Test + public void networkTypeIcon_hasCarrierIdOverride() { + int fakeCarrier = 1; + int fakeIconOverride = 12345; + int testDataNetType = 100; + String testDataString = "100"; + HashMap<String, MobileIconGroup> testMap = new HashMap<>(); + testMap.put(testDataString, NR_5G_PLUS); + + // Pretend that there is an override for this icon, and this carrier ID + NetworkTypeResIdCache mockCache = mock(NetworkTypeResIdCache.class); + when(mockCache.get(eq(NR_5G_PLUS), eq(fakeCarrier), any())).thenReturn(fakeIconOverride); + + // Turn off the default mobile mapping, so we can override + mMobileMappingsProxy.setUseRealImpl(false); + mMobileMappingsProxy.setIconMap(testMap); + // Use the mocked cache + mMobileSignalController.mCurrentState.setNetworkTypeResIdCache(mockCache); + // Rebuild the network map + mMobileSignalController.setConfiguration(mConfig); + when(mMockTm.getSimCarrierId()).thenReturn(fakeCarrier); + + setupDefaultSignal(); + updateDataConnectionState(TelephonyManager.DATA_CONNECTED, testDataNetType); + + verifyDataIndicators(fakeIconOverride); + } + private void testDataActivity(int direction, boolean in, boolean out) { updateDataActivity(direction); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkTypeResIdCacheTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkTypeResIdCacheTest.kt new file mode 100644 index 000000000000..9e73487972e8 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkTypeResIdCacheTest.kt @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.connectivity + +import android.test.suitebuilder.annotation.SmallTest +import android.testing.AndroidTestingRunner +import com.android.settingslib.SignalIcon.MobileIconGroup +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class NetworkTypeResIdCacheTest : SysuiTestCase() { + private lateinit var cache: NetworkTypeResIdCache + private var overrides = MobileIconCarrierIdOverridesFake() + + @Before + fun setUp() { + cache = NetworkTypeResIdCache(overrides) + } + + @Test + fun carrier1_noOverride_usesDefault() { + assertThat(cache.get(group1, CARRIER_1, context)).isEqualTo(iconDefault1) + } + + @Test + fun carrier1_overridden_usesOverride() { + overrides.overriddenIds.add(CARRIER_1) + overrides.overridesByCarrierId[CARRIER_1] = mapOf(NET_TYPE_1 to iconOverride1) + + assertThat(cache.get(group1, CARRIER_1, context)).isEqualTo(iconOverride1) + } + + @Test + fun carrier1_override_carrier2UsesDefault() { + overrides.overriddenIds.add(CARRIER_1) + overrides.overridesByCarrierId[CARRIER_1] = mapOf(NET_TYPE_1 to iconOverride1) + + assertThat(cache.get(group1, CARRIER_2, context)).isEqualTo(iconDefault1) + } + + @Test + fun carrier1_overrideType1_type2UsesDefault() { + overrides.overriddenIds.add(CARRIER_1) + overrides.overridesByCarrierId[CARRIER_1] = mapOf(NET_TYPE_1 to iconOverride1) + + assertThat(cache.get(group2, CARRIER_1, context)).isEqualTo(iconDefault2) + } + + companion object { + // Simplified icon overrides here + const val CARRIER_1 = 1 + const val CARRIER_2 = 2 + + const val NET_TYPE_1 = "one" + const val iconDefault1 = 123 + const val iconOverride1 = 321 + val group1 = MobileIconGroup(NET_TYPE_1, /* dataContentDesc */ 0, iconDefault1) + + const val NET_TYPE_2 = "two" + const val iconDefault2 = 234 + + val group2 = MobileIconGroup(NET_TYPE_2, /* dataContentDesc*/ 0, iconDefault2) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java index 5ebaf692ab44..d5bfe1f7c2ce 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java @@ -41,6 +41,7 @@ import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.SysuiTestCase; import com.android.systemui.assist.AssistManager; import com.android.systemui.keyguard.WakefulnessLifecycle; +import com.android.systemui.shade.CameraLauncher; import com.android.systemui.shade.NotificationPanelViewController; import com.android.systemui.shade.ShadeController; import com.android.systemui.statusbar.CommandQueue; @@ -60,6 +61,8 @@ import org.mockito.stubbing.Answer; import java.util.Optional; +import dagger.Lazy; + @SmallTest @RunWith(AndroidTestingRunner.class) public class CentralSurfacesCommandQueueCallbacksTest extends SysuiTestCase { @@ -84,6 +87,7 @@ public class CentralSurfacesCommandQueueCallbacksTest extends SysuiTestCase { @Mock private Vibrator mVibrator; @Mock private StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager; @Mock private SystemBarAttributesListener mSystemBarAttributesListener; + @Mock private Lazy<CameraLauncher> mCameraLauncherLazy; CentralSurfacesCommandQueueCallbacks mSbcqCallbacks; @@ -115,7 +119,8 @@ public class CentralSurfacesCommandQueueCallbacksTest extends SysuiTestCase { Optional.of(mVibrator), new DisableFlagsLogger(), DEFAULT_DISPLAY, - mSystemBarAttributesListener); + mSystemBarAttributesListener, + mCameraLauncherLazy); when(mDeviceProvisionedController.isCurrentUserSetup()).thenReturn(true); when(mRemoteInputQuickSettingsDisabler.adjustDisableFlags(anyInt())) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java index 5ad1431cc8d8..41912f51db56 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java @@ -112,6 +112,7 @@ import com.android.systemui.plugins.PluginDependencyProvider; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.recents.ScreenPinningRequest; import com.android.systemui.settings.brightness.BrightnessSliderController; +import com.android.systemui.shade.CameraLauncher; import com.android.systemui.shade.NotificationPanelView; import com.android.systemui.shade.NotificationPanelViewController; import com.android.systemui.shade.NotificationShadeWindowView; @@ -288,6 +289,8 @@ public class CentralSurfacesImplTest extends SysuiTestCase { @Mock private InteractionJankMonitor mJankMonitor; @Mock private DeviceStateManager mDeviceStateManager; @Mock private WiredChargingRippleController mWiredChargingRippleController; + @Mock private Lazy<CameraLauncher> mCameraLauncherLazy; + @Mock private CameraLauncher mCameraLauncher; /** * The process of registering/unregistering a predictive back callback requires a * ViewRootImpl, which is present IRL, but may be missing during a Mockito unit test. @@ -380,6 +383,7 @@ public class CentralSurfacesImplTest extends SysuiTestCase { when(mLockscreenWallpaperLazy.get()).thenReturn(mLockscreenWallpaper); when(mBiometricUnlockControllerLazy.get()).thenReturn(mBiometricUnlockController); + when(mCameraLauncherLazy.get()).thenReturn(mCameraLauncher); when(mStatusBarComponentFactory.create()).thenReturn(mCentralSurfacesComponent); when(mCentralSurfacesComponent.getNotificationShadeWindowViewController()).thenReturn( @@ -481,7 +485,9 @@ public class CentralSurfacesImplTest extends SysuiTestCase { mActivityLaunchAnimator, mJankMonitor, mDeviceStateManager, - mWiredChargingRippleController, mDreamManager) { + mWiredChargingRippleController, + mDreamManager, + mCameraLauncherLazy) { @Override protected ViewRootImpl getViewRootImpl() { return mViewRootImpl; @@ -893,7 +899,7 @@ public class CentralSurfacesImplTest extends SysuiTestCase { mCentralSurfaces.showKeyguardImpl(); // Starting a pulse should change the scrim controller to the pulsing state - when(mNotificationPanelViewController.isLaunchingAffordanceWithPreview()).thenReturn(true); + when(mCameraLauncher.isLaunchingAffordance()).thenReturn(true); mCentralSurfaces.updateScrimController(); verify(mScrimController).transitionTo(eq(ScrimState.UNLOCKED), any()); } @@ -929,7 +935,7 @@ public class CentralSurfacesImplTest extends SysuiTestCase { mCentralSurfaces.showKeyguardImpl(); // Starting a pulse should change the scrim controller to the pulsing state - when(mNotificationPanelViewController.isLaunchingAffordanceWithPreview()).thenReturn(false); + when(mCameraLauncher.isLaunchingAffordance()).thenReturn(false); mCentralSurfaces.updateScrimController(); verify(mScrimController).transitionTo(eq(ScrimState.KEYGUARD)); } 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 df48e1d43584..808abc8e9de5 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 @@ -1437,6 +1437,17 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + public void behindTint_inKeyguardState_bouncerNotActive_usesKeyguardBehindTint() { + when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false); + mScrimController.setClipsQsScrim(false); + + mScrimController.transitionTo(ScrimState.KEYGUARD); + finishAnimationsImmediately(); + assertThat(mScrimBehind.getTint()) + .isEqualTo(ScrimState.KEYGUARD.getBehindTint()); + } + + @Test public void testNotificationTransparency_followsTransitionToFullShade() { mScrimController.transitionTo(SHADE_LOCKED); mScrimController.setRawPanelExpansionFraction(1.0f); 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/pipeline/mobile/util/FakeMobileMappingsProxy.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt index 6d8d902615de..a052008d4832 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt @@ -16,31 +16,59 @@ package com.android.systemui.statusbar.pipeline.mobile.util +import android.telephony.TelephonyDisplayInfo import com.android.settingslib.SignalIcon.MobileIconGroup import com.android.settingslib.mobile.MobileMappings.Config import com.android.settingslib.mobile.TelephonyIcons class FakeMobileMappingsProxy : MobileMappingsProxy { + // The old [NetworkControllerDataTest] infra requires us to be able to use the real + // impl sometimes + var useRealImpl = false + + private var realImpl = MobileMappingsProxyImpl() private var iconMap = mapOf<String, MobileIconGroup>() private var defaultIcons = TelephonyIcons.THREE_G fun setIconMap(map: Map<String, MobileIconGroup>) { iconMap = map } - override fun mapIconSets(config: Config): Map<String, MobileIconGroup> = iconMap + override fun mapIconSets(config: Config): Map<String, MobileIconGroup> { + if (useRealImpl) { + return realImpl.mapIconSets(config) + } + return iconMap + } fun getIconMap() = iconMap fun setDefaultIcons(group: MobileIconGroup) { defaultIcons = group } - override fun getDefaultIcons(config: Config): MobileIconGroup = defaultIcons + override fun getDefaultIcons(config: Config): MobileIconGroup { + if (useRealImpl) { + return realImpl.getDefaultIcons(config) + } + return defaultIcons + } + + /** This is only used in the old pipeline, use the real impl always */ + override fun getIconKey(displayInfo: TelephonyDisplayInfo): String { + return realImpl.getIconKey(displayInfo) + } + fun getDefaultIcons(): MobileIconGroup = defaultIcons override fun toIconKey(networkType: Int): String { + if (useRealImpl) { + return realImpl.toIconKeyOverride(networkType) + } return networkType.toString() } override fun toIconKeyOverride(networkType: Int): String { + if (useRealImpl) { + return realImpl.toIconKeyOverride(networkType) + } return toIconKey(networkType) + "_override" } } 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 9dea48e3b47c..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,27 +119,27 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { ) ) - verify(logger).logViewAddition("Fake Window Title") + verify(logger).logViewAddition("id", "Fake Window Title") } @Test - fun displayView_screenOff_wakeLockAcquired() { + fun displayView_wakeLockAcquired() { underTest.displayView(getState()) assertThat(fakeWakeLock.isHeld).isTrue() } @Test - fun displayView_screenAlreadyOn_wakeLockNotAcquired() { + fun displayView_screenAlreadyOn_wakeLockAcquired() { whenever(powerManager.isScreenOn).thenReturn(true) underTest.displayView(getState()) - assertThat(fakeWakeLock.isHeld).isFalse() + assertThat(fakeWakeLock.isHeld).isTrue() } @Test - fun displayView_screenOff_wakeLockCanBeReleasedAfterTimeOut() { + fun displayView_wakeLockCanBeReleasedAfterTimeOut() { underTest.displayView(getState()) assertThat(fakeWakeLock.isHeld).isTrue() @@ -149,6 +149,16 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { } @Test + fun displayView_removeView_wakeLockCanBeReleased() { + underTest.displayView(getState()) + assertThat(fakeWakeLock.isHeld).isTrue() + + underTest.removeView("id", "test reason") + + assertThat(fakeWakeLock.isHeld).isFalse() + } + + @Test fun displayView_twice_viewNotAddedTwice() { underTest.displayView(getState()) reset(windowManager) @@ -253,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()) } @@ -319,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/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt index 6f70f0ee0f2b..a798f403c73a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt @@ -44,6 +44,9 @@ class FakeKeyguardRepository : KeyguardRepository { private val _isDozing = MutableStateFlow(false) override val isDozing: Flow<Boolean> = _isDozing + private val _isDreaming = MutableStateFlow(false) + override val isDreaming: Flow<Boolean> = _isDreaming + private val _dozeAmount = MutableStateFlow(0f) override val dozeAmount: Flow<Float> = _dozeAmount @@ -54,10 +57,13 @@ class FakeKeyguardRepository : KeyguardRepository { override val wakefulnessState: Flow<WakefulnessModel> = _wakefulnessState private val _isUdfpsSupported = MutableStateFlow(false) - + private val _isBouncerShowing = MutableStateFlow(false) override val isBouncerShowing: Flow<Boolean> = _isBouncerShowing + private val _isKeyguardGoingAway = MutableStateFlow(false) + override val isKeyguardGoingAway: Flow<Boolean> = _isKeyguardGoingAway + private val _biometricUnlockState = MutableStateFlow(BiometricUnlockModel.NONE) override val biometricUnlockState: Flow<BiometricUnlockModel> = _biometricUnlockState 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/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/SystemService.java b/services/core/java/com/android/server/SystemService.java index e40f001f27d5..933d2596aed8 100644 --- a/services/core/java/com/android/server/SystemService.java +++ b/services/core/java/com/android/server/SystemService.java @@ -473,18 +473,6 @@ public abstract class SystemService { } /** - * The {@link UserManager#isUserVisible() user visibility} changed. - * - * <p>This callback is called before the user starts or is switched to (or after it stops), when - * its visibility changed because of that action. - * - * @hide - */ - // NOTE: change visible to int if this method becomes a @SystemApi - public void onUserVisibilityChanged(@NonNull TargetUser user, boolean visible) { - } - - /** * Called when an existing user is stopping, for system services to finalize any per-user * state they maintain for running users. This is called prior to sending the SHUTDOWN * broadcast to the user; it is a good place to stop making use of any resources of that diff --git a/services/core/java/com/android/server/SystemServiceManager.java b/services/core/java/com/android/server/SystemServiceManager.java index 953e85014535..c3cd1359cd5f 100644 --- a/services/core/java/com/android/server/SystemServiceManager.java +++ b/services/core/java/com/android/server/SystemServiceManager.java @@ -82,10 +82,6 @@ public final class SystemServiceManager implements Dumpable { private static final String USER_STOPPING = "Stop"; // Logged as onUserStopping() private static final String USER_STOPPED = "Cleanup"; // Logged as onUserStopped() private static final String USER_COMPLETED_EVENT = "CompletedEvent"; // onUserCompletedEvent() - private static final String USER_VISIBLE = "Visible"; // Logged on onUserVisible() and - // onUserStarting() (when visible is true) - private static final String USER_INVISIBLE = "Invisible"; // Logged on onUserStopping() - // (when visibilityChanged is true) // The default number of threads to use if lifecycle thread pool is enabled. private static final int DEFAULT_MAX_USER_POOL_THREADS = 3; @@ -354,58 +350,17 @@ public final class SystemServiceManager implements Dumpable { /** * Starts the given user. */ - public void onUserStarting(@NonNull TimingsTraceAndSlog t, @UserIdInt int userId, - boolean visible) { - EventLog.writeEvent(EventLogTags.SSM_USER_STARTING, userId, visible ? 1 : 0); + public void onUserStarting(@NonNull TimingsTraceAndSlog t, @UserIdInt int userId) { + EventLog.writeEvent(EventLogTags.SSM_USER_STARTING, userId); final TargetUser targetUser = newTargetUser(userId); synchronized (mTargetUsers) { mTargetUsers.put(userId, targetUser); } - - if (visible) { - // Must send the user visiiblity change first, for 2 reasons: - // 1. Automotive need to update the user-zone mapping ASAP and it's one of the few - // services listening to this event (OTOH, there are manyy listeners to USER_STARTING - // and some can take a while to process it) - // 2. When a user is switched from bg to fg, the onUserVisibilityChanged() callback is - // called onUserSwitching(), so calling it before onUserStarting() make it more - // consistent with that - EventLog.writeEvent(EventLogTags.SSM_USER_VISIBILITY_CHANGED, userId, /* visible= */ 1); - onUser(t, USER_VISIBLE, /* prevUser= */ null, targetUser); - } onUser(t, USER_STARTING, /* prevUser= */ null, targetUser); } /** - * Updates the user visibility. - * - * <p><b>NOTE: </b>this method should only be called when a user that is already running become - * visible; if the user is starting visible, callers should call - * {@link #onUserStarting(TimingsTraceAndSlog, int, boolean)} instead. - */ - public void onUserVisible(@UserIdInt int userId) { - EventLog.writeEvent(EventLogTags.SSM_USER_VISIBILITY_CHANGED, userId, /* visible= */ 1); - onUser(USER_VISIBLE, userId); - } - - /** - * Updates the visibility of the system user. - * - * <p>Since the system user never stops, this method must be called when it's switched from / to - * foreground. - */ - public void onSystemUserVisibilityChanged(boolean visible) { - int userId = UserHandle.USER_SYSTEM; - EventLog.writeEvent(EventLogTags.SSM_USER_VISIBILITY_CHANGED, userId, visible ? 1 : 0); - if (visible) { - onUser(USER_VISIBLE, userId); - } else { - onUser(USER_INVISIBLE, userId); - } - } - - /** * Unlocks the given user. */ public void onUserUnlocking(@UserIdInt int userId) { @@ -452,12 +407,9 @@ public final class SystemServiceManager implements Dumpable { /** * Stops the given user. */ - public void onUserStopping(@UserIdInt int userId, boolean visibilityChanged) { - EventLog.writeEvent(EventLogTags.SSM_USER_STOPPING, userId, visibilityChanged ? 1 : 0); + public void onUserStopping(@UserIdInt int userId) { + EventLog.writeEvent(EventLogTags.SSM_USER_STOPPING, userId); onUser(USER_STOPPING, userId); - if (visibilityChanged) { - onUser(USER_INVISIBLE, userId); - } } /** @@ -580,12 +532,6 @@ public final class SystemServiceManager implements Dumpable { threadPool.submit(getOnUserCompletedEventRunnable( t, service, serviceName, curUser, completedEventType)); break; - case USER_VISIBLE: - service.onUserVisibilityChanged(curUser, /* visible= */ true); - break; - case USER_INVISIBLE: - service.onUserVisibilityChanged(curUser, /* visible= */ false); - break; default: throw new IllegalArgumentException(onWhat + " what?"); } 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/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 2761a864f22f..7d640772a4be 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -11173,9 +11173,9 @@ 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); @@ -19050,6 +19050,10 @@ public class ActivityManagerService extends IActivityManager.Stub return mOomAdjuster.mCachedAppOptimizer.useFreezer(); } + public boolean isAppFreezerExemptInstPkg() { + return mOomAdjuster.mCachedAppOptimizer.freezerExemptInstPkg(); + } + /** * Resets the state of the {@link com.android.server.am.AppErrors} instance. * This is intended for testing within the CTS only and is protected by diff --git a/services/core/java/com/android/server/am/BroadcastConstants.java b/services/core/java/com/android/server/am/BroadcastConstants.java index dfac82cbe91b..56909e35bbe3 100644 --- a/services/core/java/com/android/server/am/BroadcastConstants.java +++ b/services/core/java/com/android/server/am/BroadcastConstants.java @@ -170,7 +170,7 @@ public class BroadcastConstants { */ public long DELAY_NORMAL_MILLIS = DEFAULT_DELAY_NORMAL_MILLIS; private static final String KEY_DELAY_NORMAL_MILLIS = "bcast_delay_normal_millis"; - private static final long DEFAULT_DELAY_NORMAL_MILLIS = +500; + private static final long DEFAULT_DELAY_NORMAL_MILLIS = 0; /** * For {@link BroadcastQueueModernImpl}: Delay to apply to broadcasts @@ -178,7 +178,7 @@ public class BroadcastConstants { */ public long DELAY_CACHED_MILLIS = DEFAULT_DELAY_CACHED_MILLIS; private static final String KEY_DELAY_CACHED_MILLIS = "bcast_delay_cached_millis"; - private static final long DEFAULT_DELAY_CACHED_MILLIS = +120_000; + private static final long DEFAULT_DELAY_CACHED_MILLIS = +30_000; /** * For {@link BroadcastQueueModernImpl}: Delay to apply to urgent diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java index cbf0aae50e87..2d7b0dc7b536 100644 --- a/services/core/java/com/android/server/am/CachedAppOptimizer.java +++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java @@ -89,6 +89,8 @@ public final class CachedAppOptimizer { "compact_proc_state_throttle"; @VisibleForTesting static final String KEY_FREEZER_DEBOUNCE_TIMEOUT = "freeze_debounce_timeout"; + @VisibleForTesting static final String KEY_FREEZER_EXEMPT_INST_PKG = + "freeze_exempt_inst_pkg"; // RSS Indices private static final int RSS_TOTAL_INDEX = 0; @@ -137,6 +139,7 @@ public final class CachedAppOptimizer { @VisibleForTesting static final String DEFAULT_COMPACT_PROC_STATE_THROTTLE = String.valueOf(ActivityManager.PROCESS_STATE_RECEIVER); @VisibleForTesting static final long DEFAULT_FREEZER_DEBOUNCE_TIMEOUT = 600_000L; + @VisibleForTesting static final Boolean DEFAULT_FREEZER_EXEMPT_INST_PKG = true; @VisibleForTesting static final Uri CACHED_APP_FREEZER_ENABLED_URI = Settings.Global.getUriFor( Settings.Global.CACHED_APPS_FREEZER_ENABLED); @@ -277,6 +280,8 @@ public final class CachedAppOptimizer { for (String name : properties.getKeyset()) { if (KEY_FREEZER_DEBOUNCE_TIMEOUT.equals(name)) { updateFreezerDebounceTimeout(); + } else if (KEY_FREEZER_EXEMPT_INST_PKG.equals(name)) { + updateFreezerExemptInstPkg(); } } } @@ -357,6 +362,7 @@ public final class CachedAppOptimizer { private boolean mFreezerOverride = false; @VisibleForTesting volatile long mFreezerDebounceTimeout = DEFAULT_FREEZER_DEBOUNCE_TIMEOUT; + @VisibleForTesting volatile boolean mFreezerExemptInstPkg = DEFAULT_FREEZER_EXEMPT_INST_PKG; // Maps process ID to last compaction statistics for processes that we've fully compacted. Used // when evaluating throttles that we only consider for "full" compaction, so we don't store @@ -566,6 +572,15 @@ public final class CachedAppOptimizer { } } + /** + * Returns whether freezer exempts INSTALL_PACKAGES. + */ + public boolean freezerExemptInstPkg() { + synchronized (mPhenotypeFlagLock) { + return mUseFreezer && mFreezerExemptInstPkg; + } + } + @GuardedBy("mProcLock") void dump(PrintWriter pw) { pw.println("CachedAppOptimizer settings"); @@ -647,6 +662,7 @@ public final class CachedAppOptimizer { pw.println(" " + KEY_USE_FREEZER + "=" + mUseFreezer); pw.println(" " + KEY_FREEZER_STATSD_SAMPLE_RATE + "=" + mFreezerStatsdSampleRate); pw.println(" " + KEY_FREEZER_DEBOUNCE_TIMEOUT + "=" + mFreezerDebounceTimeout); + pw.println(" " + KEY_FREEZER_EXEMPT_INST_PKG + "=" + mFreezerExemptInstPkg); synchronized (mProcLock) { int size = mFrozenProcesses.size(); pw.println(" Apps frozen: " + size); @@ -1007,6 +1023,7 @@ public final class CachedAppOptimizer { KEY_USE_FREEZER, DEFAULT_USE_FREEZER)) { mUseFreezer = isFreezerSupported(); updateFreezerDebounceTimeout(); + updateFreezerExemptInstPkg(); } else { mUseFreezer = false; } @@ -1194,6 +1211,15 @@ public final class CachedAppOptimizer { if (mFreezerDebounceTimeout < 0) { mFreezerDebounceTimeout = DEFAULT_FREEZER_DEBOUNCE_TIMEOUT; } + Slog.d(TAG_AM, "Freezer timeout set to " + mFreezerDebounceTimeout); + } + + @GuardedBy("mPhenotypeFlagLock") + private void updateFreezerExemptInstPkg() { + mFreezerExemptInstPkg = DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_ACTIVITY_MANAGER_NATIVE_BOOT, + KEY_FREEZER_EXEMPT_INST_PKG, DEFAULT_FREEZER_EXEMPT_INST_PKG); + Slog.d(TAG_AM, "Freezer exemption set to " + mFreezerExemptInstPkg); } private boolean parseProcStateThrottle(String procStateThrottleString) { diff --git a/services/core/java/com/android/server/am/EventLogTags.logtags b/services/core/java/com/android/server/am/EventLogTags.logtags index 60e6754f4b6f..ea3c8dc1fa90 100644 --- a/services/core/java/com/android/server/am/EventLogTags.logtags +++ b/services/core/java/com/android/server/am/EventLogTags.logtags @@ -107,21 +107,21 @@ option java_package com.android.server.am 30079 uc_dispatch_user_switch (oldUserId|1|5),(newUserId|1|5) 30080 uc_continue_user_switch (oldUserId|1|5),(newUserId|1|5) 30081 uc_send_user_broadcast (userId|1|5),(IntentAction|3) + # Tags below are used by SystemServiceManager - although it's technically part of am, these are # also user switch events and useful to be analyzed together with events above. -30082 ssm_user_starting (userId|1|5),(visible|1) +30082 ssm_user_starting (userId|1|5) 30083 ssm_user_switching (oldUserId|1|5),(newUserId|1|5) 30084 ssm_user_unlocking (userId|1|5) 30085 ssm_user_unlocked (userId|1|5) -30086 ssm_user_stopping (userId|1|5),(visibilityChanged|1) +30086 ssm_user_stopping (userId|1|5) 30087 ssm_user_stopped (userId|1|5) 30088 ssm_user_completed_event (userId|1|5),(eventFlag|1|5) -30089 ssm_user_visibility_changed (userId|1|5),(visible|1) + +# Similarly, tags below are used by UserManagerService +30091 um_user_visibility_changed (userId|1|5),(visible|1) # Foreground service start/stop events. 30100 am_foreground_service_start (User|1|5),(Component Name|3),(allowWhileInUse|1),(startReasonCode|3),(targetSdk|1|1),(callerTargetSdk|1|1),(notificationWasDeferred|1),(notificationShown|1),(durationMs|1|3),(startForegroundCount|1|1),(stopReason|3) 30101 am_foreground_service_denied (User|1|5),(Component Name|3),(allowWhileInUse|1),(startReasonCode|3),(targetSdk|1|1),(callerTargetSdk|1|1),(notificationWasDeferred|1),(notificationShown|1),(durationMs|1|3),(startForegroundCount|1|1),(stopReason|3) 30102 am_foreground_service_stop (User|1|5),(Component Name|3),(allowWhileInUse|1),(startReasonCode|3),(targetSdk|1|1),(callerTargetSdk|1|1),(notificationWasDeferred|1),(notificationShown|1),(durationMs|1|3),(startForegroundCount|1|1),(stopReason|3) - - - diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java index 42bfc4cc7130..ecea96e927e3 100644 --- a/services/core/java/com/android/server/am/ProcessList.java +++ b/services/core/java/com/android/server/am/ProcessList.java @@ -1693,7 +1693,8 @@ public final class ProcessList { app.info.packageName); externalStorageAccess = storageManagerInternal.hasExternalStorageAccess(uid, app.info.packageName); - if (pm.checkPermission(Manifest.permission.INSTALL_PACKAGES, + if (mService.isAppFreezerExemptInstPkg() + && pm.checkPermission(Manifest.permission.INSTALL_PACKAGES, app.info.packageName, userId) == PackageManager.PERMISSION_GRANTED) { Slog.i(TAG, app.info.packageName + " is exempt from freezer"); diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index bffbdd0de5b6..92133274c0cc 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -177,9 +177,7 @@ class UserController implements Handler.Callback { static final int START_USER_SWITCH_FG_MSG = 120; static final int COMPLETE_USER_SWITCH_MSG = 130; static final int USER_COMPLETED_EVENT_MSG = 140; - static final int USER_VISIBLE_MSG = 150; - - private static final int NO_ARG2 = 0; + static final int USER_VISIBILITY_CHANGED_MSG = 150; // Message constant to clear {@link UserJourneySession} from {@link mUserIdToUserJourneyMap} if // the user journey, defined in the UserLifecycleJourneyReported atom for statsd, is not @@ -439,10 +437,13 @@ class UserController implements Handler.Callback { /** @see #getLastUserUnlockingUptime */ private volatile long mLastUserUnlockingUptime = 0; + // TODO(b/244333150) remove this array and let UserVisibilityMediator call the listeners + // directly, as that class should be responsible for all user visibility logic (for example, + // when the foreground user is switched out, its profiles also become invisible) /** * List of visible users (as defined by {@link UserManager#isUserVisible()}). * - * <p>It's only used to call {@link SystemServiceManager} when the visibility is changed upon + * <p>It's only used to call {@link UserManagerInternal} when the visibility is changed upon * the user starting or stopping. * * <p>Note: only the key is used, not the value. @@ -1096,10 +1097,7 @@ class UserController implements Handler.Callback { synchronized (mLock) { visibleBefore = mVisibleUsers.get(userId); if (visibleBefore) { - if (DEBUG_MU) { - Slogf.d(TAG, "Removing %d from mVisibleUsers", userId); - } - mVisibleUsers.delete(userId); + deleteVisibleUserLocked(userId); visibilityChanged = true; } else { visibilityChanged = false; @@ -1148,6 +1146,20 @@ class UserController implements Handler.Callback { } } + private void addVisibleUserLocked(@UserIdInt int userId) { + if (DEBUG_MU) { + Slogf.d(TAG, "adding %d to mVisibleUsers", userId); + } + mVisibleUsers.put(userId, true); + } + + private void deleteVisibleUserLocked(@UserIdInt int userId) { + if (DEBUG_MU) { + Slogf.d(TAG, "deleting %d from mVisibleUsers", userId); + } + mVisibleUsers.delete(userId); + } + private void finishUserStopping(final int userId, final UserState uss, final boolean allowDelayedLocking, final boolean visibilityChanged) { EventLog.writeEvent(EventLogTags.UC_FINISH_USER_STOPPING, userId); @@ -1166,7 +1178,10 @@ class UserController implements Handler.Callback { mInjector.batteryStatsServiceNoteEvent( BatteryStats.HistoryItem.EVENT_USER_RUNNING_FINISH, Integer.toString(userId), userId); - mInjector.getSystemServiceManager().onUserStopping(userId, visibilityChanged); + mInjector.getSystemServiceManager().onUserStopping(userId); + if (visibilityChanged) { + mInjector.onUserVisibilityChanged(userId, /* visible= */ false); + } Runnable finishUserStoppedAsync = () -> mHandler.post(() -> finishUserStopped(uss, allowDelayedLocking)); @@ -1635,7 +1650,12 @@ class UserController implements Handler.Callback { return false; } - mInjector.getUserManagerInternal().assignUserToDisplay(userId, displayId); + 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); + } // TODO(b/239982558): might need something similar for bg users on secondary display if (foreground && isUserSwitchUiEnabled()) { @@ -1687,12 +1707,23 @@ class UserController implements Handler.Callback { // Make sure the old user is no longer considering the display to be on. mInjector.reportGlobalUsageEvent(UsageEvents.Event.SCREEN_NON_INTERACTIVE); boolean userSwitchUiEnabled; + // TODO(b/244333150): temporary state until the callback logic is moved to + // UserVisibilityManager + int previousCurrentUserId; boolean notifyPreviousCurrentUserId; synchronized (mLock) { + previousCurrentUserId = mCurrentUserId; + notifyPreviousCurrentUserId = mVisibleUsers.get(previousCurrentUserId); + if (notifyPreviousCurrentUserId) { + deleteVisibleUserLocked(previousCurrentUserId); + } mCurrentUserId = userId; mTargetUserId = UserHandle.USER_NULL; // reset, mCurrentUserId has caught up userSwitchUiEnabled = mUserSwitchUiEnabled; } mInjector.updateUserConfiguration(); + // TODO(b/244644281): updateProfileRelatedCaches() is called on both if and else + // parts, ideally it should be moved outside, but for now it's not as there are many + // calls to external components here afterwards updateProfileRelatedCaches(); mInjector.getWindowManager().setCurrentUser(userId); mInjector.reportCurWakefulnessUsageEvent(); @@ -1705,6 +1736,11 @@ class UserController implements Handler.Callback { mInjector.getWindowManager().lockNow(null); } } + if (notifyPreviousCurrentUserId) { + mHandler.sendMessage(mHandler.obtainMessage(USER_VISIBILITY_CHANGED_MSG, + previousCurrentUserId, 0)); + } + } else { final Integer currentUserIdInt = mCurrentUserId; updateProfileRelatedCaches(); @@ -1730,10 +1766,7 @@ class UserController implements Handler.Callback { && mInjector.getUserManagerInternal().isUserVisible(userId); if (visible) { synchronized (mLock) { - if (DEBUG_MU) { - Slogf.d(TAG, "Adding %d to mVisibleUsers", userId); - } - mVisibleUsers.put(userId, true); + addVisibleUserLocked(userId); } } @@ -1776,12 +1809,15 @@ class UserController implements Handler.Callback { mHandler.sendMessage(mHandler.obtainMessage(USER_START_MSG, userId, visible ? 1 : 0)); t.traceEnd(); - } else if (visible) { + } + + if (visible) { // User was already running and became visible (for example, when switching to a // 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_VISIBLE_MSG, userId, NO_ARG2)); + mHandler.sendMessage(mHandler.obtainMessage(USER_VISIBILITY_CHANGED_MSG, userId, + visible ? 1 : 0)); } t.traceBegin("sendMessages"); @@ -2573,7 +2609,8 @@ class UserController implements Handler.Callback { if (!UserManager.isHeadlessSystemUserMode()) { // Don't need to call on HSUM because it will be called when the system user is // restarted on background - mInjector.onUserStarting(UserHandle.USER_SYSTEM, /* visible= */ true); + mInjector.onUserStarting(UserHandle.USER_SYSTEM); + mInjector.onUserVisibilityChanged(UserHandle.USER_SYSTEM, /* visible= */ true); } } @@ -2585,12 +2622,12 @@ class UserController implements Handler.Callback { int userId = UserHandle.USER_SYSTEM; synchronized (mLock) { if (visible) { - mVisibleUsers.put(userId, true); + addVisibleUserLocked(userId); } else { - mVisibleUsers.delete(userId); + deleteVisibleUserLocked(userId); } } - mInjector.notifySystemUserVisibilityChanged(visible); + mInjector.onUserVisibilityChanged(userId, visible); t.traceEnd(); } @@ -3090,7 +3127,7 @@ class UserController implements Handler.Callback { logUserLifecycleEvent(msg.arg1, USER_LIFECYCLE_EVENT_START_USER, USER_LIFECYCLE_EVENT_STATE_BEGIN); - mInjector.onUserStarting(/* userId= */ msg.arg1, /* visible= */ msg.arg2 == 1); + mInjector.onUserStarting(/* userId= */ msg.arg1); scheduleOnUserCompletedEvent(msg.arg1, UserCompletedEventType.EVENT_TYPE_USER_STARTING, USER_COMPLETED_EVENT_DELAY_MS); @@ -3171,8 +3208,9 @@ class UserController implements Handler.Callback { case COMPLETE_USER_SWITCH_MSG: completeUserSwitch(msg.arg1); break; - case USER_VISIBLE_MSG: - mInjector.getSystemServiceManager().onUserVisible(/* userId= */ msg.arg1); + case USER_VISIBILITY_CHANGED_MSG: + mInjector.onUserVisibilityChanged(/* userId= */ msg.arg1, + /* visible= */ msg.arg2 == 1); break; } return false; @@ -3704,12 +3742,12 @@ class UserController implements Handler.Callback { return UserManager.isUsersOnSecondaryDisplaysEnabled(); } - void onUserStarting(int userId, boolean visible) { - getSystemServiceManager().onUserStarting(TimingsTraceAndSlog.newAsyncLog(), userId, - visible); + void onUserStarting(@UserIdInt int userId) { + getSystemServiceManager().onUserStarting(TimingsTraceAndSlog.newAsyncLog(), userId); } - void notifySystemUserVisibilityChanged(boolean visible) { - getSystemServiceManager().onSystemUserVisibilityChanged(visible); + + void onUserVisibilityChanged(@UserIdInt int userId, boolean visible) { + getUserManagerInternal().onUserVisibilityChanged(userId, visible); } } } diff --git a/services/core/java/com/android/server/app/GameManagerService.java b/services/core/java/com/android/server/app/GameManagerService.java index efa2f25bf3e3..31d707da014f 100644 --- a/services/core/java/com/android/server/app/GameManagerService.java +++ b/services/core/java/com/android/server/app/GameManagerService.java @@ -883,6 +883,7 @@ public final class GameManagerService extends IGameManagerService.Stub { @Override public void onUserStarting(@NonNull TargetUser user) { + Slog.d(TAG, "Starting user " + user.getUserIdentifier()); mService.onUserStarting(user, Environment.getDataSystemDeDirectory(user.getUserIdentifier())); } @@ -1047,6 +1048,8 @@ public final class GameManagerService extends IGameManagerService.Stub { "com.android.server.app.GameManagerService"); if (!mSettings.containsKey(userId)) { + Slog.d(TAG, "Failed to set game mode for package " + packageName + + " as user " + userId + " is not started"); return; } GameManagerSettings userSettings = mSettings.get(userId); 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/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/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index d35d193ebff2..78b697d13f7b 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -1716,7 +1716,20 @@ public final class DisplayManagerService extends SystemService { final Point userPreferredResolution = mPersistentDataStore.getUserPreferredResolution(device); final float refreshRate = mPersistentDataStore.getUserPreferredRefreshRate(device); - if (userPreferredResolution == null && Float.isNaN(refreshRate)) { + // If value in persistentDataStore is null, preserving the mode from systemPreferredMode. + // This is required because in some devices, user-preferred mode was not stored in + // persistentDataStore, but was stored in a config which is returned through + // systemPreferredMode. + if ((userPreferredResolution == null && Float.isNaN(refreshRate)) + || (userPreferredResolution.equals(0, 0) && refreshRate == 0.0f)) { + Display.Mode systemPreferredMode = device.getSystemPreferredDisplayModeLocked(); + if (systemPreferredMode == null) { + return; + } + storeModeInPersistentDataStoreLocked( + display.getDisplayIdLocked(), systemPreferredMode.getPhysicalWidth(), + systemPreferredMode.getPhysicalHeight(), systemPreferredMode.getRefreshRate()); + device.setUserPreferredDisplayModeLocked(systemPreferredMode); return; } Display.Mode.Builder modeBuilder = new Display.Mode.Builder(); 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/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..e5c48f9c0435 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,6 +2680,8 @@ public class InputManagerService extends IInputManager.Stub @EnforcePermission(Manifest.permission.BLUETOOTH) @Override public String getInputDeviceBluetoothAddress(int deviceId) { + super.getInputDeviceBluetoothAddress_enforcePermission(); + return mNative.getBluetoothAddress(deviceId); } @@ -2689,74 +2695,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 +3792,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 +3866,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/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 27d966e8673e..8b083bd72722 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -164,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; @@ -3212,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) { @@ -3220,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; @@ -3236,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) { @@ -5387,6 +5403,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mCurrentSubtype = getCurrentInputMethodSubtypeLocked(); } } + notifyInputMethodSubtypeChangedLocked(mSettings.getCurrentUserId(), imi, mCurrentSubtype); if (!setSubtypeOnly) { // Set InputMethod here diff --git a/services/core/java/com/android/server/location/LocationManagerService.java b/services/core/java/com/android/server/location/LocationManagerService.java index dcec0aaf9358..2669d217120f 100644 --- a/services/core/java/com/android/server/location/LocationManagerService.java +++ b/services/core/java/com/android/server/location/LocationManagerService.java @@ -140,7 +140,9 @@ import com.android.server.location.provider.StationaryThrottlingLocationProvider import com.android.server.location.provider.proxy.ProxyLocationProvider; import com.android.server.location.settings.LocationSettings; import com.android.server.location.settings.LocationUserSettings; +import com.android.server.pm.UserManagerInternal; import com.android.server.pm.permission.LegacyPermissionManagerInternal; +import com.android.server.utils.Slogf; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -308,6 +310,10 @@ public class LocationManagerService extends ILocationManager.Stub implements permissionManagerInternal.setLocationExtraPackagesProvider( userId -> mContext.getResources().getStringArray( com.android.internal.R.array.config_locationExtraPackageNames)); + + // TODO(b/241604546): properly handle this callback + LocalServices.getService(UserManagerInternal.class).addUserVisibilityListener( + (u, v) -> Slogf.i(TAG, "onUserVisibilityChanged(): %d -> %b", u, v)); } @Nullable 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/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/UserManagerInternal.java b/services/core/java/com/android/server/pm/UserManagerInternal.java index 56ec8e46291d..91558308e305 100644 --- a/services/core/java/com/android/server/pm/UserManagerInternal.java +++ b/services/core/java/com/android/server/pm/UserManagerInternal.java @@ -77,6 +77,23 @@ public abstract class UserManagerInternal { } /** + * Listener for {@link UserManager#isUserVisible() user visibility} changes. + */ + public interface UserVisibilityListener { + + /** + * Called when the {@link UserManager#isUserVisible() user visibility} changed. + * + * <p><b>Note:</b> this method is called independently of + * {@link com.android.server.SystemService} callbacks; for example, the call with + * {@code visible} {@code true} might be called before the + * {@link com.android.server.SystemService#onUserStarting(com.android.server.SystemService.TargetUser)} + * call. + */ + void onUserVisibilityChanged(@UserIdInt int userId, boolean visible); + } + + /** * Called by {@link com.android.server.devicepolicy.DevicePolicyManagerService} to set * restrictions enforced by the user. * @@ -331,13 +348,18 @@ public abstract class UserManagerInternal { * <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). * + * <p><b>NOTE: </b>this method is meant to be used only by {@code UserController} (when a user + * is started) + * * <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). - * */ - public abstract void assignUserToDisplay(@UserIdInt int userId, int displayId); + // 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, + boolean foreground, int displayId); /** * Unassigns a user from its current display. @@ -346,7 +368,7 @@ public abstract class UserManagerInternal { * multiple users on multiple displays (like automotives with passenger displays). * * <p><b>NOTE: </b>this method is meant to be used only by {@code UserController} (when a user - * is stopped) and {@code DisplayManagerService} (when a virtual display is destroyed). + * is stopped). */ public abstract void unassignUserFromDisplay(@UserIdInt int userId); @@ -390,4 +412,13 @@ public abstract class UserManagerInternal { * would make such call). */ public abstract @UserIdInt int getUserAssignedToDisplay(int displayId); + + /** Adds a {@link UserVisibilityListener}. */ + public abstract void addUserVisibilityListener(UserVisibilityListener listener); + + /** Removes a {@link UserVisibilityListener}. */ + public abstract void removeUserVisibilityListener(UserVisibilityListener listener); + + /** TODO(b/244333150): temporary method until UserVisibilityMediator handles that logic */ + public abstract void onUserVisibilityChanged(@UserIdInt int userId, boolean visible); } diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 0eff9e9a8e4d..d25566980fbb 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -96,6 +96,7 @@ import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.AtomicFile; +import android.util.EventLog; import android.util.IndentingPrintWriter; import android.util.IntArray; import android.util.Slog; @@ -124,9 +125,11 @@ import com.android.server.BundleUtils; import com.android.server.LocalServices; import com.android.server.LockGuard; import com.android.server.SystemService; +import com.android.server.am.EventLogTags; import com.android.server.am.UserState; import com.android.server.pm.UserManagerInternal.UserLifecycleListener; import com.android.server.pm.UserManagerInternal.UserRestrictionsListener; +import com.android.server.pm.UserManagerInternal.UserVisibilityListener; import com.android.server.storage.DeviceStorageMonitorInternal; import com.android.server.utils.Slogf; import com.android.server.utils.TimingsTraceAndSlog; @@ -504,6 +507,10 @@ public class UserManagerService extends IUserManager.Stub { @GuardedBy("mUserLifecycleListeners") private final ArrayList<UserLifecycleListener> mUserLifecycleListeners = new ArrayList<>(); + // TODO(b/244333150): temporary array, should belong to UserVisibilityMediator + @GuardedBy("mUserVisibilityListeners") + private final ArrayList<UserVisibilityListener> mUserVisibilityListeners = new ArrayList<>(); + private final LockPatternUtils mLockPatternUtils; private final String ACTION_DISABLE_QUIET_MODE_AFTER_UNLOCK = @@ -626,7 +633,7 @@ public class UserManagerService extends IUserManager.Stub { @GuardedBy("mUserStates") private final WatchedUserStates mUserStates = new WatchedUserStates(); - private final UserVisibilityMediator mUserVisibilityMediator; + private final UserVisibilityMediator mUserVisibilityMediator = new UserVisibilityMediator(); private static UserManagerService sInstance; @@ -749,7 +756,6 @@ public class UserManagerService extends IUserManager.Stub { mUserStates.put(UserHandle.USER_SYSTEM, UserState.STATE_BOOTING); mUser0Allocations = DBG_ALLOCATION ? new AtomicInteger() : null; emulateSystemUserModeIfNeeded(); - mUserVisibilityMediator = new UserVisibilityMediator(this); } void systemReady() { @@ -1776,7 +1782,7 @@ public class UserManagerService extends IUserManager.Stub { } @Override - public List<UserHandle> getVisibleUsers() { + public int[] getVisibleUsers() { if (!hasManageUsersOrPermission(android.Manifest.permission.INTERACT_ACROSS_USERS)) { throw new SecurityException("Caller needs MANAGE_USERS or INTERACT_ACROSS_USERS " + "permission to get list of visible users"); @@ -1784,18 +1790,19 @@ public class UserManagerService extends IUserManager.Stub { final long ident = Binder.clearCallingIdentity(); try { // TODO(b/2399825580): refactor into UserDisplayAssigner + IntArray visibleUsers; synchronized (mUsersLock) { int usersSize = mUsers.size(); - ArrayList<UserHandle> visibleUsers = new ArrayList<>(usersSize); + visibleUsers = new IntArray(); for (int i = 0; i < usersSize; i++) { UserInfo ui = mUsers.valueAt(i).info; if (!ui.partial && !ui.preCreated && !mRemovingUserIds.get(ui.id) && mUserVisibilityMediator.isUserVisible(ui.id)) { - visibleUsers.add(UserHandle.of(ui.id)); + visibleUsers.add(ui.id); } } - return visibleUsers; } + return visibleUsers.toArray(); } finally { Binder.restoreCallingIdentity(ident); } @@ -6147,7 +6154,7 @@ public class UserManagerService extends IUserManager.Stub { dumpUser(pw, UserHandle.parseUserArg(args[1]), sb, now, nowRealtime); return; case "--visibility-mediator": - mUserVisibilityMediator.dump(pw); + mUserVisibilityMediator.dump(pw, args); return; } } @@ -6213,7 +6220,7 @@ public class UserManagerService extends IUserManager.Stub { } // synchronized (mPackagesLock) pw.println(); - mUserVisibilityMediator.dump(pw); + mUserVisibilityMediator.dump(pw, args); pw.println(); // Dump some capabilities @@ -6250,6 +6257,9 @@ public class UserManagerService extends IUserManager.Stub { synchronized (mUserLifecycleListeners) { pw.println(" user lifecycle events: " + mUserLifecycleListeners.size()); } + synchronized (mUserVisibilityListeners) { + pw.println(" user visibility events: " + mUserVisibilityListeners.size()); + } // Dump UserTypes pw.println(); @@ -6789,13 +6799,16 @@ public class UserManagerService extends IUserManager.Stub { } @Override - public void assignUserToDisplay(@UserIdInt int userId, int displayId) { - mUserVisibilityMediator.assignUserToDisplay(userId, displayId); + public void assignUserToDisplay(@UserIdInt int userId, @UserIdInt int profileGroupId, + boolean foreground, int displayId) { + mUserVisibilityMediator.startUser(userId, profileGroupId, foreground, displayId); + mUserVisibilityMediator.assignUserToDisplay(userId, profileGroupId, displayId); } @Override public void unassignUserFromDisplay(@UserIdInt int userId) { mUserVisibilityMediator.unassignUserFromDisplay(userId); + mUserVisibilityMediator.stopUser(userId); } @Override @@ -6817,8 +6830,39 @@ public class UserManagerService extends IUserManager.Stub { public @UserIdInt int getUserAssignedToDisplay(int displayId) { return mUserVisibilityMediator.getUserAssignedToDisplay(displayId); } + + @Override + public void addUserVisibilityListener(UserVisibilityListener listener) { + synchronized (mUserVisibilityListeners) { + mUserVisibilityListeners.add(listener); + } + } + + @Override + public void removeUserVisibilityListener(UserVisibilityListener listener) { + synchronized (mUserVisibilityListeners) { + mUserVisibilityListeners.remove(listener); + } + } + + @Override + public void onUserVisibilityChanged(@UserIdInt int userId, boolean visible) { + EventLog.writeEvent(EventLogTags.UM_USER_VISIBILITY_CHANGED, userId, visible ? 1 : 0); + mHandler.post(() -> { + UserVisibilityListener[] listeners; + synchronized (mUserVisibilityListeners) { + listeners = new UserVisibilityListener[mUserVisibilityListeners.size()]; + mUserVisibilityListeners.toArray(listeners); + } + for (UserVisibilityListener listener : listeners) { + listener.onUserVisibilityChanged(userId, visible); + } + }); + } } // class LocalService + + /** * Check if user has restrictions * @param restriction restrictions to check 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..b8950e1c6fb6 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)); } /** @@ -310,6 +312,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. diff --git a/services/core/java/com/android/server/pm/UserVisibilityMediator.java b/services/core/java/com/android/server/pm/UserVisibilityMediator.java index f725c486ec1f..bd81062b0ff1 100644 --- a/services/core/java/com/android/server/pm/UserVisibilityMediator.java +++ b/services/core/java/com/android/server/pm/UserVisibilityMediator.java @@ -15,10 +15,18 @@ */ package com.android.server.pm; +import static android.content.pm.UserInfo.NO_PROFILE_GROUP_ID; +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 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; import android.view.Display; @@ -29,6 +37,8 @@ import com.android.internal.util.Preconditions; import com.android.server.utils.Slogf; import java.io.PrintWriter; +import java.util.LinkedHashMap; +import java.util.Map; /** * Class responsible for deciding whether a user is visible (or visible for a given display). @@ -36,72 +46,146 @@ import java.io.PrintWriter; * <p>This class is thread safe. */ // TODO(b/244644281): improve javadoc (for example, explain all cases / modes) -public final class UserVisibilityMediator { +public final class UserVisibilityMediator implements Dumpable { private static final boolean DBG = false; // DO NOT SUBMIT WITH TRUE private static final String TAG = UserVisibilityMediator.class.getSimpleName(); - private final Object mLock = new Object(); + 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; - // TODO(b/244644281): should not depend on service, but keep its own internal state (like - // current user and profile groups), but it is initially as the code was just moved from UMS - // "as is". Similarly, it shouldn't need to pass the SparseIntArray on constructor (which was - // added to UMS for testing purposes) - private final UserManagerService mService; + @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; + @UserIdInt + @GuardedBy("mLock") + private int mCurrentUserId = INITIAL_CURRENT_USER_ID; + @Nullable @GuardedBy("mLock") - private final SparseIntArray mUsersOnSecondaryDisplays; + private final SparseIntArray mUsersOnSecondaryDisplays = new SparseIntArray(); - UserVisibilityMediator(UserManagerService service) { - this(service, UserManager.isUsersOnSecondaryDisplaysEnabled(), - /* usersOnSecondaryDisplays= */ null); + /** + * Mapping from each started user to its profile group. + */ + @GuardedBy("mLock") + private final SparseIntArray mStartedProfileGroupIds = new SparseIntArray(); + + UserVisibilityMediator() { + this(UserManager.isUsersOnSecondaryDisplaysEnabled()); } @VisibleForTesting - UserVisibilityMediator(UserManagerService service, boolean usersOnSecondaryDisplaysEnabled, - @Nullable SparseIntArray usersOnSecondaryDisplays) { - mService = service; + UserVisibilityMediator(boolean usersOnSecondaryDisplaysEnabled) { mUsersOnSecondaryDisplaysEnabled = usersOnSecondaryDisplaysEnabled; - if (mUsersOnSecondaryDisplaysEnabled) { - mUsersOnSecondaryDisplays = usersOnSecondaryDisplays == null - ? new SparseIntArray() // default behavior - : usersOnSecondaryDisplays; // passed by unit test - } else { - mUsersOnSecondaryDisplays = null; + } + + /** + * TODO(b/244644281): merge with assignUserToDisplay() or add javadoc. + */ + public @StartUserResult int startUser(@UserIdInt int userId, @UserIdInt int profileGroupId, + boolean foreground, int displayId) { + int actualProfileGroupId = profileGroupId == NO_PROFILE_GROUP_ID + ? userId + : profileGroupId; + if (DBG) { + Slogf.d(TAG, "startUser(%d, %d, %b, %d): actualProfileGroupId=%d", + userId, profileGroupId, foreground, displayId, actualProfileGroupId); + } + 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; + } + + int visibility; + synchronized (mLock) { + if (isProfile(userId, actualProfileGroupId)) { + if (displayId != DEFAULT_DISPLAY) { + 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; + } + 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; + } else { + boolean isParentRunning = mStartedProfileGroupIds + .get(actualProfileGroupId) == actualProfileGroupId; + if (DBG) { + Slogf.d(TAG, "profile parent running: %b", isParentRunning); + } + visibility = isParentRunning + ? START_USER_RESULT_SUCCESS_VISIBLE + : START_USER_RESULT_SUCCESS_INVISIBLE; + } + } else if (foreground) { + mCurrentUserId = userId; + visibility = START_USER_RESULT_SUCCESS_VISIBLE; + } else { + visibility = START_USER_RESULT_SUCCESS_INVISIBLE; + } + if (DBG) { + Slogf.d(TAG, "adding user / profile mapping (%d -> %d) and returning %s", + userId, actualProfileGroupId, startUserResultToString(visibility)); + } + mStartedProfileGroupIds.put(userId, actualProfileGroupId); + } + return visibility; + } + + /** + * 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)}. */ - public void assignUserToDisplay(int userId, int displayId) { + public void assignUserToDisplay(int userId, int profileGroupId, int displayId) { if (DBG) { - Slogf.d(TAG, "assignUserToDisplay(%d, %d)", userId, displayId); + Slogf.d(TAG, "assignUserToDisplay(%d, %d): mUsersOnSecondaryDisplaysEnabled=%b", + userId, displayId, mUsersOnSecondaryDisplaysEnabled); } - // NOTE: Using Boolean instead of boolean as it will be re-used below - Boolean isProfile = null; - if (displayId == Display.DEFAULT_DISPLAY) { - if (mUsersOnSecondaryDisplaysEnabled) { - // Profiles are only supported in the default display, but it cannot return yet - // as it needs to check if the parent is also assigned to the DEFAULT_DISPLAY - // (this is done indirectly below when it checks that the profile parent is the - // current user, as the current user is always assigned to the DEFAULT_DISPLAY). - isProfile = isProfileUnchecked(userId); - } - if (isProfile == null || !isProfile) { - // 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. - if (DBG) { - Slogf.d(TAG, "ignoring on default display"); - } - return; + if (displayId == DEFAULT_DISPLAY + && (!mUsersOnSecondaryDisplaysEnabled || !isProfile(userId, profileGroupId))) { + // 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. + // But on MUMD devices, it profiles are only supported in the default display, so it + // cannot return yet as it needs to check if the parent is also assigned to the + // DEFAULT_DISPLAY (this is done indirectly below when it checks that the profile parent + // is the current user, as the current user is always assigned to the DEFAULT_DISPLAY). + if (DBG) { + Slogf.d(TAG, "ignoring on default display"); } + return; } if (!mUsersOnSecondaryDisplaysEnabled) { @@ -119,24 +203,21 @@ public final class UserVisibilityMediator { Preconditions.checkArgument(userId != currentUserId, "Cannot assign current user (%d) to other displays", currentUserId); - if (isProfile == null) { - isProfile = isProfileUnchecked(userId); - } - synchronized (mLock) { - if (isProfile) { - // Profile can only start in the same display as parent. And for simplicity, - // that display must be the DEFAULT_DISPLAY. - Preconditions.checkArgument(displayId == Display.DEFAULT_DISPLAY, - "Profile user can only be started in the default display"); - int parentUserId = getProfileParentId(userId); - Preconditions.checkArgument(parentUserId == currentUserId, - "Only profile of current user can be assigned to a display"); - if (DBG) { - Slogf.d(TAG, "Ignoring profile user %d on default display", userId); - } - return; + if (isProfile(userId, profileGroupId)) { + // Profile can only start in the same display as parent. And for simplicity, + // that display must be the DEFAULT_DISPLAY. + Preconditions.checkArgument(displayId == Display.DEFAULT_DISPLAY, + "Profile user can only be started in the default display"); + int parentUserId = getStartedProfileGroupId(userId); + Preconditions.checkArgument(parentUserId == currentUserId, + "Only profile of current user can be assigned to a display"); + if (DBG) { + Slogf.d(TAG, "Ignoring profile user %d on default display", userId); } + return; + } + synchronized (mLock) { // Check if display is available for (int i = 0; i < mUsersOnSecondaryDisplays.size(); i++) { int assignedUserId = mUsersOnSecondaryDisplays.keyAt(i); @@ -289,7 +370,7 @@ public final class UserVisibilityMediator { continue; } int userId = mUsersOnSecondaryDisplays.keyAt(i); - if (!isProfileUnchecked(userId)) { + if (!isStartedProfile(userId)) { return userId; } else if (DBG) { Slogf.d(TAG, "getUserAssignedToDisplay(%d): skipping user %d because it's " @@ -307,23 +388,42 @@ public final class UserVisibilityMediator { } private void dump(IndentingPrintWriter ipw) { - ipw.println("UserVisibilityManager"); + ipw.println("UserVisibilityMediator"); ipw.increaseIndent(); - ipw.print("Supports users on secondary displays: "); - ipw.println(mUsersOnSecondaryDisplaysEnabled); + synchronized (mLock) { + 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(); + } - if (mUsersOnSecondaryDisplaysEnabled) { - ipw.print("Users on secondary displays: "); - synchronized (mLock) { - ipw.println(mUsersOnSecondaryDisplays); + ipw.print("Supports users on secondary displays: "); + ipw.println(mUsersOnSecondaryDisplaysEnabled); + + if (mUsersOnSecondaryDisplaysEnabled) { + ipw.print("Users on secondary displays: "); + synchronized (mLock) { + ipw.println(mUsersOnSecondaryDisplays); + } } } ipw.decreaseIndent(); } - void dump(PrintWriter pw) { + @Override + public void dump(PrintWriter pw, String[] args) { if (pw instanceof IndentingPrintWriter) { dump((IndentingPrintWriter) pw); return; @@ -331,20 +431,70 @@ public final class UserVisibilityMediator { dump(new IndentingPrintWriter(pw)); } - // TODO(b/244644281): remove methods below once this class caches that state - private @UserIdInt int getCurrentUserId() { - return mService.getCurrentUserId(); + @VisibleForTesting + Map<Integer, Integer> getUsersOnSecondaryDisplays() { + Map<Integer, Integer> map; + synchronized (mLock) { + int size = mUsersOnSecondaryDisplays.size(); + map = new LinkedHashMap<>(size); + for (int i = 0; i < size; i++) { + map.put(mUsersOnSecondaryDisplays.keyAt(i), mUsersOnSecondaryDisplays.valueAt(i)); + } + } + Slogf.v(TAG, "getUsersOnSecondaryDisplays(): returning %s", map); + 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); } - private boolean isCurrentUserOrRunningProfileOfCurrentUser(@UserIdInt int userId) { - return mService.isCurrentUserOrRunningProfileOfCurrentUser(userId); + // 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. + + @VisibleForTesting + @UserIdInt int getCurrentUserId() { + synchronized (mLock) { + return mCurrentUserId; + } } - private boolean isProfileUnchecked(@UserIdInt int userId) { - return mService.isProfileUnchecked(userId); + @VisibleForTesting + boolean isCurrentUserOrRunningProfileOfCurrentUser(@UserIdInt int userId) { + synchronized (mLock) { + // Special case as NO_PROFILE_GROUP_ID == USER_NULL + if (userId == USER_NULL || mCurrentUserId == USER_NULL) { + return false; + } + if (mCurrentUserId == userId) { + return true; + } + return mStartedProfileGroupIds.get(userId, NO_PROFILE_GROUP_ID) == mCurrentUserId; + } } - private @UserIdInt int getProfileParentId(@UserIdInt int userId) { - return mService.getProfileParentId(userId); + private static boolean isProfile(@UserIdInt int userId, @UserIdInt int profileGroupId) { + return profileGroupId != NO_PROFILE_GROUP_ID && profileGroupId != userId; + } + + @VisibleForTesting + boolean isStartedProfile(@UserIdInt int userId) { + int profileGroupId; + synchronized (mLock) { + profileGroupId = mStartedProfileGroupIds.get(userId, NO_PROFILE_GROUP_ID); + } + return isProfile(userId, profileGroupId); + } + + @VisibleForTesting + @UserIdInt int getStartedProfileGroupId(@UserIdInt int userId) { + synchronized (mLock) { + return mStartedProfileGroupIds.get(userId, NO_PROFILE_GROUP_ID); + } } } 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..28f86edaa724 100644 --- a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java +++ b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java @@ -616,6 +616,10 @@ 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), @@ -933,6 +937,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/policy/SideFpsEventHandler.java b/services/core/java/com/android/server/policy/SideFpsEventHandler.java index 8582f5424321..2d76c5092b92 100644 --- a/services/core/java/com/android/server/policy/SideFpsEventHandler.java +++ b/services/core/java/com/android/server/policy/SideFpsEventHandler.java @@ -127,7 +127,7 @@ public class SideFpsEventHandler implements View.OnClickListener { */ public void notifyPowerPressed() { Log.i(TAG, "notifyPowerPressed"); - if (mFingerprintManager == null) { + if (mFingerprintManager == null && mSideFpsEventHandlerReady.get()) { mFingerprintManager = mContext.getSystemService(FingerprintManager.class); } if (mFingerprintManager == null) { 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/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java index 7dc4f9782e70..b8cd8d9f3d71 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java @@ -1157,6 +1157,8 @@ public class WallpaperManagerService extends IWallpaperManager.Stub Slog.w(TAG, "WallpaperService is not connected yet"); return; } + TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG); + t.traceBegin("WPMS.connectLocked-" + wallpaper.wallpaperComponent); if (DEBUG) Slog.v(TAG, "Adding window token: " + mToken); mWindowManagerInternal.addWindowToken(mToken, TYPE_WALLPAPER, mDisplayId, null /* options */); @@ -1173,6 +1175,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub false /* fromUser */, wallpaper, null /* reply */); } } + t.traceEnd(); } void disconnectLocked() { @@ -1322,6 +1325,8 @@ public class WallpaperManagerService extends IWallpaperManager.Stub @Override public void onServiceConnected(ComponentName name, IBinder service) { + TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG); + t.traceBegin("WPMS.onServiceConnected-" + name); synchronized (mLock) { if (mWallpaper.connection == this) { mService = IWallpaperService.Stub.asInterface(service); @@ -1338,6 +1343,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub mContext.getMainThreadHandler().removeCallbacks(mDisconnectRunnable); } } + t.traceEnd(); } @Override @@ -1545,6 +1551,8 @@ public class WallpaperManagerService extends IWallpaperManager.Stub public void engineShown(IWallpaperEngine engine) { synchronized (mLock) { if (mReply != null) { + TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG); + t.traceBegin("WPMS.mReply.sendResult"); final long ident = Binder.clearCallingIdentity(); try { mReply.sendResult(null); @@ -1553,6 +1561,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub } finally { Binder.restoreCallingIdentity(ident); } + t.traceEnd(); mReply = null; } } @@ -3058,6 +3067,8 @@ public class WallpaperManagerService extends IWallpaperManager.Stub return true; } + TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG); + t.traceBegin("WPMS.bindWallpaperComponentLocked-" + componentName); try { if (componentName == null) { componentName = mDefaultWallpaperComponent; @@ -3190,6 +3201,8 @@ public class WallpaperManagerService extends IWallpaperManager.Stub } Slog.w(TAG, msg); return false; + } finally { + t.traceEnd(); } return true; } @@ -3234,7 +3247,10 @@ public class WallpaperManagerService extends IWallpaperManager.Stub } private void attachServiceLocked(WallpaperConnection conn, WallpaperData wallpaper) { + TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG); + t.traceBegin("WPMS.attachServiceLocked"); conn.forEachDisplayConnector(connector-> connector.connectLocked(conn, wallpaper)); + t.traceEnd(); } private void notifyCallbacksLocked(WallpaperData wallpaper) { @@ -3360,6 +3376,8 @@ public class WallpaperManagerService extends IWallpaperManager.Stub } void saveSettingsLocked(int userId) { + TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG); + t.traceBegin("WPMS.saveSettingsLocked-" + userId); JournaledFile journal = makeJournaledFile(userId); FileOutputStream fstream = null; try { @@ -3388,6 +3406,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub IoUtils.closeQuietly(fstream); journal.rollback(); } + t.traceEnd(); } diff --git a/services/core/java/com/android/server/wm/AccessibilityWindowsPopulator.java b/services/core/java/com/android/server/wm/AccessibilityWindowsPopulator.java index d0c381e9debc..21b241a0d117 100644 --- a/services/core/java/com/android/server/wm/AccessibilityWindowsPopulator.java +++ b/services/core/java/com/android/server/wm/AccessibilityWindowsPopulator.java @@ -56,8 +56,11 @@ public final class AccessibilityWindowsPopulator extends WindowInfosListener { private static final int SURFACE_FLINGER_CALLBACK_WINDOWS_STABLE_TIMES_MS = 35; // To avoid the surface flinger callbacks always comes within in 2 frames, then no windows // are reported to the A11y framework, and the animation duration time is 500ms, so setting - // this value as the max timeout value to force computing changed windows. - private static final int WINDOWS_CHANGED_NOTIFICATION_MAX_DURATION_TIMES_MS = 500; + // this value as the max timeout value to force computing changed windows. However, since + // UiAutomator waits 500ms to determine that things are idle. Since we aren't actually idle, + // we need to reduce the timeout here a little so that we can deliver an updated state before + // UiAutomator reports idle based-on stale information. + private static final int WINDOWS_CHANGED_NOTIFICATION_MAX_DURATION_TIMES_MS = 450; private static final float[] sTempFloats = new float[9]; diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java index 59f37c27f637..7386a19009e0 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 diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 0eee8a329a1e..f6dce3b6ab1e 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(); + } } /** @@ -6896,11 +6908,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 +7910,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 +9213,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..e6d94920c00e 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() ); } } 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..9398bbec4a63 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -19,6 +19,8 @@ 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.TRANSIT_CLOSE; +import static android.view.WindowManager.TRANSIT_TO_BACK; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_BACK_PREVIEW; @@ -32,6 +34,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 +44,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 +62,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 +286,6 @@ class BackNavigationController { } if (prepareAnimation) { - infoBuilder.setDepartingWCT(toWindowContainerToken(currentTask)); prepareAnimationIfNeeded(currentTask, prevTask, prevActivity, removedWindowContainer, backType, adapter); } @@ -303,11 +304,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 +596,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 +636,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); } 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/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index b24ab4cb4a4c..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); + } } /** @@ -2089,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(); @@ -2131,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 = @@ -2593,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/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java index ea82417a2389..2dbccae6dde6 100644 --- a/services/core/java/com/android/server/wm/LetterboxUiController.java +++ b/services/core/java/com/android/server/wm/LetterboxUiController.java @@ -501,12 +501,16 @@ final class LetterboxUiController { if (hasVisibleTaskbar(mainWindow)) { cropBounds = new Rect(mActivityRecord.getBounds()); + + // Rounded corners should be displayed above the taskbar. + // It is important to call adjustBoundsForTaskbarUnchecked before offsetTo + // because taskbar bounds are in screen coordinates + adjustBoundsForTaskbarUnchecked(mainWindow, cropBounds); + // Activity bounds are in screen coordinates while (0,0) for activity's surface // control is at the top left corner of an app window so offsetting bounds // accordingly. cropBounds.offsetTo(0, 0); - // Rounded corners should be displayed above the taskbar. - adjustBoundsForTaskbarUnchecked(mainWindow, cropBounds); } transaction @@ -576,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/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/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 5485df6f6a34..3419207eb14f 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -426,7 +426,7 @@ public class WindowManagerService extends IWindowManager.Stub * @see #ENABLE_SHELL_TRANSITIONS */ public static final boolean sEnableShellTransitions = - SystemProperties.getBoolean(ENABLE_SHELL_TRANSITIONS, false); + SystemProperties.getBoolean(ENABLE_SHELL_TRANSITIONS, true); /** * Allows a fullscreen windowing mode activity to launch in its desired orientation directly @@ -1843,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); @@ -8903,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/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index b4c377745aba..5c5c70334e65 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -476,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; @@ -1257,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; } /** @@ -3866,7 +3868,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 @@ -6026,7 +6029,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..31971246e9e7 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -450,6 +450,10 @@ 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"; 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/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/tests/mockingservicestests/src/com/android/server/DumpableDumperRule.java b/services/tests/mockingservicestests/src/com/android/server/DumpableDumperRule.java new file mode 100644 index 000000000000..33275bd471f6 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/DumpableDumperRule.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server; + +import android.util.Dumpable; +import android.util.Log; + +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; + +/** + * {@code JUnit} rule that logs (using tag {@value #TAG} the contents of + * {@link Dumpable dumpables} in case of failure. + */ +public final class DumpableDumperRule implements TestRule { + + private static final String TAG = DumpableDumperRule.class.getSimpleName(); + + private static final String[] NO_ARGS = {}; + + private final List<Dumpable> mDumpables = new ArrayList<>(); + + /** + * Adds a {@link Dumpable} to be logged if the test case fails. + */ + public void addDumpable(Dumpable dumpable) { + mDumpables.add(dumpable); + } + + @Override + public Statement apply(Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + try { + base.evaluate(); + } catch (Throwable t) { + dumpOnFailure(description.getMethodName()); + throw t; + } + } + }; + } + + private void dumpOnFailure(String testName) throws IOException { + if (mDumpables.isEmpty()) { + return; + } + Log.w(TAG, "Dumping " + mDumpables.size() + " dumpables on failure of " + testName); + mDumpables.forEach(d -> logDumpable(d)); + } + + private void logDumpable(Dumpable dumpable) { + try { + try (StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw)) { + dumpable.dump(pw, NO_ARGS); + String[] dump = sw.toString().split(System.lineSeparator()); + Log.w(TAG, "Dumping " + dumpable.getDumpableName() + " (" + dump.length + + " lines):"); + for (String line : dump) { + Log.w(TAG, line); + } + + } catch (RuntimeException e) { + Log.e(TAG, "RuntimeException dumping " + dumpable.getDumpableName(), e); + } + } catch (IOException e) { + Log.e(TAG, "IOException dumping " + dumpable.getDumpableName(), e); + } + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/ExtendedMockitoTestCase.java b/services/tests/mockingservicestests/src/com/android/server/ExtendedMockitoTestCase.java index 9aa28ce741aa..c0b507068358 100644 --- a/services/tests/mockingservicestests/src/com/android/server/ExtendedMockitoTestCase.java +++ b/services/tests/mockingservicestests/src/com/android/server/ExtendedMockitoTestCase.java @@ -23,6 +23,7 @@ import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.mockito.MockitoSession; import org.mockito.quality.Strictness; @@ -38,6 +39,9 @@ public abstract class ExtendedMockitoTestCase { private MockitoSession mSession; + @Rule + public final DumpableDumperRule mDumpableDumperRule = new DumpableDumperRule(); + @Before public void startSession() { if (DEBUG) { 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/pm/UserVisibilityMediatorMUMDTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUMDTest.java index 21f541f54790..923c3e385b5e 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUMDTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUMDTest.java @@ -44,14 +44,14 @@ public final class UserVisibilityMediatorMUMDTest extends UserVisibilityMediator @Test public void testAssignUserToDisplay_systemUser() { - assertThrows(IllegalArgumentException.class, - () -> mMediator.assignUserToDisplay(USER_SYSTEM, SECONDARY_DISPLAY_ID)); + assertThrows(IllegalArgumentException.class, () -> mMediator + .assignUserToDisplay(USER_SYSTEM, USER_SYSTEM, SECONDARY_DISPLAY_ID)); } @Test public void testAssignUserToDisplay_invalidDisplay() { assertThrows(IllegalArgumentException.class, - () -> mMediator.assignUserToDisplay(USER_ID, INVALID_DISPLAY)); + () -> mMediator.assignUserToDisplay(USER_ID, USER_ID, INVALID_DISPLAY)); } @Test @@ -59,7 +59,7 @@ public final class UserVisibilityMediatorMUMDTest extends UserVisibilityMediator mockCurrentUser(USER_ID); assertThrows(IllegalArgumentException.class, - () -> mMediator.assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID)); + () -> mMediator.assignUserToDisplay(USER_ID, USER_ID, SECONDARY_DISPLAY_ID)); assertNoUserAssignedToDisplay(); } @@ -67,11 +67,10 @@ public final class UserVisibilityMediatorMUMDTest extends UserVisibilityMediator @Test public void testAssignUserToDisplay_startedProfileOfCurrentUser() { mockCurrentUser(PARENT_USER_ID); - addDefaultProfileAndParent(); startDefaultProfile(); - IllegalArgumentException e = assertThrows(IllegalArgumentException.class, - () -> mMediator.assignUserToDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID)); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> mMediator + .assignUserToDisplay(PROFILE_USER_ID, PARENT_USER_ID, SECONDARY_DISPLAY_ID)); Log.v(TAG, "Exception: " + e); assertNoUserAssignedToDisplay(); @@ -80,11 +79,10 @@ public final class UserVisibilityMediatorMUMDTest extends UserVisibilityMediator @Test public void testAssignUserToDisplay_stoppedProfileOfCurrentUser() { mockCurrentUser(PARENT_USER_ID); - addDefaultProfileAndParent(); stopDefaultProfile(); - IllegalArgumentException e = assertThrows(IllegalArgumentException.class, - () -> mMediator.assignUserToDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID)); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> mMediator + .assignUserToDisplay(PROFILE_USER_ID, PARENT_USER_ID, SECONDARY_DISPLAY_ID)); Log.v(TAG, "Exception: " + e); assertNoUserAssignedToDisplay(); @@ -92,17 +90,17 @@ public final class UserVisibilityMediatorMUMDTest extends UserVisibilityMediator @Test public void testAssignUserToDisplay_displayAvailable() { - mMediator.assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID); + mMediator.assignUserToDisplay(USER_ID, USER_ID, SECONDARY_DISPLAY_ID); assertUserAssignedToDisplay(USER_ID, SECONDARY_DISPLAY_ID); } @Test public void testAssignUserToDisplay_displayAlreadyAssigned() { - mMediator.assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID); + mMediator.assignUserToDisplay(USER_ID, USER_ID, SECONDARY_DISPLAY_ID); - IllegalStateException e = assertThrows(IllegalStateException.class, - () -> mMediator.assignUserToDisplay(OTHER_USER_ID, SECONDARY_DISPLAY_ID)); + IllegalStateException e = assertThrows(IllegalStateException.class, () -> mMediator + .assignUserToDisplay(OTHER_USER_ID, OTHER_USER_ID, SECONDARY_DISPLAY_ID)); Log.v(TAG, "Exception: " + e); assertWithMessage("exception (%s) message", e).that(e).hasMessageThat() @@ -112,10 +110,10 @@ public final class UserVisibilityMediatorMUMDTest extends UserVisibilityMediator @Test public void testAssignUserToDisplay_userAlreadyAssigned() { - mMediator.assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID); + mMediator.assignUserToDisplay(USER_ID, USER_ID, SECONDARY_DISPLAY_ID); IllegalStateException e = assertThrows(IllegalStateException.class, - () -> mMediator.assignUserToDisplay(USER_ID, OTHER_SECONDARY_DISPLAY_ID)); + () -> mMediator.assignUserToDisplay(USER_ID, USER_ID, OTHER_SECONDARY_DISPLAY_ID)); Log.v(TAG, "Exception: " + e); assertWithMessage("exception (%s) message", e).that(e).hasMessageThat() @@ -127,11 +125,9 @@ public final class UserVisibilityMediatorMUMDTest extends UserVisibilityMediator @Test public void testAssignUserToDisplay_profileOnSameDisplayAsParent() { - addDefaultProfileAndParent(); - - mMediator.assignUserToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID); - IllegalArgumentException e = assertThrows(IllegalArgumentException.class, - () -> mMediator.assignUserToDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID)); + mMediator.assignUserToDisplay(PARENT_USER_ID, PARENT_USER_ID, SECONDARY_DISPLAY_ID); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> mMediator + .assignUserToDisplay(PROFILE_USER_ID, PARENT_USER_ID, SECONDARY_DISPLAY_ID)); Log.v(TAG, "Exception: " + e); assertUserAssignedToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID); @@ -139,11 +135,9 @@ public final class UserVisibilityMediatorMUMDTest extends UserVisibilityMediator @Test public void testAssignUserToDisplay_profileOnDifferentDisplayAsParent() { - addDefaultProfileAndParent(); - - mMediator.assignUserToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID); - IllegalArgumentException e = assertThrows(IllegalArgumentException.class, - () -> mMediator.assignUserToDisplay(PROFILE_USER_ID, OTHER_SECONDARY_DISPLAY_ID)); + mMediator.assignUserToDisplay(PARENT_USER_ID, PARENT_USER_ID, SECONDARY_DISPLAY_ID); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> mMediator + .assignUserToDisplay(PROFILE_USER_ID, PARENT_USER_ID, OTHER_SECONDARY_DISPLAY_ID)); Log.v(TAG, "Exception: " + e); assertUserAssignedToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID); @@ -151,11 +145,9 @@ public final class UserVisibilityMediatorMUMDTest extends UserVisibilityMediator @Test public void testAssignUserToDisplay_profileDefaultDisplayParentOnSecondaryDisplay() { - addDefaultProfileAndParent(); - - mMediator.assignUserToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID); - IllegalArgumentException e = assertThrows(IllegalArgumentException.class, - () -> mMediator.assignUserToDisplay(PROFILE_USER_ID, DEFAULT_DISPLAY)); + mMediator.assignUserToDisplay(PARENT_USER_ID, PARENT_USER_ID, SECONDARY_DISPLAY_ID); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> mMediator + .assignUserToDisplay(PROFILE_USER_ID, PARENT_USER_ID, DEFAULT_DISPLAY)); Log.v(TAG, "Exception: " + e); assertUserAssignedToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID); @@ -201,7 +193,6 @@ public final class UserVisibilityMediatorMUMDTest extends UserVisibilityMediator @Test public void testIsUserVisibleOnDisplay_startedProfileOfCurrentUserSecondaryDisplayAssignedToAnotherUser() { - addDefaultProfileAndParent(); startDefaultProfile(); mockCurrentUser(PARENT_USER_ID); assignUserToDisplay(OTHER_USER_ID, SECONDARY_DISPLAY_ID); @@ -212,7 +203,6 @@ public final class UserVisibilityMediatorMUMDTest extends UserVisibilityMediator @Test public void testIsUserVisibleOnDisplay_stoppedProfileOfCurrentUserSecondaryDisplayAssignedToAnotherUser() { - addDefaultProfileAndParent(); stopDefaultProfile(); mockCurrentUser(PARENT_USER_ID); assignUserToDisplay(OTHER_USER_ID, SECONDARY_DISPLAY_ID); @@ -223,7 +213,6 @@ public final class UserVisibilityMediatorMUMDTest extends UserVisibilityMediator @Test public void testIsUserVisibleOnDisplay_startedProfileOfCurrentUserOnUnassignedSecondaryDisplay() { - addDefaultProfileAndParent(); startDefaultProfile(); mockCurrentUser(PARENT_USER_ID); @@ -285,19 +274,6 @@ public final class UserVisibilityMediatorMUMDTest extends UserVisibilityMediator .that(mMediator.getUserAssignedToDisplay(SECONDARY_DISPLAY_ID)).isEqualTo(USER_ID); } - // TODO(b/244644281): scenario below shouldn't happen on "real life", as the profile cannot be - // started on secondary display if its parent isn't, so we might need to remove (or refactor - // this test) if/when the underlying logic changes - @Test - public void testGetUserAssignedToDisplay_profileOnSecondaryDisplay() { - addDefaultProfileAndParent(); - mockCurrentUser(USER_ID); - assignUserToDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID); - - assertWithMessage("getUserAssignedToDisplay(%s)", SECONDARY_DISPLAY_ID) - .that(mMediator.getUserAssignedToDisplay(SECONDARY_DISPLAY_ID)).isEqualTo(USER_ID); - } - // NOTE: we don't need to add tests for profiles (started / stopped profiles of bg user), as // getUserAssignedToDisplay() for bg users relies only on the user / display assignments } 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 7ae811793949..7af5f5d6b2fe 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorSUSDTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorSUSDTest.java @@ -39,27 +39,25 @@ public final class UserVisibilityMediatorSUSDTest extends UserVisibilityMediator mockCurrentUser(USER_ID); assertThrows(UnsupportedOperationException.class, - () -> mMediator.assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID)); + () -> mMediator.assignUserToDisplay(USER_ID, USER_ID, SECONDARY_DISPLAY_ID)); } @Test public void testAssignUserToDisplay_otherDisplay_startProfileOfcurrentUser() { mockCurrentUser(PARENT_USER_ID); - addDefaultProfileAndParent(); startDefaultProfile(); - assertThrows(UnsupportedOperationException.class, - () -> mMediator.assignUserToDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID)); + assertThrows(UnsupportedOperationException.class, () -> mMediator + .assignUserToDisplay(PROFILE_USER_ID, PARENT_USER_ID, SECONDARY_DISPLAY_ID)); } @Test public void testAssignUserToDisplay_otherDisplay_stoppedProfileOfcurrentUser() { mockCurrentUser(PARENT_USER_ID); - addDefaultProfileAndParent(); stopDefaultProfile(); - assertThrows(UnsupportedOperationException.class, - () -> mMediator.assignUserToDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID)); + assertThrows(UnsupportedOperationException.class, () -> mMediator + .assignUserToDisplay(PROFILE_USER_ID, PARENT_USER_ID, SECONDARY_DISPLAY_ID)); } @Test 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 22e6e0dcae1c..7b20092b503a 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java @@ -15,23 +15,27 @@ */ package com.android.server.pm; +import static android.content.pm.UserInfo.NO_PROFILE_GROUP_ID; 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.am.UserState.STATE_RUNNING_UNLOCKED; +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; import android.annotation.UserIdInt; -import android.util.SparseIntArray; +import android.util.Log; + +import com.android.server.ExtendedMockitoTestCase; import org.junit.Before; import org.junit.Test; -import java.util.LinkedHashMap; -import java.util.Map; - /** * Base class for {@link UserVisibilityMediator} tests. * @@ -39,7 +43,33 @@ import java.util.Map; * device mode (for example, whether the device supports concurrent multiple users on multiple * displays or not). */ -abstract class UserVisibilityMediatorTestCase extends UserManagerServiceOrInternalTestCase { +abstract class UserVisibilityMediatorTestCase extends ExtendedMockitoTestCase { + + private static final String TAG = UserVisibilityMediatorTestCase.class.getSimpleName(); + + /** + * Id for a simple user (that doesn't have profiles). + */ + protected static final int USER_ID = 600; + + /** + * Id for another simple user. + */ + protected static final int OTHER_USER_ID = 666; + + /** + * Id for a user that has one profile (whose id is {@link #PROFILE_USER_ID}. + * + * <p>You can use {@link #addDefaultProfileAndParent()} to add both of this user to the service. + */ + protected static final int PARENT_USER_ID = 642; + + /** + * Id for a profile whose parent is {@link #PARENTUSER_ID}. + * + * <p>You can use {@link #addDefaultProfileAndParent()} to add both of this user to the service. + */ + protected static final int PROFILE_USER_ID = 643; /** * Id of a secondary display (i.e, not {@link android.view.Display.DEFAULT_DISPLAY}). @@ -51,12 +81,10 @@ abstract class UserVisibilityMediatorTestCase extends UserManagerServiceOrIntern */ protected static final int OTHER_SECONDARY_DISPLAY_ID = 108; - private final boolean mUsersOnSecondaryDisplaysEnabled; + private static final boolean FG = true; + private static final boolean BG = false; - // TODO(b/244644281): manipulating mUsersOnSecondaryDisplays directly leaks implementation - // details into the unit test, but it's fine for now as the tests were copied "as is" - it - // would be better to use a geter() instead - protected final SparseIntArray mUsersOnSecondaryDisplays = new SparseIntArray(); + private final boolean mUsersOnSecondaryDisplaysEnabled; protected UserVisibilityMediator mMediator; @@ -66,13 +94,93 @@ abstract class UserVisibilityMediatorTestCase extends UserManagerServiceOrIntern @Before public final void setMediator() { - mMediator = new UserVisibilityMediator(mUms, mUsersOnSecondaryDisplaysEnabled, - mUsersOnSecondaryDisplays); + mMediator = new UserVisibilityMediator(mUsersOnSecondaryDisplaysEnabled); + mDumpableDumperRule.addDumpable(mMediator); + } + + @Test + public final void testStartUser_currentUser() { + int result = mMediator.startUser(USER_ID, USER_ID, FG, DEFAULT_DISPLAY); + assertStartUserResult(result, START_USER_RESULT_SUCCESS_VISIBLE); + + assertCurrentUser(USER_ID); + assertIsCurrentUserOrRunningProfileOfCurrentUser(USER_ID); + assertStartedProfileGroupIdOf(USER_ID, 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); + + assertCurrentUser(INITIAL_CURRENT_USER_ID); + assertIsNotCurrentUserOrRunningProfileOfCurrentUser(USER_ID); + assertStartedProfileGroupIdOf(USER_ID, NO_PROFILE_GROUP_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); + + assertCurrentUser(PARENT_USER_ID); + assertIsCurrentUserOrRunningProfileOfCurrentUser(PROFILE_USER_ID); + assertStartedProfileGroupIdOf(PROFILE_USER_ID, PARENT_USER_ID); + assertIsStartedProfile(PROFILE_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); + + assertCurrentUser(INITIAL_CURRENT_USER_ID); + assertIsNotCurrentUserOrRunningProfileOfCurrentUser(PROFILE_USER_ID); + assertStartedProfileGroupIdOf(PROFILE_USER_ID, PARENT_USER_ID); + assertIsStartedProfile(PROFILE_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); + + assertCurrentUser(INITIAL_CURRENT_USER_ID); + assertIsNotCurrentUserOrRunningProfileOfCurrentUser(PROFILE_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); + + assertCurrentUser(INITIAL_CURRENT_USER_ID); + assertIsNotCurrentUserOrRunningProfileOfCurrentUser(PROFILE_USER_ID); + assertStartedProfileGroupIdOf(PROFILE_USER_ID, NO_PROFILE_GROUP_ID); + } + + @Test + public final void testStartUser_profileFgSecondaryDisplay() { + int result = mMediator.startUser(PROFILE_USER_ID, PARENT_USER_ID, FG, SECONDARY_DISPLAY_ID); + + assertStartUserResult(result, START_USER_RESULT_FAILURE); + assertCurrentUser(INITIAL_CURRENT_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); + + assertWithMessage("shit").that(mMediator.getStartedProfileGroupId(USER_ID)) + .isEqualTo(USER_ID); } @Test public final void testAssignUserToDisplay_defaultDisplayIgnored() { - mMediator.assignUserToDisplay(USER_ID, DEFAULT_DISPLAY); + mMediator.assignUserToDisplay(USER_ID, USER_ID, DEFAULT_DISPLAY); assertNoUserAssignedToDisplay(); } @@ -103,18 +211,14 @@ abstract class UserVisibilityMediatorTestCase extends UserManagerServiceOrIntern @Test public final void testIsUserVisible_startedProfileOfcurrentUser() { - addDefaultProfileAndParent(); mockCurrentUser(PARENT_USER_ID); startDefaultProfile(); - setUserState(PROFILE_USER_ID, STATE_RUNNING_UNLOCKED); - assertWithMessage("isUserVisible(%s)", PROFILE_USER_ID) .that(mMediator.isUserVisible(PROFILE_USER_ID)).isTrue(); } @Test public final void testIsUserVisible_stoppedProfileOfcurrentUser() { - addDefaultProfileAndParent(); mockCurrentUser(PARENT_USER_ID); stopDefaultProfile(); @@ -164,7 +268,6 @@ abstract class UserVisibilityMediatorTestCase extends UserManagerServiceOrIntern @Test public final void testIsUserVisibleOnDisplay_startedProfileOfcurrentUserInvalidDisplay() { - addDefaultProfileAndParent(); mockCurrentUser(PARENT_USER_ID); startDefaultProfile(); @@ -174,7 +277,6 @@ abstract class UserVisibilityMediatorTestCase extends UserManagerServiceOrIntern @Test public final void testIsUserVisibleOnDisplay_stoppedProfileOfcurrentUserInvalidDisplay() { - addDefaultProfileAndParent(); mockCurrentUser(PARENT_USER_ID); stopDefaultProfile(); @@ -184,18 +286,14 @@ abstract class UserVisibilityMediatorTestCase extends UserManagerServiceOrIntern @Test public final void testIsUserVisibleOnDisplay_startedProfileOfcurrentUserDefaultDisplay() { - addDefaultProfileAndParent(); mockCurrentUser(PARENT_USER_ID); startDefaultProfile(); - setUserState(PROFILE_USER_ID, STATE_RUNNING_UNLOCKED); - assertWithMessage("isUserVisible(%s, %s)", PROFILE_USER_ID, DEFAULT_DISPLAY) .that(mMediator.isUserVisible(PROFILE_USER_ID, DEFAULT_DISPLAY)).isTrue(); } @Test public final void testIsUserVisibleOnDisplay_stoppedProfileOfcurrentUserDefaultDisplay() { - addDefaultProfileAndParent(); mockCurrentUser(PARENT_USER_ID); stopDefaultProfile(); @@ -205,18 +303,14 @@ abstract class UserVisibilityMediatorTestCase extends UserManagerServiceOrIntern @Test public final void testIsUserVisibleOnDisplay_startedProfileOfCurrentUserSecondaryDisplay() { - addDefaultProfileAndParent(); mockCurrentUser(PARENT_USER_ID); startDefaultProfile(); - setUserState(PROFILE_USER_ID, STATE_RUNNING_UNLOCKED); - assertWithMessage("isUserVisible(%s, %s)", PROFILE_USER_ID, SECONDARY_DISPLAY_ID) .that(mMediator.isUserVisible(PROFILE_USER_ID, SECONDARY_DISPLAY_ID)).isTrue(); } @Test public void testIsUserVisibleOnDisplay_stoppedProfileOfcurrentUserSecondaryDisplay() { - addDefaultProfileAndParent(); mockCurrentUser(PARENT_USER_ID); stopDefaultProfile(); @@ -250,11 +344,8 @@ abstract class UserVisibilityMediatorTestCase extends UserManagerServiceOrIntern @Test public final void testGetDisplayAssignedToUser_startedProfileOfcurrentUser() { - addDefaultProfileAndParent(); mockCurrentUser(PARENT_USER_ID); startDefaultProfile(); - setUserState(PROFILE_USER_ID, STATE_RUNNING_UNLOCKED); - assertWithMessage("getDisplayAssignedToUser(%s)", PROFILE_USER_ID) .that(mMediator.getDisplayAssignedToUser(PROFILE_USER_ID)) .isEqualTo(DEFAULT_DISPLAY); @@ -262,7 +353,6 @@ abstract class UserVisibilityMediatorTestCase extends UserManagerServiceOrIntern @Test public final void testGetDisplayAssignedToUser_stoppedProfileOfcurrentUser() { - addDefaultProfileAndParent(); mockCurrentUser(PARENT_USER_ID); stopDefaultProfile(); @@ -296,28 +386,96 @@ abstract class UserVisibilityMediatorTestCase extends UserManagerServiceOrIntern .isEqualTo(USER_ID); } - // NOTE: should only called by tests that indirectly needs to check user assignments (like - // isUserVisible), not by tests for the user assignment methods per se. + // 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) { + throw new IllegalStateException("Failed to mock current user " + userId + + ": mediator returned " + startUserResultToString(result)); + } + } + + // 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 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) { + throw new IllegalStateException("Failed to start profile user " + PROFILE_USER_ID + + ": mediator returned " + startUserResultToString(result)); + } + } + + // 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 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 + // it's not meant to be used to test assignUserToDisplay() itself. protected final void assignUserToDisplay(@UserIdInt int userId, int displayId) { - mUsersOnSecondaryDisplays.put(userId, displayId); + Log.d(TAG, "assignUserToDisplay(" + userId + ", " + displayId + ")"); + int result = mMediator.startUser(userId, userId, BG, displayId); + if (result != START_USER_RESULT_SUCCESS_INVISIBLE) { + throw new IllegalStateException("Failed to startuser " + userId + + " on background: mediator returned " + startUserResultToString(result)); + } + mMediator.assignUserToDisplay(userId, userId, displayId); + } protected final void assertNoUserAssignedToDisplay() { - assertWithMessage("mUsersOnSecondaryDisplays()").that(usersOnSecondaryDisplaysAsMap()) + assertWithMessage("uses on secondary displays") + .that(mMediator.getUsersOnSecondaryDisplays()) .isEmpty(); } protected final void assertUserAssignedToDisplay(@UserIdInt int userId, int displayId) { - assertWithMessage("mUsersOnSecondaryDisplays()").that(usersOnSecondaryDisplaysAsMap()) + assertWithMessage("uses on secondary displays") + .that(mMediator.getUsersOnSecondaryDisplays()) .containsExactly(userId, displayId); } - private Map<Integer, Integer> usersOnSecondaryDisplaysAsMap() { - int size = mUsersOnSecondaryDisplays.size(); - Map<Integer, Integer> map = new LinkedHashMap<>(size); - for (int i = 0; i < size; i++) { - map.put(mUsersOnSecondaryDisplays.keyAt(i), mUsersOnSecondaryDisplays.valueAt(i)); - } - return map; + private void assertCurrentUser(@UserIdInt int userId) { + assertWithMessage("mediator.getCurrentUserId()").that(mMediator.getCurrentUserId()) + .isEqualTo(userId); + } + + private void assertIsStartedProfile(@UserIdInt int userId) { + assertWithMessage("mediator.isStartedProfile(%s)", userId) + .that(mMediator.isStartedProfile(userId)) + .isTrue(); + } + + private void assertStartedProfileGroupIdOf(@UserIdInt int profileId, @UserIdInt int parentId) { + assertWithMessage("mediator.getStartedProfileGroupId(%s)", profileId) + .that(mMediator.getStartedProfileGroupId(profileId)) + .isEqualTo(parentId); + } + + private void assertIsCurrentUserOrRunningProfileOfCurrentUser(int userId) { + assertWithMessage("mediator.isCurrentUserOrRunningProfileOfCurrentUser(%s)", userId) + .that(mMediator.isCurrentUserOrRunningProfileOfCurrentUser(userId)) + .isTrue(); + } + + private void assertIsNotCurrentUserOrRunningProfileOfCurrentUser(int userId) { + assertWithMessage("mediator.isCurrentUserOrRunningProfileOfCurrentUser(%s)", userId) + .that(mMediator.isCurrentUserOrRunningProfileOfCurrentUser(userId)) + .isFalse(); + } + + private void assertStartUserResult(int actualResult, int expectedResult) { + assertWithMessage("startUser() result (where %s=%s and %s=%s)", + actualResult, startUserResultToString(actualResult), + expectedResult, startUserResultToString(expectedResult)) + .that(actualResult).isEqualTo(expectedResult); } } 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 935d1d880a2a..80cee50cef79 100644 --- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java @@ -38,6 +38,7 @@ import static com.android.server.am.UserController.USER_COMPLETED_EVENT_MSG; 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.google.android.collect.Lists.newArrayList; import static com.google.android.collect.Sets.newHashSet; @@ -158,6 +159,7 @@ public class UserControllerTest { REPORT_USER_SWITCH_MSG, USER_SWITCH_TIMEOUT_MSG, USER_START_MSG, + USER_VISIBILITY_CHANGED_MSG, USER_CURRENT_MSG); private static final Set<Integer> START_BACKGROUND_USER_MESSAGE_CODES = newHashSet( @@ -283,7 +285,7 @@ public class UserControllerTest { assertWithMessage("wrong binder message calls").that(mInjector.mHandler.getMessageCodes()) .containsExactly(USER_START_MSG); - verifyUserAssignedToDisplay(TEST_PRE_CREATED_USER_ID, Display.DEFAULT_DISPLAY); + verifyUserNeverAssignedToDisplay(); } private void startUserAssertions( @@ -948,11 +950,13 @@ public class UserControllerTest { } private void verifyUserAssignedToDisplay(@UserIdInt int userId, int displayId) { - verify(mInjector.getUserManagerInternal()).assignUserToDisplay(userId, displayId); + verify(mInjector.getUserManagerInternal()).assignUserToDisplay(eq(userId), anyInt(), + anyBoolean(), eq(displayId)); } private void verifyUserNeverAssignedToDisplay() { - verify(mInjector.getUserManagerInternal(), never()).assignUserToDisplay(anyInt(), anyInt()); + verify(mInjector.getUserManagerInternal(), never()).assignUserToDisplay(anyInt(), anyInt(), + anyBoolean(), anyInt()); } private void verifyUserUnassignedFromDisplay(@UserIdInt int userId) { @@ -964,7 +968,7 @@ public class UserControllerTest { } private void verifySystemUserVisibilityChangedNotified(boolean visible) { - verify(mInjector).notifySystemUserVisibilityChanged(visible); + verify(mInjector).onUserVisibilityChanged(UserHandle.USER_SYSTEM, visible); } // Should be public to allow mocking @@ -1104,13 +1108,13 @@ public class UserControllerTest { } @Override - void onUserStarting(@UserIdInt int userId, boolean visible) { - Log.i(TAG, "onUserStarting(" + userId + ", " + visible + ")"); + void onUserStarting(@UserIdInt int userId) { + Log.i(TAG, "onUserStarting(" + userId + ")"); } @Override - void notifySystemUserVisibilityChanged(boolean visible) { - Log.i(TAG, "notifySystemUserVisibilityChanged(" + visible + ")"); + void onUserVisibilityChanged(@UserIdInt int userId, boolean visible) { + Log.i(TAG, "onUserVisibilityChanged(" + userId + ", " + 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 f1383119605e..164161e34b6f 100644 --- a/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java +++ b/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java @@ -155,7 +155,18 @@ public class JobStoreTest { } @Test - public void testWritingTwoFilesToDisk() throws Exception { + public void testWritingTwoJobsToDisk_singleFile() throws Exception { + mTaskStoreUnderTest.setUseSplitFiles(false); + runWritingTwoJobsToDisk(); + } + + @Test + public void testWritingTwoJobsToDisk_splitFiles() throws Exception { + mTaskStoreUnderTest.setUseSplitFiles(true); + runWritingTwoJobsToDisk(); + } + + private void runWritingTwoJobsToDisk() throws Exception { final JobInfo task1 = new Builder(8, mComponent) .setRequiresDeviceIdle(true) .setPeriodic(10000L) @@ -169,8 +180,10 @@ public class JobStoreTest { .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED) .setPersisted(true) .build(); - final JobStatus taskStatus1 = JobStatus.createFromJobInfo(task1, SOME_UID, null, -1, null); - final JobStatus taskStatus2 = JobStatus.createFromJobInfo(task2, SOME_UID, null, -1, null); + final int uid1 = SOME_UID; + final int uid2 = uid1 + 1; + final JobStatus taskStatus1 = JobStatus.createFromJobInfo(task1, uid1, null, -1, null); + final JobStatus taskStatus2 = JobStatus.createFromJobInfo(task2, uid2, null, -1, null); mTaskStoreUnderTest.add(taskStatus1); mTaskStoreUnderTest.add(taskStatus2); waitForPendingIo(); @@ -414,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) @@ -528,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/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..6fe33eef8713 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); 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/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/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/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/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java index c906abcdc986..e65610f0959b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -25,6 +25,7 @@ import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.provider.DeviceConfig.NAMESPACE_CONSTRAIN_DISPLAY_APIS; +import static android.view.InsetsState.ITYPE_EXTRA_NAVIGATION_BAR; import static android.view.InsetsState.ITYPE_STATUS_BAR; import static android.view.InsetsState.ITYPE_TOP_MANDATORY_GESTURES; import static android.view.InsetsState.ITYPE_TOP_TAPPABLE_ELEMENT; @@ -71,6 +72,7 @@ import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.times; import android.annotation.Nullable; import android.app.ActivityManager; @@ -87,6 +89,7 @@ import android.platform.test.annotations.Presubmit; import android.provider.DeviceConfig; import android.provider.DeviceConfig.Properties; import android.view.InsetsFrameProvider; +import android.view.InsetsSource; import android.view.WindowManager; import androidx.test.filters.MediumTest; @@ -105,6 +108,9 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestRule; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; + +import java.util.List; /** * Tests for Size Compatibility mode. @@ -2368,6 +2374,48 @@ public class SizeCompatTests extends WindowTestsBase { } @Test + public void testLetterboxDetailsForTaskBar_letterboxNotOverlappingTaskBar() { + mAtm.mDevEnableNonResizableMultiWindow = true; + final int screenHeight = 2200; + final int screenWidth = 1400; + final int taskbarHeight = 200; + setUpDisplaySizeWithApp(screenWidth, screenHeight); + + final TestSplitOrganizer organizer = + new TestSplitOrganizer(mAtm, mActivity.getDisplayContent()); + + // Move first activity to split screen which takes half of the screen. + organizer.mPrimary.setBounds(0, screenHeight / 2, screenWidth, screenHeight); + organizer.putTaskToPrimary(mTask, true); + + final InsetsSource navSource = new InsetsSource(ITYPE_EXTRA_NAVIGATION_BAR); + navSource.setFrame(new Rect(0, screenHeight - taskbarHeight, screenWidth, screenHeight)); + + mActivity.mWmService.mLetterboxConfiguration.setLetterboxActivityCornersRadius(15); + + final WindowState w1 = addWindowToActivity(mActivity); + w1.mAboveInsetsState.addSource(navSource); + + // Prepare unresizable activity with max aspect ratio + prepareUnresizable(mActivity, /* maxAspect */ 1.1f, SCREEN_ORIENTATION_UNSPECIFIED); + + // Refresh the letterboxes + mActivity.mRootWindowContainer.performSurfacePlacement(); + + final ArgumentCaptor<Rect> cropCapturer = ArgumentCaptor.forClass(Rect.class); + verify(mTransaction, times(2)).setWindowCrop( + eq(w1.getSurfaceControl()), + cropCapturer.capture() + ); + final List<Rect> capturedCrops = cropCapturer.getAllValues(); + + final int expectedHeight = screenHeight / 2 - taskbarHeight; + assertEquals(2, capturedCrops.size()); + assertEquals(expectedHeight, capturedCrops.get(0).bottom); + assertEquals(expectedHeight, capturedCrops.get(1).bottom); + } + + @Test public void testSplitScreenLetterboxDetailsForStatusBar_twoLetterboxedApps() { mAtm.mDevEnableNonResizableMultiWindow = true; setUpDisplaySizeWithApp(2800, 1000); @@ -3160,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/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java index 7f5beb1f6cf0..4fd2b78b6f66 100644 --- a/services/usage/java/com/android/server/usage/UsageStatsService.java +++ b/services/usage/java/com/android/server/usage/UsageStatsService.java @@ -2392,6 +2392,8 @@ public class UsageStatsService extends SystemService implements @Override public void setAppStandbyBucket(String packageName, int bucket, int userId) { + super.setAppStandbyBucket_enforcePermission(); + final int callingUid = Binder.getCallingUid(); final int callingPid = Binder.getCallingPid(); final long token = Binder.clearCallingIdentity(); @@ -2442,6 +2444,8 @@ public class UsageStatsService extends SystemService implements @Override public void setAppStandbyBuckets(ParceledListSlice appBuckets, int userId) { + super.setAppStandbyBuckets_enforcePermission(); + final int callingUid = Binder.getCallingUid(); final int callingPid = Binder.getCallingPid(); final long token = Binder.clearCallingIdentity(); @@ -2493,6 +2497,8 @@ public class UsageStatsService extends SystemService implements public void setEstimatedLaunchTime(String packageName, long estimatedLaunchTime, int userId) { + super.setEstimatedLaunchTime_enforcePermission(); + final long token = Binder.clearCallingIdentity(); try { UsageStatsService.this @@ -2506,6 +2512,8 @@ public class UsageStatsService extends SystemService implements @Override public void setEstimatedLaunchTimes(ParceledListSlice estimatedLaunchTimes, int userId) { + super.setEstimatedLaunchTimes_enforcePermission(); + final long token = Binder.clearCallingIdentity(); try { UsageStatsService.this 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/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index b22e2b76a477..936fad51f095 100644 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -8785,7 +8785,7 @@ public class CarrierConfigManager { * The default value is 30 minutes. * * @see TelephonyManager#PURCHASE_PREMIUM_CAPABILITY_RESULT_CARRIER_ERROR - * @see TelephonyManager#PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_CONGESTED + * @see TelephonyManager#PURCHASE_PREMIUM_CAPABILITY_RESULT_ENTITLEMENT_CHECK_FAILED */ public static final String KEY_PREMIUM_CAPABILITY_PURCHASE_CONDITION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG = diff --git a/telephony/java/android/telephony/ServiceState.java b/telephony/java/android/telephony/ServiceState.java index bfa60ba960ca..6be2f770efe9 100644 --- a/telephony/java/android/telephony/ServiceState.java +++ b/telephony/java/android/telephony/ServiceState.java @@ -138,13 +138,6 @@ public class ServiceState implements Parcelable { */ public static final int FREQUENCY_RANGE_MMWAVE = 4; - private static final List<Integer> FREQUENCY_RANGE_ORDER = Arrays.asList( - FREQUENCY_RANGE_UNKNOWN, - FREQUENCY_RANGE_LOW, - FREQUENCY_RANGE_MID, - FREQUENCY_RANGE_HIGH, - FREQUENCY_RANGE_MMWAVE); - /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(prefix = "DUPLEX_MODE_", @@ -2108,15 +2101,6 @@ public class ServiceState implements Parcelable { } /** - * @hide - */ - public static final int getBetterNRFrequencyRange(int range1, int range2) { - return FREQUENCY_RANGE_ORDER.indexOf(range1) > FREQUENCY_RANGE_ORDER.indexOf(range2) - ? range1 - : range2; - } - - /** * Returns a copy of self with location-identifying information removed. * Always clears the NetworkRegistrationInfo's CellIdentity fields, but if removeCoarseLocation * is true, clears other info as well. diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index 51a7840a9fbe..d473c6ab794a 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -17324,14 +17324,14 @@ public class TelephonyManager { public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_NOT_AVAILABLE = 12; /** - * Purchase premium capability failed because the network is congested. + * Purchase premium capability failed because the entitlement check failed. * Subsequent attempts will be throttled for the amount of time specified by * {@link CarrierConfigManager * #KEY_PREMIUM_CAPABILITY_PURCHASE_CONDITION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG} * and return {@link #PURCHASE_PREMIUM_CAPABILITY_RESULT_THROTTLED}. * Throttling will be reevaluated when the network is no longer congested. */ - public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_CONGESTED = 13; + public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_ENTITLEMENT_CHECK_FAILED = 13; /** * Purchase premium capability failed because the request was not made on the default data @@ -17339,7 +17339,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 @@ -17368,8 +17368,8 @@ public class TelephonyManager { PURCHASE_PREMIUM_CAPABILITY_RESULT_TIMEOUT, PURCHASE_PREMIUM_CAPABILITY_RESULT_FEATURE_NOT_SUPPORTED, PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_NOT_AVAILABLE, - PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_CONGESTED, - PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA_SUB, + PURCHASE_PREMIUM_CAPABILITY_RESULT_ENTITLEMENT_CHECK_FAILED, + PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA_SUBSCRIPTION, PURCHASE_PREMIUM_CAPABILITY_RESULT_PENDING_NETWORK_SETUP}) public @interface PurchasePremiumCapabilityResult {} @@ -17407,10 +17407,10 @@ public class TelephonyManager { return "REQUEST_FAILED"; case PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_NOT_AVAILABLE: return "NETWORK_NOT_AVAILABLE"; - case PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_CONGESTED: - return "NETWORK_CONGESTED"; - case PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA_SUB: - return "NOT_DEFAULT_DATA_SUB"; + case PURCHASE_PREMIUM_CAPABILITY_RESULT_ENTITLEMENT_CHECK_FAILED: + return "ENTITLEMENT_CHECK_FAILED"; + 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/DataCallResponse.java b/telephony/java/android/telephony/data/DataCallResponse.java index 73aff4351785..a834e2bbd0d1 100644 --- a/telephony/java/android/telephony/data/DataCallResponse.java +++ b/telephony/java/android/telephony/data/DataCallResponse.java @@ -468,14 +468,14 @@ public final class DataCallResponse implements Parcelable { final boolean isQosBearerSessionsSame = (mQosBearerSessions == null || other.mQosBearerSessions == null) ? mQosBearerSessions == other.mQosBearerSessions - : mQosBearerSessions.size() == other.mQosBearerSessions.size() - && mQosBearerSessions.containsAll(other.mQosBearerSessions); + : (mQosBearerSessions.size() == other.mQosBearerSessions.size() + && mQosBearerSessions.containsAll(other.mQosBearerSessions)); final boolean isTrafficDescriptorsSame = (mTrafficDescriptors == null || other.mTrafficDescriptors == null) ? mTrafficDescriptors == other.mTrafficDescriptors - : mTrafficDescriptors.size() == other.mTrafficDescriptors.size() - && mTrafficDescriptors.containsAll(other.mTrafficDescriptors); + : (mTrafficDescriptors.size() == other.mTrafficDescriptors.size() + && mTrafficDescriptors.containsAll(other.mTrafficDescriptors)); return mCause == other.mCause && mSuggestedRetryTime == other.mSuggestedRetryTime @@ -504,10 +504,35 @@ public final class DataCallResponse implements Parcelable { @Override public int hashCode() { + // Generate order-independent hashes for lists + int addressesHash = mAddresses.stream() + .map(LinkAddress::hashCode) + .mapToInt(Integer::intValue) + .sum(); + int dnsAddressesHash = mDnsAddresses.stream() + .map(InetAddress::hashCode) + .mapToInt(Integer::intValue) + .sum(); + int gatewayAddressesHash = mGatewayAddresses.stream() + .map(InetAddress::hashCode) + .mapToInt(Integer::intValue) + .sum(); + int pcscfAddressesHash = mPcscfAddresses.stream() + .map(InetAddress::hashCode) + .mapToInt(Integer::intValue) + .sum(); + int qosBearerSessionsHash = mQosBearerSessions.stream() + .map(QosBearerSession::hashCode) + .mapToInt(Integer::intValue) + .sum(); + int trafficDescriptorsHash = mTrafficDescriptors.stream() + .map(TrafficDescriptor::hashCode) + .mapToInt(Integer::intValue) + .sum(); return Objects.hash(mCause, mSuggestedRetryTime, mId, mLinkStatus, mProtocolType, - mInterfaceName, mAddresses, mDnsAddresses, mGatewayAddresses, mPcscfAddresses, - mMtu, mMtuV4, mMtuV6, mHandoverFailureMode, mPduSessionId, mDefaultQos, - mQosBearerSessions, mSliceInfo, mTrafficDescriptors); + mInterfaceName, addressesHash, dnsAddressesHash, gatewayAddressesHash, + pcscfAddressesHash, mMtu, mMtuV4, mMtuV6, mHandoverFailureMode, mPduSessionId, + mDefaultQos, qosBearerSessionsHash, mSliceInfo, trafficDescriptorsHash); } @Override @@ -816,8 +841,8 @@ public final class DataCallResponse implements Parcelable { /** * Set pdu session id. * <p/> - * The id must be between 1 and 15 when linked to a pdu session. If no pdu session - * exists for the current data call, the id must be set to {@link PDU_SESSION_ID_NOT_SET}. + * The id must be between 1 and 15 when linked to a pdu session. If no pdu session + * exists for the current data call, the id must be set to {@link #PDU_SESSION_ID_NOT_SET}. * * @param pduSessionId Pdu Session Id of the data call. * @return The same instance of the builder. @@ -858,6 +883,7 @@ public final class DataCallResponse implements Parcelable { */ public @NonNull Builder setQosBearerSessions( @NonNull List<QosBearerSession> qosBearerSessions) { + Objects.requireNonNull(qosBearerSessions); mQosBearerSessions = qosBearerSessions; return this; } @@ -891,6 +917,7 @@ public final class DataCallResponse implements Parcelable { */ public @NonNull Builder setTrafficDescriptors( @NonNull List<TrafficDescriptor> trafficDescriptors) { + Objects.requireNonNull(trafficDescriptors); mTrafficDescriptors = trafficDescriptors; return this; } 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/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/tests/JobSchedulerPerfTests/src/com/android/frameworks/perftests/job/JobStorePerfTests.java b/tests/JobSchedulerPerfTests/src/com/android/frameworks/perftests/job/JobStorePerfTests.java index dd9b294a9596..afaeca1f76ae 100644 --- a/tests/JobSchedulerPerfTests/src/com/android/frameworks/perftests/job/JobStorePerfTests.java +++ b/tests/JobSchedulerPerfTests/src/com/android/frameworks/perftests/job/JobStorePerfTests.java @@ -15,7 +15,6 @@ */ package com.android.frameworks.perftests.job; - import android.app.job.JobInfo; import android.content.ComponentName; import android.content.Context; @@ -46,7 +45,8 @@ import java.util.List; public class JobStorePerfTests { private static final String SOURCE_PACKAGE = "com.android.frameworks.perftests.job"; private static final int SOURCE_USER_ID = 0; - private static final int CALLING_UID = 10079; + private static final int BASE_CALLING_UID = 10079; + private static final int MAX_UID_COUNT = 10; private static Context sContext; private static File sTestDir; @@ -65,10 +65,10 @@ public class JobStorePerfTests { sJobStore = JobStore.initAndGetForTesting(sContext, sTestDir); for (int i = 0; i < 50; i++) { - sFewJobs.add(createJobStatus("fewJobs", i)); + sFewJobs.add(createJobStatus("fewJobs", i, BASE_CALLING_UID + (i % MAX_UID_COUNT))); } for (int i = 0; i < 500; i++) { - sManyJobs.add(createJobStatus("manyJobs", i)); + sManyJobs.add(createJobStatus("manyJobs", i, BASE_CALLING_UID + (i % MAX_UID_COUNT))); } } @@ -104,6 +104,64 @@ public class JobStorePerfTests { runPersistedJobWriting(sManyJobs); } + private void runPersistedJobWriting_delta(List<JobStatus> jobList, + List<JobStatus> jobAdditions, List<JobStatus> jobRemovals) { + final ManualBenchmarkState benchmarkState = mPerfManualStatusReporter.getBenchmarkState(); + + long elapsedTimeNs = 0; + while (benchmarkState.keepRunning(elapsedTimeNs)) { + sJobStore.clearForTesting(); + for (JobStatus job : jobList) { + sJobStore.addForTesting(job); + } + sJobStore.writeStatusToDiskForTesting(); + + for (JobStatus job : jobAdditions) { + sJobStore.addForTesting(job); + } + for (JobStatus job : jobRemovals) { + sJobStore.removeForTesting(job); + } + + final long startTime = SystemClock.elapsedRealtimeNanos(); + sJobStore.writeStatusToDiskForTesting(); + final long endTime = SystemClock.elapsedRealtimeNanos(); + elapsedTimeNs = endTime - startTime; + } + } + + @Test + public void testPersistedJobWriting_delta_fewJobs() { + List<JobStatus> additions = new ArrayList<>(); + List<JobStatus> removals = new ArrayList<>(); + final int numModifiedUids = MAX_UID_COUNT / 2; + for (int i = 0; i < sFewJobs.size() / 3; ++i) { + JobStatus job = createJobStatus("fewJobs", i, BASE_CALLING_UID + (i % numModifiedUids)); + if (i % 2 == 0) { + additions.add(job); + } else { + removals.add(job); + } + } + runPersistedJobWriting_delta(sFewJobs, additions, removals); + } + + @Test + public void testPersistedJobWriting_delta_manyJobs() { + List<JobStatus> additions = new ArrayList<>(); + List<JobStatus> removals = new ArrayList<>(); + final int numModifiedUids = MAX_UID_COUNT / 2; + for (int i = 0; i < sManyJobs.size() / 3; ++i) { + JobStatus job = createJobStatus("fewJobs", i, BASE_CALLING_UID + (i % numModifiedUids)); + if (i % 2 == 0) { + additions.add(job); + } else { + removals.add(job); + } + } + runPersistedJobWriting_delta(sManyJobs, additions, removals); + } + private void runPersistedJobReading(List<JobStatus> jobList, boolean rtcIsGood) { final ManualBenchmarkState benchmarkState = mPerfManualStatusReporter.getBenchmarkState(); @@ -144,12 +202,12 @@ public class JobStorePerfTests { runPersistedJobReading(sManyJobs, false); } - private static JobStatus createJobStatus(String testTag, int jobId) { + private static JobStatus createJobStatus(String testTag, int jobId, int callingUid) { JobInfo jobInfo = new JobInfo.Builder(jobId, new ComponentName(sContext, "JobStorePerfTestJobService")) .setPersisted(true) .build(); return JobStatus.createFromJobInfo( - jobInfo, CALLING_UID, SOURCE_PACKAGE, SOURCE_USER_ID, testTag); + jobInfo, callingUid, SOURCE_PACKAGE, SOURCE_USER_ID, testTag); } } 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 |