Merge "Add tests of BrowseActivity"
diff --git a/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl b/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl
index d281da0..a3390b7 100644
--- a/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl
+++ b/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl
@@ -30,6 +30,25 @@
*/
interface IJobCallback {
/**
+ * Immediate callback to the system after sending a data transfer download progress request
+ * signal; used to quickly detect ANR.
+ *
+ * @param jobId Unique integer used to identify this job.
+ * @param workId Unique integer used to identify a specific work item.
+ * @param transferredBytes How much data has been downloaded, in bytes.
+ */
+ void acknowledgeGetTransferredDownloadBytesMessage(int jobId, int workId,
+ long transferredBytes);
+ /**
+ * Immediate callback to the system after sending a data transfer upload progress request
+ * signal; used to quickly detect ANR.
+ *
+ * @param jobId Unique integer used to identify this job.
+ * @param workId Unique integer used to identify a specific work item.
+ * @param transferredBytes How much data has been uploaded, in bytes.
+ */
+ void acknowledgeGetTransferredUploadBytesMessage(int jobId, int workId, long transferredBytes);
+ /**
* Immediate callback to the system after sending a start signal, used to quickly detect ANR.
*
* @param jobId Unique integer used to identify this job.
@@ -65,4 +84,24 @@
*/
@UnsupportedAppUsage
void jobFinished(int jobId, boolean reschedule);
+ /*
+ * Inform JobScheduler of a change in the estimated transfer payload.
+ *
+ * @param jobId Unique integer used to identify this job.
+ * @param item The particular JobWorkItem this progress is associated with, if any.
+ * @param downloadBytes How many bytes the app expects to download.
+ * @param uploadBytes How many bytes the app expects to upload.
+ */
+ void updateEstimatedNetworkBytes(int jobId, in JobWorkItem item,
+ long downloadBytes, long uploadBytes);
+ /*
+ * Update JobScheduler of how much data the job has successfully transferred.
+ *
+ * @param jobId Unique integer used to identify this job.
+ * @param item The particular JobWorkItem this progress is associated with, if any.
+ * @param transferredDownloadBytes The number of bytes that have successfully been downloaded.
+ * @param transferredUploadBytes The number of bytes that have successfully been uploaded.
+ */
+ void updateTransferredNetworkBytes(int jobId, in JobWorkItem item,
+ long transferredDownloadBytes, long transferredUploadBytes);
}
diff --git a/apex/jobscheduler/framework/java/android/app/job/IJobService.aidl b/apex/jobscheduler/framework/java/android/app/job/IJobService.aidl
index 22ad252..2bb82bd 100644
--- a/apex/jobscheduler/framework/java/android/app/job/IJobService.aidl
+++ b/apex/jobscheduler/framework/java/android/app/job/IJobService.aidl
@@ -17,6 +17,7 @@
package android.app.job;
import android.app.job.JobParameters;
+import android.app.job.JobWorkItem;
/**
* Interface that the framework uses to communicate with application code that implements a
@@ -31,4 +32,8 @@
/** Stop execution of application's job. */
@UnsupportedAppUsage
void stopJob(in JobParameters jobParams);
+ /** Update JS of how much data has been downloaded. */
+ void getTransferredDownloadBytes(in JobParameters jobParams, in JobWorkItem jobWorkItem);
+ /** Update JS of how much data has been uploaded. */
+ void getTransferredUploadBytes(in JobParameters jobParams, in JobWorkItem jobWorkItem);
}
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
index e0db3a6..76f71a2 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
@@ -23,8 +23,11 @@
import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.annotation.UserIdInt;
+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;
@@ -94,6 +97,16 @@
*/
@SystemService(Context.JOB_SCHEDULER_SERVICE)
public abstract class JobScheduler {
+ /**
+ * Whether to throw an exception when an app doesn't properly implement all the necessary
+ * data transfer APIs.
+ *
+ * @hide
+ */
+ @ChangeId
+ @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.TIRAMISU)
+ public static final long THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION = 255371817L;
+
/** @hide */
@IntDef(prefix = { "RESULT_" }, value = {
RESULT_FAILURE,
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobService.java b/apex/jobscheduler/framework/java/android/app/job/JobService.java
index d184d44..bad641c 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobService.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobService.java
@@ -16,7 +16,13 @@
package android.app.job;
+import static android.app.job.JobScheduler.THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION;
+
+import android.annotation.BytesLong;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.app.Service;
+import android.compat.Compatibility;
import android.content.Intent;
import android.os.IBinder;
@@ -72,6 +78,28 @@
public boolean onStopJob(JobParameters params) {
return JobService.this.onStopJob(params);
}
+
+ @Override
+ @BytesLong
+ public long getTransferredDownloadBytes(@NonNull JobParameters params,
+ @Nullable JobWorkItem item) {
+ if (item == null) {
+ return JobService.this.getTransferredDownloadBytes();
+ } else {
+ return JobService.this.getTransferredDownloadBytes(item);
+ }
+ }
+
+ @Override
+ @BytesLong
+ public long getTransferredUploadBytes(@NonNull JobParameters params,
+ @Nullable JobWorkItem item) {
+ if (item == null) {
+ return JobService.this.getTransferredUploadBytes();
+ } else {
+ return JobService.this.getTransferredUploadBytes(item);
+ }
+ }
};
}
return mEngine.getBinder();
@@ -171,4 +199,169 @@
* 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.
+ *
+ * @hide
+ * @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.
+ *
+ * @hide
+ * @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.
+ * @hide
+ */
+ 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}.
+ * @hide
+ */
+ 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.
+ *
+ * @hide
+ * @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.
+ *
+ * @hide
+ * @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.
+ *
+ * @hide
+ * @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.
+ *
+ * @hide
+ * @see JobInfo#NETWORK_BYTES_UNKNOWN
+ */
+ // TODO(255371817): specify the actual time JS will wait for progress before requesting
+ @BytesLong
+ public long getTransferredUploadBytes(@NonNull JobWorkItem item) {
+ if (item == null) {
+ return getTransferredUploadBytes();
+ }
+ if (Compatibility.isChangeEnabled(THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION)) {
+ // Regular jobs don't have to implement this and JobScheduler won't call this API for
+ // non-data transfer jobs.
+ throw new RuntimeException("Not implemented. Must override in a subclass.");
+ }
+ return 0;
+ }
}
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java b/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java
index 3d43d20..83296a6 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java
@@ -16,7 +16,13 @@
package android.app.job;
+import static android.app.job.JobScheduler.THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION;
+
+import android.annotation.BytesLong;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.app.Service;
+import android.compat.Compatibility;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
@@ -25,6 +31,8 @@
import android.os.RemoteException;
import android.util.Log;
+import com.android.internal.os.SomeArgs;
+
import java.lang.ref.WeakReference;
/**
@@ -51,6 +59,20 @@
* Message that the client has completed execution of this job.
*/
private static final int MSG_JOB_FINISHED = 2;
+ /**
+ * Message that will result in a call to
+ * {@link #getTransferredDownloadBytes(JobParameters, JobWorkItem)}.
+ */
+ private static final int MSG_GET_TRANSFERRED_DOWNLOAD_BYTES = 3;
+ /**
+ * Message that will result in a call to
+ * {@link #getTransferredUploadBytes(JobParameters, JobWorkItem)}.
+ */
+ private static final int MSG_GET_TRANSFERRED_UPLOAD_BYTES = 4;
+ /** Message that the client wants to update JobScheduler of the data transfer progress. */
+ private static final int MSG_UPDATE_TRANSFERRED_NETWORK_BYTES = 5;
+ /** Message that the client wants to update JobScheduler of the estimated transfer size. */
+ private static final int MSG_UPDATE_ESTIMATED_NETWORK_BYTES = 6;
private final IJobService mBinder;
@@ -68,6 +90,32 @@
}
@Override
+ public void getTransferredDownloadBytes(@NonNull JobParameters jobParams,
+ @Nullable JobWorkItem jobWorkItem) throws RemoteException {
+ JobServiceEngine service = mService.get();
+ if (service != null) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = jobParams;
+ args.arg2 = jobWorkItem;
+ service.mHandler.obtainMessage(MSG_GET_TRANSFERRED_DOWNLOAD_BYTES, args)
+ .sendToTarget();
+ }
+ }
+
+ @Override
+ public void getTransferredUploadBytes(@NonNull JobParameters jobParams,
+ @Nullable JobWorkItem jobWorkItem) throws RemoteException {
+ JobServiceEngine service = mService.get();
+ if (service != null) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = jobParams;
+ args.arg2 = jobWorkItem;
+ service.mHandler.obtainMessage(MSG_GET_TRANSFERRED_UPLOAD_BYTES, args)
+ .sendToTarget();
+ }
+ }
+
+ @Override
public void startJob(JobParameters jobParams) throws RemoteException {
JobServiceEngine service = mService.get();
if (service != null) {
@@ -98,9 +146,9 @@
@Override
public void handleMessage(Message msg) {
- final JobParameters params = (JobParameters) msg.obj;
switch (msg.what) {
- case MSG_EXECUTE_JOB:
+ case MSG_EXECUTE_JOB: {
+ final JobParameters params = (JobParameters) msg.obj;
try {
boolean workOngoing = JobServiceEngine.this.onStartJob(params);
ackStartMessage(params, workOngoing);
@@ -109,7 +157,9 @@
throw new RuntimeException(e);
}
break;
- case MSG_STOP_JOB:
+ }
+ case MSG_STOP_JOB: {
+ final JobParameters params = (JobParameters) msg.obj;
try {
boolean ret = JobServiceEngine.this.onStopJob(params);
ackStopMessage(params, ret);
@@ -118,7 +168,9 @@
throw new RuntimeException(e);
}
break;
- case MSG_JOB_FINISHED:
+ }
+ case MSG_JOB_FINISHED: {
+ final JobParameters params = (JobParameters) msg.obj;
final boolean needsReschedule = (msg.arg2 == 1);
IJobCallback callback = params.getCallback();
if (callback != null) {
@@ -132,19 +184,117 @@
Log.e(TAG, "finishJob() called for a nonexistent job id.");
}
break;
+ }
+ case MSG_GET_TRANSFERRED_DOWNLOAD_BYTES: {
+ final SomeArgs args = (SomeArgs) msg.obj;
+ final JobParameters params = (JobParameters) args.arg1;
+ final JobWorkItem item = (JobWorkItem) args.arg2;
+ try {
+ long ret = JobServiceEngine.this.getTransferredDownloadBytes(params, item);
+ ackGetTransferredDownloadBytesMessage(params, item, ret);
+ } catch (Exception e) {
+ Log.e(TAG, "Application unable to handle getTransferredDownloadBytes.", e);
+ throw new RuntimeException(e);
+ }
+ args.recycle();
+ break;
+ }
+ case MSG_GET_TRANSFERRED_UPLOAD_BYTES: {
+ final SomeArgs args = (SomeArgs) msg.obj;
+ final JobParameters params = (JobParameters) args.arg1;
+ final JobWorkItem item = (JobWorkItem) args.arg2;
+ try {
+ long ret = JobServiceEngine.this.getTransferredUploadBytes(params, item);
+ ackGetTransferredUploadBytesMessage(params, item, ret);
+ } catch (Exception e) {
+ Log.e(TAG, "Application unable to handle getTransferredUploadBytes.", e);
+ throw new RuntimeException(e);
+ }
+ args.recycle();
+ break;
+ }
+ case MSG_UPDATE_TRANSFERRED_NETWORK_BYTES: {
+ final SomeArgs args = (SomeArgs) msg.obj;
+ final JobParameters params = (JobParameters) args.arg1;
+ IJobCallback callback = params.getCallback();
+ if (callback != null) {
+ try {
+ callback.updateTransferredNetworkBytes(params.getJobId(),
+ (JobWorkItem) args.arg2, args.argl1, args.argl2);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error updating data transfer progress to system:"
+ + " binder has gone away.");
+ }
+ } else {
+ Log.e(TAG, "updateDataTransferProgress() called for a nonexistent job id.");
+ }
+ args.recycle();
+ break;
+ }
+ case MSG_UPDATE_ESTIMATED_NETWORK_BYTES: {
+ final SomeArgs args = (SomeArgs) msg.obj;
+ final JobParameters params = (JobParameters) args.arg1;
+ IJobCallback callback = params.getCallback();
+ if (callback != null) {
+ try {
+ callback.updateEstimatedNetworkBytes(params.getJobId(),
+ (JobWorkItem) args.arg2, args.argl1, args.argl2);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error updating estimated transfer size to system:"
+ + " binder has gone away.");
+ }
+ } else {
+ Log.e(TAG,
+ "updateEstimatedNetworkBytes() called for a nonexistent job id.");
+ }
+ args.recycle();
+ break;
+ }
default:
Log.e(TAG, "Unrecognised message received.");
break;
}
}
+ private void ackGetTransferredDownloadBytesMessage(@NonNull JobParameters params,
+ @Nullable JobWorkItem item, long progress) {
+ final IJobCallback callback = params.getCallback();
+ final int jobId = params.getJobId();
+ final int workId = item == null ? -1 : item.getWorkId();
+ if (callback != null) {
+ try {
+ callback.acknowledgeGetTransferredDownloadBytesMessage(jobId, workId, progress);
+ } catch (RemoteException e) {
+ Log.e(TAG, "System unreachable for returning progress.");
+ }
+ } else if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Attempting to ack a job that has already been processed.");
+ }
+ }
+
+ private void ackGetTransferredUploadBytesMessage(@NonNull JobParameters params,
+ @Nullable JobWorkItem item, long progress) {
+ final IJobCallback callback = params.getCallback();
+ final int jobId = params.getJobId();
+ final int workId = item == null ? -1 : item.getWorkId();
+ if (callback != null) {
+ try {
+ callback.acknowledgeGetTransferredUploadBytesMessage(jobId, workId, progress);
+ } catch (RemoteException e) {
+ Log.e(TAG, "System unreachable for returning progress.");
+ }
+ } else if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Attempting to ack a job that has already been processed.");
+ }
+ }
+
private void ackStartMessage(JobParameters params, boolean workOngoing) {
final IJobCallback callback = params.getCallback();
final int jobId = params.getJobId();
if (callback != null) {
try {
callback.acknowledgeStartMessage(jobId, workOngoing);
- } catch(RemoteException e) {
+ } catch (RemoteException e) {
Log.e(TAG, "System unreachable for starting job.");
}
} else {
@@ -213,4 +363,73 @@
m.arg2 = needsReschedule ? 1 : 0;
m.sendToTarget();
}
+
+ /**
+ * Engine's request to get how much data has been downloaded.
+ *
+ * @hide
+ * @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.
+ *
+ * @hide
+ * @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.
+ *
+ * @hide
+ * @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.
+ *
+ * @hide
+ * @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/JobServiceContext.java b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
index 334647e..9aa6b1c 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
@@ -21,6 +21,7 @@
import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_NONE;
import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
+import android.annotation.BytesLong;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.job.IJobCallback;
@@ -187,6 +188,18 @@
public long mStoppedTime;
@Override
+ public void acknowledgeGetTransferredDownloadBytesMessage(int jobId, int workId,
+ @BytesLong long transferredBytes) {
+ doAcknowledgeGetTransferredDownloadBytesMessage(this, jobId, workId, transferredBytes);
+ }
+
+ @Override
+ public void acknowledgeGetTransferredUploadBytesMessage(int jobId, int workId,
+ @BytesLong long transferredBytes) {
+ doAcknowledgeGetTransferredUploadBytesMessage(this, jobId, workId, transferredBytes);
+ }
+
+ @Override
public void acknowledgeStartMessage(int jobId, boolean ongoing) {
doAcknowledgeStartMessage(this, jobId, ongoing);
}
@@ -210,6 +223,18 @@
public void jobFinished(int jobId, boolean reschedule) {
doJobFinished(this, jobId, reschedule);
}
+
+ @Override
+ public void updateEstimatedNetworkBytes(int jobId, JobWorkItem item,
+ long downloadBytes, long uploadBytes) {
+ doUpdateEstimatedNetworkBytes(this, jobId, item, downloadBytes, uploadBytes);
+ }
+
+ @Override
+ public void updateTransferredNetworkBytes(int jobId, JobWorkItem item,
+ long downloadBytes, long uploadBytes) {
+ doUpdateTransferredNetworkBytes(this, jobId, item, downloadBytes, uploadBytes);
+ }
}
JobServiceContext(JobSchedulerService service, JobConcurrencyManager concurrencyManager,
@@ -506,6 +531,16 @@
}
}
+ private void doAcknowledgeGetTransferredDownloadBytesMessage(JobCallback jobCallback, int jobId,
+ int workId, @BytesLong long transferredBytes) {
+ // TODO(255393346): Make sure apps call this appropriately and monitor for abuse
+ }
+
+ private void doAcknowledgeGetTransferredUploadBytesMessage(JobCallback jobCallback, int jobId,
+ int workId, @BytesLong long transferredBytes) {
+ // TODO(255393346): Make sure apps call this appropriately and monitor for abuse
+ }
+
void doAcknowledgeStopMessage(JobCallback cb, int jobId, boolean reschedule) {
doCallback(cb, reschedule, null);
}
@@ -558,6 +593,16 @@
}
}
+ private void doUpdateTransferredNetworkBytes(JobCallback jobCallback, int jobId,
+ @Nullable JobWorkItem item, long downloadBytes, long uploadBytes) {
+ // TODO(255393346): Make sure apps call this appropriately and monitor for abuse
+ }
+
+ private void doUpdateEstimatedNetworkBytes(JobCallback jobCallback, int jobId,
+ @Nullable JobWorkItem item, long downloadBytes, long uploadBytes) {
+ // TODO(255393346): Make sure apps call this appropriately and monitor for abuse
+ }
+
/**
* We acquire/release a wakelock on onServiceConnected/unbindService. This mirrors the work
* we intend to send to the client - we stop sending work when the service is unbound so until
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 c2602f2..145ac52 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
@@ -412,6 +412,14 @@
/** Version of the db schema. */
private static final int JOBS_FILE_VERSION = 1;
+ /**
+ * For legacy reasons, this tag is used to encapsulate the entire job list.
+ */
+ private static final String XML_TAG_JOB_INFO = "job-info";
+ /**
+ * For legacy reasons, this tag represents a single {@link JobStatus} object.
+ */
+ private static final String XML_TAG_JOB = "job";
/** Tag corresponds to constraints this job needs. */
private static final String XML_TAG_PARAMS_CONSTRAINTS = "constraints";
/** Tag corresponds to execution parameters. */
@@ -645,19 +653,19 @@
out.startDocument(null, true);
out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
- out.startTag(null, "job-info");
+ out.startTag(null, XML_TAG_JOB_INFO);
out.attribute(null, "version", Integer.toString(JOBS_FILE_VERSION));
for (int i=0; i<jobList.size(); i++) {
JobStatus jobStatus = jobList.get(i);
if (DEBUG) {
Slog.d(TAG, "Saving job " + jobStatus.getJobId());
}
- out.startTag(null, "job");
+ out.startTag(null, XML_TAG_JOB);
addAttributesToJobTag(out, jobStatus);
writeConstraintsToXml(out, jobStatus);
writeExecutionCriteriaToXml(out, jobStatus);
writeBundleToXml(jobStatus.getJob().getExtras(), out);
- out.endTag(null, "job");
+ out.endTag(null, XML_TAG_JOB);
numJobs++;
if (jobStatus.getUid() == Process.SYSTEM_UID) {
@@ -667,7 +675,7 @@
}
}
}
- out.endTag(null, "job-info");
+ out.endTag(null, XML_TAG_JOB_INFO);
out.endDocument();
file.finishWrite(fos);
@@ -903,17 +911,17 @@
return;
}
boolean needFileMigration = false;
- long now = sElapsedRealtimeClock.millis();
+ long nowElapsed = sElapsedRealtimeClock.millis();
for (File file : files) {
final AtomicFile aFile = createJobFile(file);
try (FileInputStream fis = aFile.openRead()) {
synchronized (mLock) {
- jobs = readJobMapImpl(fis, rtcGood);
+ jobs = readJobMapImpl(fis, rtcGood, nowElapsed);
if (jobs != null) {
for (int i = 0; i < jobs.size(); i++) {
JobStatus js = jobs.get(i);
js.prepareLocked();
- js.enqueueTime = now;
+ js.enqueueTime = nowElapsed;
this.jobSet.add(js);
numJobs++;
@@ -959,7 +967,7 @@
}
}
- private List<JobStatus> readJobMapImpl(InputStream fis, boolean rtcIsGood)
+ private List<JobStatus> readJobMapImpl(InputStream fis, boolean rtcIsGood, long nowElapsed)
throws XmlPullParserException, IOException {
TypedXmlPullParser parser = Xml.resolvePullParser(fis);
@@ -977,28 +985,24 @@
}
String tagName = parser.getName();
- if ("job-info".equals(tagName)) {
+ if (XML_TAG_JOB_INFO.equals(tagName)) {
final List<JobStatus> jobs = new ArrayList<JobStatus>();
- final int version;
+ final int version = parser.getAttributeInt(null, "version");
// Read in version info.
- try {
- version = Integer.parseInt(parser.getAttributeValue(null, "version"));
- if (version > JOBS_FILE_VERSION || version < 0) {
- Slog.d(TAG, "Invalid version number, aborting jobs file read.");
- return null;
- }
- } catch (NumberFormatException e) {
- Slog.e(TAG, "Invalid version number, aborting jobs file read.");
+ if (version > JOBS_FILE_VERSION || version < 0) {
+ Slog.d(TAG, "Invalid version number, aborting jobs file read.");
return null;
}
+
eventType = parser.next();
do {
// Read each <job/>
if (eventType == XmlPullParser.START_TAG) {
tagName = parser.getName();
// Start reading job.
- if ("job".equals(tagName)) {
- JobStatus persistedJob = restoreJobFromXml(rtcIsGood, parser, version);
+ if (XML_TAG_JOB.equals(tagName)) {
+ JobStatus persistedJob =
+ restoreJobFromXml(rtcIsGood, parser, version, nowElapsed);
if (persistedJob != null) {
if (DEBUG) {
Slog.d(TAG, "Read out " + persistedJob);
@@ -1022,7 +1026,7 @@
* @return Newly instantiated job holding all the information we just read out of the xml tag.
*/
private JobStatus restoreJobFromXml(boolean rtcIsGood, TypedXmlPullParser parser,
- int schemaVersion) throws XmlPullParserException, IOException {
+ int schemaVersion, long nowElapsed) throws XmlPullParserException, IOException {
JobInfo.Builder jobBuilder;
int uid, sourceUserId;
long lastSuccessfulRunTime;
@@ -1113,18 +1117,9 @@
}
// Tuple of (earliest runtime, latest runtime) in UTC.
- final Pair<Long, Long> rtcRuntimes;
- try {
- rtcRuntimes = buildRtcExecutionTimesFromXml(parser);
- } catch (NumberFormatException e) {
- if (DEBUG) {
- Slog.d(TAG, "Error parsing execution time parameters, skipping.");
- }
- return null;
- }
+ final Pair<Long, Long> rtcRuntimes = buildRtcExecutionTimesFromXml(parser);
- final long elapsedNow = sElapsedRealtimeClock.millis();
- Pair<Long, Long> elapsedRuntimes = convertRtcBoundsToElapsed(rtcRuntimes, elapsedNow);
+ Pair<Long, Long> elapsedRuntimes = convertRtcBoundsToElapsed(rtcRuntimes, nowElapsed);
if (XML_TAG_PERIODIC.equals(parser.getName())) {
try {
@@ -1137,8 +1132,8 @@
// from now. This is the latest the periodic could be pushed out. This could
// happen if the periodic ran early (at flex time before period), and then the
// device rebooted.
- if (elapsedRuntimes.second > elapsedNow + periodMillis + flexMillis) {
- final long clampedLateRuntimeElapsed = elapsedNow + flexMillis
+ if (elapsedRuntimes.second > nowElapsed + periodMillis + flexMillis) {
+ final long clampedLateRuntimeElapsed = nowElapsed + flexMillis
+ periodMillis;
final long clampedEarlyRuntimeElapsed = clampedLateRuntimeElapsed
- flexMillis;
@@ -1163,11 +1158,11 @@
} else if (XML_TAG_ONEOFF.equals(parser.getName())) {
try {
if (elapsedRuntimes.first != JobStatus.NO_EARLIEST_RUNTIME) {
- jobBuilder.setMinimumLatency(elapsedRuntimes.first - elapsedNow);
+ jobBuilder.setMinimumLatency(elapsedRuntimes.first - nowElapsed);
}
if (elapsedRuntimes.second != JobStatus.NO_LATEST_RUNTIME) {
jobBuilder.setOverrideDeadline(
- elapsedRuntimes.second - elapsedNow);
+ elapsedRuntimes.second - nowElapsed);
}
} catch (NumberFormatException e) {
Slog.d(TAG, "Error reading job execution criteria, skipping.");
@@ -1236,7 +1231,7 @@
// And now we're done
final int appBucket = JobSchedulerService.standbyBucketForPackage(sourcePackageName,
- sourceUserId, elapsedNow);
+ sourceUserId, nowElapsed);
JobStatus js = new JobStatus(
builtJob, uid, sourcePackageName, sourceUserId,
appBucket, sourceTag,
@@ -1246,9 +1241,10 @@
return js;
}
- private JobInfo.Builder buildBuilderFromXml(XmlPullParser parser) throws NumberFormatException {
+ private JobInfo.Builder buildBuilderFromXml(TypedXmlPullParser parser)
+ throws XmlPullParserException {
// Pull out required fields from <job> attributes.
- int jobId = Integer.parseInt(parser.getAttributeValue(null, "jobid"));
+ int jobId = parser.getAttributeInt(null, "jobid");
String packageName = parser.getAttributeValue(null, "package");
String className = parser.getAttributeValue(null, "class");
ComponentName cname = new ComponentName(packageName, className);
@@ -1405,20 +1401,13 @@
* @return A Pair of timestamps in UTC wall-clock time. The first is the earliest
* time at which the job is to become runnable, and the second is the deadline at
* which it becomes overdue to execute.
- * @throws NumberFormatException
*/
- private Pair<Long, Long> buildRtcExecutionTimesFromXml(XmlPullParser parser)
- throws NumberFormatException {
- String val;
+ private Pair<Long, Long> buildRtcExecutionTimesFromXml(TypedXmlPullParser parser) {
// Pull out execution time data.
- val = parser.getAttributeValue(null, "delay");
- final long earliestRunTimeRtc = (val != null)
- ? Long.parseLong(val)
- : JobStatus.NO_EARLIEST_RUNTIME;
- val = parser.getAttributeValue(null, "deadline");
- final long latestRunTimeRtc = (val != null)
- ? Long.parseLong(val)
- : JobStatus.NO_LATEST_RUNTIME;
+ final long earliestRunTimeRtc =
+ parser.getAttributeLong(null, "delay", JobStatus.NO_EARLIEST_RUNTIME);
+ final long latestRunTimeRtc =
+ parser.getAttributeLong(null, "deadline", JobStatus.NO_LATEST_RUNTIME);
return Pair.create(earliestRunTimeRtc, latestRunTimeRtc);
}
}
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index b32b3b6..eac990d 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -4173,6 +4173,7 @@
method @NonNull @RequiresPermission(android.Manifest.permission.HDMI_CEC) public String getPowerStateChangeOnActiveSourceLost();
method @NonNull @RequiresPermission(android.Manifest.permission.HDMI_CEC) public int getRoutingControl();
method @RequiresPermission(android.Manifest.permission.HDMI_CEC) public int getSadPresenceInQuery(@NonNull String);
+ method @RequiresPermission(android.Manifest.permission.HDMI_CEC) public int getSoundbarMode();
method @Nullable public android.hardware.hdmi.HdmiSwitchClient getSwitchClient();
method @NonNull @RequiresPermission(android.Manifest.permission.HDMI_CEC) public int getSystemAudioControl();
method @NonNull @RequiresPermission(android.Manifest.permission.HDMI_CEC) public int getSystemAudioModeMuting();
@@ -4194,6 +4195,7 @@
method @RequiresPermission(android.Manifest.permission.HDMI_CEC) public void setRoutingControl(@NonNull int);
method @RequiresPermission(android.Manifest.permission.HDMI_CEC) public void setSadPresenceInQuery(@NonNull String, int);
method @RequiresPermission(android.Manifest.permission.HDMI_CEC) public void setSadsPresenceInQuery(@NonNull java.util.List<java.lang.String>, int);
+ method @RequiresPermission(android.Manifest.permission.HDMI_CEC) public void setSoundbarMode(int);
method @RequiresPermission(android.Manifest.permission.HDMI_CEC) public void setStandbyMode(boolean);
method @RequiresPermission(android.Manifest.permission.HDMI_CEC) public void setSystemAudioControl(@NonNull int);
method @RequiresPermission(android.Manifest.permission.HDMI_CEC) public void setSystemAudioModeMuting(@NonNull int);
@@ -4221,6 +4223,7 @@
field public static final String CEC_SETTING_NAME_QUERY_SAD_TRUEHD = "query_sad_truehd";
field public static final String CEC_SETTING_NAME_QUERY_SAD_WMAPRO = "query_sad_wmapro";
field public static final String CEC_SETTING_NAME_ROUTING_CONTROL = "routing_control";
+ field public static final String CEC_SETTING_NAME_SOUNDBAR_MODE = "soundbar_mode";
field public static final String CEC_SETTING_NAME_SYSTEM_AUDIO_CONTROL = "system_audio_control";
field public static final String CEC_SETTING_NAME_SYSTEM_AUDIO_MODE_MUTING = "system_audio_mode_muting";
field public static final String CEC_SETTING_NAME_TV_SEND_STANDBY_ON_SLEEP = "tv_send_standby_on_sleep";
@@ -4302,6 +4305,8 @@
field public static final int ROUTING_CONTROL_DISABLED = 0; // 0x0
field public static final int ROUTING_CONTROL_ENABLED = 1; // 0x1
field public static final String SETTING_NAME_EARC_ENABLED = "earc_enabled";
+ field public static final int SOUNDBAR_MODE_DISABLED = 0; // 0x0
+ field public static final int SOUNDBAR_MODE_ENABLED = 1; // 0x1
field public static final int SYSTEM_AUDIO_CONTROL_DISABLED = 0; // 0x0
field public static final int SYSTEM_AUDIO_CONTROL_ENABLED = 1; // 0x1
field public static final int SYSTEM_AUDIO_MODE_MUTING_DISABLED = 0; // 0x0
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index a61ade0..5aa8f1f 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -47,6 +47,8 @@
import android.app.assist.AssistContent;
import android.app.assist.AssistStructure;
import android.app.backup.BackupAgent;
+import android.app.backup.BackupAnnotations.BackupDestination;
+import android.app.backup.BackupAnnotations.OperationType;
import android.app.servertransaction.ActivityLifecycleItem;
import android.app.servertransaction.ActivityLifecycleItem.LifecycleState;
import android.app.servertransaction.ActivityRelaunchItem;
@@ -799,7 +801,7 @@
ApplicationInfo appInfo;
int backupMode;
int userId;
- int operationType;
+ @BackupDestination int backupDestination;
public String toString() {
return "CreateBackupAgentData{appInfo=" + appInfo
+ " backupAgent=" + appInfo.backupAgentName
@@ -1034,12 +1036,12 @@
}
public final void scheduleCreateBackupAgent(ApplicationInfo app,
- int backupMode, int userId, int operationType) {
+ int backupMode, int userId, @BackupDestination int backupDestination) {
CreateBackupAgentData d = new CreateBackupAgentData();
d.appInfo = app;
d.backupMode = backupMode;
d.userId = userId;
- d.operationType = operationType;
+ d.backupDestination = backupDestination;
sendMessage(H.CREATE_BACKUP_AGENT, d);
}
@@ -4402,7 +4404,8 @@
context.setOuterContext(agent);
agent.attach(context);
- agent.onCreate(UserHandle.of(data.userId), data.operationType);
+ agent.onCreate(UserHandle.of(data.userId), data.backupDestination,
+ getOperationTypeFromBackupMode(data.backupMode));
binder = agent.onBind();
backupAgents.put(packageName, agent);
} catch (Exception e) {
@@ -4430,6 +4433,22 @@
}
}
+ @OperationType
+ private static int getOperationTypeFromBackupMode(int backupMode) {
+ switch (backupMode) {
+ case ApplicationThreadConstants.BACKUP_MODE_RESTORE:
+ case ApplicationThreadConstants.BACKUP_MODE_RESTORE_FULL:
+ return OperationType.RESTORE;
+ case ApplicationThreadConstants.BACKUP_MODE_FULL:
+ case ApplicationThreadConstants.BACKUP_MODE_INCREMENTAL:
+ return OperationType.BACKUP;
+ default:
+ Slog.w(TAG, "Invalid backup mode when initialising BackupAgent: "
+ + backupMode);
+ return OperationType.UNKNOWN;
+ }
+ }
+
private String getBackupAgentName(CreateBackupAgentData data) {
String agentName = data.appInfo.backupAgentName;
// full backup operation but no app-supplied agent? use the default implementation
diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl
index 7475ef8..902f172 100644
--- a/core/java/android/app/IActivityManager.aidl
+++ b/core/java/android/app/IActivityManager.aidl
@@ -304,7 +304,7 @@
@UnsupportedAppUsage
void resumeAppSwitches();
boolean bindBackupAgent(in String packageName, int backupRestoreMode, int targetUserId,
- int operationType);
+ int backupDestination);
void backupAgentCreated(in String packageName, in IBinder agent, int userId);
void unbindBackupAgent(in ApplicationInfo appInfo);
int handleIncomingUser(int callingPid, int callingUid, int userId, boolean allowAll,
diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java
index b9a7186..8685259 100644
--- a/core/java/android/app/WallpaperManager.java
+++ b/core/java/android/app/WallpaperManager.java
@@ -1148,12 +1148,26 @@
* @return the dimensions of system wallpaper
* @hide
*/
+ @Nullable
public Rect peekBitmapDimensions() {
return sGlobals.peekWallpaperDimensions(
mContext, true /* returnDefault */, mContext.getUserId());
}
/**
+ * Peek the dimensions of given wallpaper of the user without decoding it.
+ *
+ * @param which Wallpaper type. Must be either {@link #FLAG_SYSTEM} or
+ * {@link #FLAG_LOCK}.
+ * @return the dimensions of system wallpaper
+ * @hide
+ */
+ @Nullable
+ public Rect peekBitmapDimensions(@SetWallpaperFlags int which) {
+ return peekBitmapDimensions();
+ }
+
+ /**
* Get an open, readable file descriptor to the given wallpaper image file.
* The caller is responsible for closing the file descriptor when done ingesting the file.
*
diff --git a/core/java/android/app/backup/BackupAgent.java b/core/java/android/app/backup/BackupAgent.java
index a4f612d..e323e89 100644
--- a/core/java/android/app/backup/BackupAgent.java
+++ b/core/java/android/app/backup/BackupAgent.java
@@ -20,7 +20,8 @@
import android.annotation.Nullable;
import android.app.IBackupAgent;
import android.app.QueuedWork;
-import android.app.backup.BackupManager.OperationType;
+import android.app.backup.BackupAnnotations.BackupDestination;
+import android.app.backup.BackupAnnotations.OperationType;
import android.app.backup.FullBackup.BackupScheme.PathWithRequiredFlags;
import android.content.Context;
import android.content.ContextWrapper;
@@ -137,7 +138,7 @@
public abstract class BackupAgent extends ContextWrapper {
private static final String TAG = "BackupAgent";
private static final boolean DEBUG = false;
- private static final int DEFAULT_OPERATION_TYPE = OperationType.BACKUP;
+ private static final int DEFAULT_BACKUP_DESTINATION = BackupDestination.CLOUD;
/** @hide */
public static final int RESULT_SUCCESS = 0;
@@ -207,7 +208,7 @@
@Nullable private UserHandle mUser;
// This field is written from the main thread (in onCreate), and read in a Binder thread (in
// onFullBackup that is called from system_server via Binder).
- @OperationType private volatile int mOperationType = DEFAULT_OPERATION_TYPE;
+ @BackupDestination private volatile int mBackupDestination = DEFAULT_BACKUP_DESTINATION;
Handler getHandler() {
if (mHandler == null) {
@@ -265,13 +266,6 @@
}
/**
- * @hide
- */
- public void onCreate(UserHandle user) {
- onCreate(user, DEFAULT_OPERATION_TYPE);
- }
-
- /**
* Provided as a convenience for agent implementations that need an opportunity
* to do one-time initialization before the actual backup or restore operation
* is begun with information about the calling user.
@@ -279,14 +273,33 @@
*
* @hide
*/
- public void onCreate(UserHandle user, @OperationType int operationType) {
- // TODO: Instantiate with the correct type using a parameter.
- mLogger = new BackupRestoreEventLogger(BackupRestoreEventLogger.OperationType.BACKUP);
-
+ public void onCreate(UserHandle user) {
onCreate();
+ }
+ /**
+ * @deprecated Use {@link BackupAgent#onCreate(UserHandle, int, int)} instead.
+ *
+ * @hide
+ */
+ @Deprecated
+ public void onCreate(UserHandle user, @BackupDestination int backupDestination) {
mUser = user;
- mOperationType = operationType;
+ mBackupDestination = backupDestination;
+
+ onCreate(user);
+ }
+
+ /**
+ * @hide
+ */
+ public void onCreate(UserHandle user, @BackupDestination int backupDestination,
+ @OperationType int operationType) {
+ mUser = user;
+ mBackupDestination = backupDestination;
+ mLogger = new BackupRestoreEventLogger(operationType);
+
+ onCreate(user, backupDestination);
}
/**
@@ -433,7 +446,7 @@
*/
public void onFullBackup(FullBackupDataOutput data) throws IOException {
FullBackup.BackupScheme backupScheme = FullBackup.getBackupScheme(this,
- mOperationType);
+ mBackupDestination);
if (!backupScheme.isFullBackupEnabled(data.getTransportFlags())) {
return;
}
@@ -643,7 +656,7 @@
if (includeMap == null || includeMap.size() == 0) {
// Do entire sub-tree for the provided token.
fullBackupFileTree(packageName, domainToken,
- FullBackup.getBackupScheme(this, mOperationType)
+ FullBackup.getBackupScheme(this, mBackupDestination)
.tokenToDirectoryPath(domainToken),
filterSet, traversalExcludeSet, data);
} else if (includeMap.get(domainToken) != null) {
@@ -815,7 +828,7 @@
ArraySet<String> systemExcludes,
FullBackupDataOutput output) {
// Pull out the domain and set it aside to use when making the tarball.
- String domainPath = FullBackup.getBackupScheme(this, mOperationType)
+ String domainPath = FullBackup.getBackupScheme(this, mBackupDestination)
.tokenToDirectoryPath(domain);
if (domainPath == null) {
// Should never happen.
@@ -927,7 +940,7 @@
}
private boolean isFileEligibleForRestore(File destination) throws IOException {
- FullBackup.BackupScheme bs = FullBackup.getBackupScheme(this, mOperationType);
+ FullBackup.BackupScheme bs = FullBackup.getBackupScheme(this, mBackupDestination);
if (!bs.isFullRestoreEnabled()) {
if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) {
Log.v(FullBackup.TAG_XML_PARSER,
@@ -1001,7 +1014,7 @@
+ " domain=" + domain + " relpath=" + path + " mode=" + mode
+ " mtime=" + mtime);
- basePath = FullBackup.getBackupScheme(this, mOperationType).tokenToDirectoryPath(
+ basePath = FullBackup.getBackupScheme(this, mBackupDestination).tokenToDirectoryPath(
domain);
if (domain.equals(FullBackup.MANAGED_EXTERNAL_TREE_TOKEN)) {
mode = -1; // < 0 is a token to skip attempting a chmod()
diff --git a/core/java/android/app/backup/BackupAnnotations.java b/core/java/android/app/backup/BackupAnnotations.java
new file mode 100644
index 0000000..d922861
--- /dev/null
+++ b/core/java/android/app/backup/BackupAnnotations.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.backup;
+
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Annotations related to Android Backup&Restore.
+ *
+ * @hide
+ */
+public class BackupAnnotations {
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ OperationType.UNKNOWN,
+ OperationType.BACKUP,
+ OperationType.RESTORE,
+ })
+ public @interface OperationType {
+ int UNKNOWN = -1;
+ int BACKUP = 0;
+ int RESTORE = 1;
+ }
+
+ /**
+ * Denotes where the backup data is going (e.g. to the cloud or directly to the other device)
+ * during backup or where it is coming from during restore.
+ *
+ * @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ BackupDestination.CLOUD,
+ BackupDestination.DEVICE_TRANSFER,
+ BackupDestination.ADB_BACKUP
+ })
+ public @interface BackupDestination {
+ // A cloud backup.
+ int CLOUD = 0;
+ // A device to device migration.
+ int DEVICE_TRANSFER = 1;
+ // An adb backup.
+ int ADB_BACKUP = 2;
+ }
+}
diff --git a/core/java/android/app/backup/BackupManager.java b/core/java/android/app/backup/BackupManager.java
index d2c7972..378020f 100644
--- a/core/java/android/app/backup/BackupManager.java
+++ b/core/java/android/app/backup/BackupManager.java
@@ -200,22 +200,6 @@
@SystemApi
public static final int ERROR_TRANSPORT_INVALID = -2;
- /** @hide */
- @Retention(RetentionPolicy.SOURCE)
- @IntDef({
- OperationType.BACKUP,
- OperationType.MIGRATION,
- OperationType.ADB_BACKUP,
- })
- public @interface OperationType {
- // A backup / restore to / from an off-device location, e.g. cloud.
- int BACKUP = 0;
- // A direct transfer to another device.
- int MIGRATION = 1;
- // Backup via adb, data saved on the host machine.
- int ADB_BACKUP = 3;
- }
-
private Context mContext;
@UnsupportedAppUsage
private static IBackupManager sService;
diff --git a/core/java/android/app/backup/BackupRestoreEventLogger.java b/core/java/android/app/backup/BackupRestoreEventLogger.java
index 68740cb..f892833 100644
--- a/core/java/android/app/backup/BackupRestoreEventLogger.java
+++ b/core/java/android/app/backup/BackupRestoreEventLogger.java
@@ -16,13 +16,13 @@
package android.app.backup;
-import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.ArrayMap;
+import android.app.backup.BackupAnnotations.OperationType;
import android.util.Slog;
import java.lang.annotation.Retention;
@@ -56,21 +56,6 @@
public static final int DATA_TYPES_ALLOWED = 15;
/**
- * Operation types for which this logger can be used.
- *
- * @hide
- */
- @Retention(RetentionPolicy.SOURCE)
- @IntDef({
- OperationType.BACKUP,
- OperationType.RESTORE
- })
- @interface OperationType {
- int BACKUP = 1;
- int RESTORE = 2;
- }
-
- /**
* Denotes that the annotated element identifies a data type as required by the logging methods
* of {@code BackupRestoreEventLogger}
*/
diff --git a/core/java/android/app/backup/FullBackup.java b/core/java/android/app/backup/FullBackup.java
index bf9a9b0..6371871 100644
--- a/core/java/android/app/backup/FullBackup.java
+++ b/core/java/android/app/backup/FullBackup.java
@@ -16,10 +16,9 @@
package android.app.backup;
-import static android.app.backup.BackupManager.OperationType;
-
import android.annotation.Nullable;
import android.annotation.StringDef;
+import android.app.backup.BackupAnnotations.BackupDestination;
import android.app.compat.CompatChanges;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
@@ -123,20 +122,20 @@
/**
* Identify {@link BackupScheme} object by package and operation type
- * (see {@link OperationType}) it corresponds to.
+ * (see {@link BackupDestination}) it corresponds to.
*/
private static class BackupSchemeId {
final String mPackageName;
- @OperationType final int mOperationType;
+ @BackupDestination final int mBackupDestination;
- BackupSchemeId(String packageName, @OperationType int operationType) {
+ BackupSchemeId(String packageName, @BackupDestination int backupDestination) {
mPackageName = packageName;
- mOperationType = operationType;
+ mBackupDestination = backupDestination;
}
@Override
public int hashCode() {
- return Objects.hash(mPackageName, mOperationType);
+ return Objects.hash(mPackageName, mBackupDestination);
}
@Override
@@ -149,7 +148,7 @@
}
BackupSchemeId that = (BackupSchemeId) object;
return Objects.equals(mPackageName, that.mPackageName) &&
- Objects.equals(mOperationType, that.mOperationType);
+ Objects.equals(mBackupDestination, that.mBackupDestination);
}
}
@@ -164,19 +163,20 @@
new ArrayMap<>();
static synchronized BackupScheme getBackupScheme(Context context,
- @OperationType int operationType) {
- BackupSchemeId backupSchemeId = new BackupSchemeId(context.getPackageName(), operationType);
+ @BackupDestination int backupDestination) {
+ BackupSchemeId backupSchemeId = new BackupSchemeId(context.getPackageName(),
+ backupDestination);
BackupScheme backupSchemeForPackage =
kPackageBackupSchemeMap.get(backupSchemeId);
if (backupSchemeForPackage == null) {
- backupSchemeForPackage = new BackupScheme(context, operationType);
+ backupSchemeForPackage = new BackupScheme(context, backupDestination);
kPackageBackupSchemeMap.put(backupSchemeId, backupSchemeForPackage);
}
return backupSchemeForPackage;
}
public static BackupScheme getBackupSchemeForTest(Context context) {
- BackupScheme testing = new BackupScheme(context, OperationType.BACKUP);
+ BackupScheme testing = new BackupScheme(context, BackupDestination.CLOUD);
testing.mExcludes = new ArraySet();
testing.mIncludes = new ArrayMap();
return testing;
@@ -303,7 +303,7 @@
final int mDataExtractionRules;
final int mFullBackupContent;
- @OperationType final int mOperationType;
+ @BackupDestination final int mBackupDestination;
final PackageManager mPackageManager;
final StorageManager mStorageManager;
final String mPackageName;
@@ -426,12 +426,12 @@
*/
ArraySet<PathWithRequiredFlags> mExcludes;
- BackupScheme(Context context, @OperationType int operationType) {
+ BackupScheme(Context context, @BackupDestination int backupDestination) {
ApplicationInfo applicationInfo = context.getApplicationInfo();
mDataExtractionRules = applicationInfo.dataExtractionRulesRes;
mFullBackupContent = applicationInfo.fullBackupContent;
- mOperationType = operationType;
+ mBackupDestination = backupDestination;
mStorageManager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
mPackageManager = context.getPackageManager();
mPackageName = context.getPackageName();
@@ -568,7 +568,7 @@
}
try {
- parseSchemeForOperationType(mOperationType);
+ parseSchemeForBackupDestination(mBackupDestination);
} catch (PackageManager.NameNotFoundException e) {
// Throw it as an IOException
throw new IOException(e);
@@ -576,12 +576,12 @@
}
}
- private void parseSchemeForOperationType(@OperationType int operationType)
+ private void parseSchemeForBackupDestination(@BackupDestination int backupDestination)
throws PackageManager.NameNotFoundException, IOException, XmlPullParserException {
- String configSection = getConfigSectionForOperationType(operationType);
+ String configSection = getConfigSectionForBackupDestination(backupDestination);
if (configSection == null) {
- Slog.w(TAG, "Given operation type isn't supported by backup scheme: "
- + operationType);
+ Slog.w(TAG, "Given backup destination isn't supported by backup scheme: "
+ + backupDestination);
return;
}
@@ -600,7 +600,7 @@
}
}
- if (operationType == OperationType.MIGRATION
+ if (backupDestination == BackupDestination.DEVICE_TRANSFER
&& CompatChanges.isChangeEnabled(IGNORE_FULL_BACKUP_CONTENT_IN_D2D)) {
mIsUsingNewScheme = true;
return;
@@ -615,11 +615,12 @@
}
@Nullable
- private String getConfigSectionForOperationType(@OperationType int operationType) {
- switch (operationType) {
- case OperationType.BACKUP:
+ private String getConfigSectionForBackupDestination(
+ @BackupDestination int backupDestination) {
+ switch (backupDestination) {
+ case BackupDestination.CLOUD:
return ConfigSection.CLOUD_BACKUP;
- case OperationType.MIGRATION:
+ case BackupDestination.DEVICE_TRANSFER:
return ConfigSection.DEVICE_TRANSFER;
default:
return null;
diff --git a/core/java/android/app/time/LocationTimeZoneAlgorithmStatus.java b/core/java/android/app/time/LocationTimeZoneAlgorithmStatus.java
index 710b8c4..ec10d84 100644
--- a/core/java/android/app/time/LocationTimeZoneAlgorithmStatus.java
+++ b/core/java/android/app/time/LocationTimeZoneAlgorithmStatus.java
@@ -16,8 +16,9 @@
package android.app.time;
+import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_NOT_RUNNING;
+import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_NOT_SUPPORTED;
import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_RUNNING;
-import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_UNKNOWN;
import static android.app.time.DetectorStatusTypes.detectionAlgorithmStatusFromString;
import static android.app.time.DetectorStatusTypes.detectionAlgorithmStatusToString;
import static android.app.time.DetectorStatusTypes.requireValidDetectionAlgorithmStatus;
@@ -86,12 +87,24 @@
public static final @ProviderStatus int PROVIDER_STATUS_IS_UNCERTAIN = 4;
/**
- * An instance that provides no information about algorithm status because the algorithm has not
- * yet reported. Effectively a "null" status placeholder.
+ * An instance used when the location algorithm is not supported by the device.
*/
- @NonNull
- public static final LocationTimeZoneAlgorithmStatus UNKNOWN =
- new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_UNKNOWN,
+ public static final LocationTimeZoneAlgorithmStatus NOT_SUPPORTED =
+ new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_NOT_SUPPORTED,
+ PROVIDER_STATUS_NOT_PRESENT, null, PROVIDER_STATUS_NOT_PRESENT, null);
+
+ /**
+ * An instance used when the location algorithm is running, but has not reported an event.
+ */
+ public static final LocationTimeZoneAlgorithmStatus RUNNING_NOT_REPORTED =
+ new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_NOT_RUNNING,
+ PROVIDER_STATUS_NOT_READY, null, PROVIDER_STATUS_NOT_READY, null);
+
+ /**
+ * An instance used when the location algorithm is supported but not running.
+ */
+ public static final LocationTimeZoneAlgorithmStatus NOT_RUNNING =
+ new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_NOT_RUNNING,
PROVIDER_STATUS_NOT_READY, null, PROVIDER_STATUS_NOT_READY, null);
private final @DetectionAlgorithmStatus int mStatus;
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index fcdf440..9f9fd3c 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -6201,7 +6201,7 @@
*/
@CheckResult(suggest="#enforceCallingOrSelfPermission(String,String)")
@PackageManager.PermissionResult
- @PermissionMethod
+ @PermissionMethod(orSelf = true)
public abstract int checkCallingOrSelfPermission(@NonNull @PermissionName String permission);
/**
@@ -6269,7 +6269,7 @@
*
* @see #checkCallingOrSelfPermission(String)
*/
- @PermissionMethod
+ @PermissionMethod(orSelf = true)
public abstract void enforceCallingOrSelfPermission(
@NonNull @PermissionName String permission, @Nullable String message);
diff --git a/core/java/android/content/pm/PermissionMethod.java b/core/java/android/content/pm/PermissionMethod.java
index ba97342..647c696 100644
--- a/core/java/android/content/pm/PermissionMethod.java
+++ b/core/java/android/content/pm/PermissionMethod.java
@@ -33,4 +33,20 @@
*/
@Retention(CLASS)
@Target({METHOD})
-public @interface PermissionMethod {}
+public @interface PermissionMethod {
+ /**
+ * Hard-coded list of permissions checked by this method
+ */
+ @PermissionName String[] value() default {};
+ /**
+ * If true, the check passes if the caller
+ * has any ONE of the supplied permissions
+ */
+ boolean anyOf() default false;
+ /**
+ * Signifies that the permission check passes if
+ * the calling process OR the current process has
+ * the permission
+ */
+ boolean orSelf() default false;
+}
diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java
index 50551fee..f858227 100644
--- a/core/java/android/hardware/camera2/CameraManager.java
+++ b/core/java/android/hardware/camera2/CameraManager.java
@@ -245,11 +245,13 @@
* applications is not guaranteed to be supported, however.</p>
*
* <p>For concurrent operation, in chronological order :
- * - Applications must first close any open cameras that have sessions configured, using
- * {@link CameraDevice#close}.
- * - All camera devices intended to be operated concurrently, must be opened using
- * {@link #openCamera}, before configuring sessions on any of the camera devices.</p>
- *
+ * <ul>
+ * <li> Applications must first close any open cameras that have sessions configured, using
+ * {@link CameraDevice#close}. </li>
+ * <li> All camera devices intended to be operated concurrently, must be opened using
+ * {@link #openCamera}, before configuring sessions on any of the camera devices.</li>
+ *</ul>
+ *</p>
* <p>Each device in a combination, is guaranteed to support stream combinations which may be
* obtained by querying {@link #getCameraCharacteristics} for the key
* {@link android.hardware.camera2.CameraCharacteristics#SCALER_MANDATORY_CONCURRENT_STREAM_COMBINATIONS}.</p>
diff --git a/core/java/android/hardware/hdmi/HdmiControlManager.java b/core/java/android/hardware/hdmi/HdmiControlManager.java
index 96773f8..b0b7a41 100644
--- a/core/java/android/hardware/hdmi/HdmiControlManager.java
+++ b/core/java/android/hardware/hdmi/HdmiControlManager.java
@@ -398,6 +398,30 @@
@Retention(RetentionPolicy.SOURCE)
public @interface RoutingControl {}
+ // -- Whether the Soundbar mode feature is enabled or disabled.
+ /**
+ * Soundbar mode feature enabled.
+ *
+ * @see HdmiControlManager#CEC_SETTING_NAME_SOUNDBAR_MODE
+ */
+ public static final int SOUNDBAR_MODE_ENABLED = 1;
+ /**
+ * Soundbar mode feature disabled.
+ *
+ * @see HdmiControlManager#CEC_SETTING_NAME_SOUNDBAR_MODE
+ */
+ public static final int SOUNDBAR_MODE_DISABLED = 0;
+ /**
+ * @see HdmiControlManager#CEC_SETTING_NAME_SOUNDBAR_MODE
+ * @hide
+ */
+ @IntDef(prefix = { "SOUNDBAR_MODE" }, value = {
+ SOUNDBAR_MODE_ENABLED,
+ SOUNDBAR_MODE_DISABLED
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface SoundbarMode {}
+
// -- Scope of CEC power control messages sent by a playback device.
/**
* Send CEC power control messages to TV only:
@@ -820,6 +844,14 @@
*/
public static final String CEC_SETTING_NAME_ROUTING_CONTROL = "routing_control";
/**
+ * Name of a setting deciding whether the Soundbar mode feature is enabled.
+ * Before exposing this setting make sure the hardware supports it, otherwise, you may
+ * experience multiple issues.
+ *
+ * @see HdmiControlManager#setSoundbarMode(int)
+ */
+ public static final String CEC_SETTING_NAME_SOUNDBAR_MODE = "soundbar_mode";
+ /**
* Name of a setting deciding on the power control mode.
*
* @see HdmiControlManager#setPowerControlMode(String)
@@ -1070,6 +1102,8 @@
@StringDef(value = {
CEC_SETTING_NAME_HDMI_CEC_ENABLED,
CEC_SETTING_NAME_HDMI_CEC_VERSION,
+ CEC_SETTING_NAME_ROUTING_CONTROL,
+ CEC_SETTING_NAME_SOUNDBAR_MODE,
CEC_SETTING_NAME_POWER_CONTROL_MODE,
CEC_SETTING_NAME_POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST,
CEC_SETTING_NAME_SYSTEM_AUDIO_CONTROL,
@@ -1222,7 +1256,16 @@
case HdmiDeviceInfo.DEVICE_PLAYBACK:
return mHasPlaybackDevice ? new HdmiPlaybackClient(mService) : null;
case HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM:
- return mHasAudioSystemDevice ? new HdmiAudioSystemClient(mService) : null;
+ try {
+ if ((mService.getCecSettingIntValue(CEC_SETTING_NAME_SOUNDBAR_MODE)
+ == SOUNDBAR_MODE_ENABLED && mHasPlaybackDevice)
+ || mHasAudioSystemDevice) {
+ return new HdmiAudioSystemClient(mService);
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ return null;
case HdmiDeviceInfo.DEVICE_PURE_CEC_SWITCH:
return (mHasSwitchDevice || mIsSwitchDevice)
? new HdmiSwitchClient(mService) : null;
@@ -2290,6 +2333,54 @@
}
/**
+ * Set the status of Soundbar mode feature.
+ *
+ * <p>This allows to enable/disable Soundbar mode on the playback device.
+ * The setting's effect will be available on devices where the hardware supports this feature.
+ * If enabled, an audio system local device will be allocated and try to establish an ARC
+ * connection with the TV. If disabled, the ARC connection will be terminated and the audio
+ * system local device will be removed from the network.
+ *
+ * @see HdmiControlManager#CEC_SETTING_NAME_SOUNDBAR_MODE
+ */
+ @RequiresPermission(android.Manifest.permission.HDMI_CEC)
+ public void setSoundbarMode(@SoundbarMode int value) {
+ if (mService == null) {
+ Log.e(TAG, "setSoundbarMode: HdmiControlService is not available");
+ throw new RuntimeException("HdmiControlService is not available");
+ }
+ try {
+ mService.setCecSettingIntValue(CEC_SETTING_NAME_SOUNDBAR_MODE, value);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Get the current status of Soundbar mode feature.
+ *
+ * <p>Reflects whether Soundbar mode is currently enabled on the playback device.
+ * If enabled, an audio system local device will be allocated and try to establish an ARC
+ * connection with the TV. If disabled, the ARC connection will be terminated and the audio
+ * system local device will be removed from the network.
+ *
+ * @see HdmiControlManager#CEC_SETTING_NAME_SOUNDBAR_MODE
+ */
+ @SoundbarMode
+ @RequiresPermission(android.Manifest.permission.HDMI_CEC)
+ public int getSoundbarMode() {
+ if (mService == null) {
+ Log.e(TAG, "getSoundbarMode: HdmiControlService is not available");
+ throw new RuntimeException("HdmiControlService is not available");
+ }
+ try {
+ return mService.getCecSettingIntValue(CEC_SETTING_NAME_SOUNDBAR_MODE);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Set the status of Power Control.
*
* <p>Specifies to which devices Power Control messages should be sent:
diff --git a/core/java/android/window/WindowProviderService.java b/core/java/android/window/WindowProviderService.java
index fdc3e5a..f2ae973 100644
--- a/core/java/android/window/WindowProviderService.java
+++ b/core/java/android/window/WindowProviderService.java
@@ -146,7 +146,7 @@
@SuppressLint("OnNameExpected")
@Override
- public void onConfigurationChanged(@Nullable Configuration configuration) {
+ public void onConfigurationChanged(@NonNull Configuration configuration) {
// This is only called from WindowTokenClient.
mCallbacksController.dispatchConfigurationChanged(configuration);
}
diff --git a/core/java/com/android/internal/os/TimeoutRecord.java b/core/java/com/android/internal/os/TimeoutRecord.java
index 680f8fe..a587834 100644
--- a/core/java/com/android/internal/os/TimeoutRecord.java
+++ b/core/java/com/android/internal/os/TimeoutRecord.java
@@ -40,7 +40,8 @@
TimeoutKind.SERVICE_START,
TimeoutKind.SERVICE_EXEC,
TimeoutKind.CONTENT_PROVIDER,
- TimeoutKind.APP_REGISTERED})
+ TimeoutKind.APP_REGISTERED,
+ TimeoutKind.SHORT_FGS_TIMEOUT})
@Retention(RetentionPolicy.SOURCE)
public @interface TimeoutKind {
@@ -51,6 +52,7 @@
int SERVICE_EXEC = 5;
int CONTENT_PROVIDER = 6;
int APP_REGISTERED = 7;
+ int SHORT_FGS_TIMEOUT = 8;
}
/** Kind of timeout, e.g. BROADCAST_RECEIVER, etc. */
@@ -144,4 +146,10 @@
public static TimeoutRecord forApp(@NonNull String reason) {
return TimeoutRecord.endingApproximatelyNow(TimeoutKind.APP_REGISTERED, reason);
}
+
+ /** Record for a "short foreground service" timeout. */
+ @NonNull
+ public static TimeoutRecord forShortFgsTimeout(String reason) {
+ return TimeoutRecord.endingNow(TimeoutKind.SHORT_FGS_TIMEOUT, reason);
+ }
}
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 86b0715..ccce9ba 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -5354,6 +5354,12 @@
<bool name="config_cecRoutingControlDisabled_allowed">true</bool>
<bool name="config_cecRoutingControlDisabled_default">true</bool>
+ <bool name="config_cecSoundbarMode_userConfigurable">true</bool>
+ <bool name="config_cecSoundbarModeEnabled_allowed">true</bool>
+ <bool name="config_cecSoundbarModeEnabled_default">false</bool>
+ <bool name="config_cecSoundbarModeDisabled_allowed">true</bool>
+ <bool name="config_cecSoundbarModeDisabled_default">true</bool>
+
<bool name="config_cecPowerControlMode_userConfigurable">true</bool>
<bool name="config_cecPowerControlModeTv_allowed">true</bool>
<bool name="config_cecPowerControlModeTv_default">false</bool>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 1a86af0..ae033ca 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -4556,6 +4556,12 @@
<java-symbol type="bool" name="config_cecRoutingControlDisabled_allowed" />
<java-symbol type="bool" name="config_cecRoutingControlDisabled_default" />
+ <java-symbol type="bool" name="config_cecSoundbarMode_userConfigurable" />
+ <java-symbol type="bool" name="config_cecSoundbarModeEnabled_allowed" />
+ <java-symbol type="bool" name="config_cecSoundbarModeEnabled_default" />
+ <java-symbol type="bool" name="config_cecSoundbarModeDisabled_allowed" />
+ <java-symbol type="bool" name="config_cecSoundbarModeDisabled_default" />
+
<java-symbol type="bool" name="config_cecPowerControlMode_userConfigurable" />
<java-symbol type="bool" name="config_cecPowerControlModeTv_allowed" />
<java-symbol type="bool" name="config_cecPowerControlModeTv_default" />
diff --git a/core/tests/coretests/src/android/app/backup/BackupAgentTest.java b/core/tests/coretests/src/android/app/backup/BackupAgentTest.java
index 4d5b0d2..561c10ba 100644
--- a/core/tests/coretests/src/android/app/backup/BackupAgentTest.java
+++ b/core/tests/coretests/src/android/app/backup/BackupAgentTest.java
@@ -21,7 +21,8 @@
import static org.mockito.Mockito.when;
import android.app.backup.BackupAgent.IncludeExcludeRules;
-import android.app.backup.BackupManager.OperationType;
+import android.app.backup.BackupAnnotations.BackupDestination;
+import android.app.backup.BackupAnnotations.OperationType;
import android.app.backup.FullBackup.BackupScheme.PathWithRequiredFlags;
import android.os.ParcelFileDescriptor;
import android.os.UserHandle;
@@ -66,7 +67,7 @@
excludePaths.add(path);
IncludeExcludeRules expectedRules = new IncludeExcludeRules(includePaths, excludePaths);
- mBackupAgent = getAgentForOperationType(OperationType.BACKUP);
+ mBackupAgent = getAgentForBackupDestination(BackupDestination.CLOUD);
when(mBackupScheme.maybeParseAndGetCanonicalExcludePaths()).thenReturn(excludePaths);
when(mBackupScheme.maybeParseAndGetCanonicalIncludePaths()).thenReturn(includePaths);
@@ -84,24 +85,26 @@
@Test
public void getBackupRestoreEventLogger_afterOnCreateForBackup_initializedForBackup() {
BackupAgent agent = new TestFullBackupAgent();
- agent.onCreate(USER_HANDLE, OperationType.BACKUP); // TODO: pass in new operation type
+ agent.onCreate(USER_HANDLE, BackupDestination.CLOUD, OperationType.BACKUP);
- assertThat(agent.getBackupRestoreEventLogger().getOperationType()).isEqualTo(1);
+ assertThat(agent.getBackupRestoreEventLogger().getOperationType()).isEqualTo(
+ OperationType.BACKUP);
}
@Test
public void getBackupRestoreEventLogger_afterOnCreateForRestore_initializedForRestore() {
BackupAgent agent = new TestFullBackupAgent();
- agent.onCreate(USER_HANDLE, OperationType.BACKUP); // TODO: pass in new operation type
+ agent.onCreate(USER_HANDLE, BackupDestination.CLOUD, OperationType.RESTORE);
- assertThat(agent.getBackupRestoreEventLogger().getOperationType()).isEqualTo(1);
+ assertThat(agent.getBackupRestoreEventLogger().getOperationType()).isEqualTo(
+ OperationType.RESTORE);
}
@Test
public void getBackupRestoreEventLogger_afterBackup_containsLogsLoggedByAgent()
throws Exception {
BackupAgent agent = new TestFullBackupAgent();
- agent.onCreate(USER_HANDLE, OperationType.BACKUP); // TODO: pass in new operation type
+ agent.onCreate(USER_HANDLE, BackupDestination.CLOUD, OperationType.BACKUP);
// TestFullBackupAgent logs DATA_TYPE_BACKED_UP when onFullBackup is called.
agent.onFullBackup(new FullBackupDataOutput(/* quota = */ 0));
@@ -110,9 +113,9 @@
.isEqualTo(DATA_TYPE_BACKED_UP);
}
- private BackupAgent getAgentForOperationType(@OperationType int operationType) {
+ private BackupAgent getAgentForBackupDestination(@BackupDestination int backupDestination) {
BackupAgent agent = new TestFullBackupAgent();
- agent.onCreate(USER_HANDLE, operationType);
+ agent.onCreate(USER_HANDLE, backupDestination);
return agent;
}
diff --git a/core/tests/coretests/src/android/app/backup/BackupRestoreEventLoggerTest.java b/core/tests/coretests/src/android/app/backup/BackupRestoreEventLoggerTest.java
index b9fdc6d..112d394 100644
--- a/core/tests/coretests/src/android/app/backup/BackupRestoreEventLoggerTest.java
+++ b/core/tests/coretests/src/android/app/backup/BackupRestoreEventLoggerTest.java
@@ -16,8 +16,8 @@
package android.app.backup;
-import static android.app.backup.BackupRestoreEventLogger.OperationType.BACKUP;
-import static android.app.backup.BackupRestoreEventLogger.OperationType.RESTORE;
+import static android.app.backup.BackupAnnotations.OperationType.BACKUP;
+import static android.app.backup.BackupAnnotations.OperationType.RESTORE;
import static com.google.common.truth.Truth.assertThat;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
index 4edc642..d9b4f47 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
@@ -295,8 +295,8 @@
mImeSourceControl.release(SurfaceControl::release);
}
}
+ mImeSourceControl = imeSourceControl;
}
- mImeSourceControl = imeSourceControl;
}
private void applyVisibilityToLeash(InsetsSourceControl imeSourceControl) {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
index 8641541..40f2e88 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
@@ -21,9 +21,7 @@
import static android.view.Surface.ROTATION_0;
import static android.view.WindowInsets.Type.ime;
-import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
@@ -92,22 +90,6 @@
}
@Test
- public void insetsControlChanged_updateExpectedImeSourceControl() {
- final InsetsSourceControl[] insetsSourceControls = new InsetsSourceControl[]{
- new InsetsSourceControl(ITYPE_IME, mock(SurfaceControl.class), false,
- new Point(0, 0), Insets.NONE)};
- final InsetsSourceControl imeSourceControl = insetsSourceControls[0];
-
- mPerDisplay.insetsControlChanged(insetsStateWithIme(false), insetsSourceControls);
-
- assertEquals(imeSourceControl, mPerDisplay.mImeSourceControl);
-
- mPerDisplay.insetsControlChanged(insetsStateWithIme(false), null);
-
- assertNull(mPerDisplay.mImeSourceControl);
- }
-
- @Test
public void insetsChanged_schedulesNoWorkOnExecutor() {
mPerDisplay.insetsChanged(insetsStateWithIme(false));
verifyZeroInteractions(mExecutor);
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 9daa4f7..db49909 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
@@ -28,6 +28,7 @@
import com.android.settingslib.spa.gallery.page.ChartPageProvider
import com.android.settingslib.spa.gallery.page.FooterPageProvider
import com.android.settingslib.spa.gallery.page.IllustrationPageProvider
+import com.android.settingslib.spa.gallery.page.LoadingBarPageProvider
import com.android.settingslib.spa.gallery.page.ProgressBarPageProvider
import com.android.settingslib.spa.gallery.page.SettingsPagerPageProvider
import com.android.settingslib.spa.gallery.page.SliderPageProvider
@@ -72,6 +73,7 @@
CategoryPageProvider,
ActionButtonPageProvider,
ProgressBarPageProvider,
+ LoadingBarPageProvider,
ChartPageProvider,
AlterDialogPageProvider,
),
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt
index 6d53dae..5d26b34 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt
@@ -33,6 +33,7 @@
import com.android.settingslib.spa.gallery.page.ChartPageProvider
import com.android.settingslib.spa.gallery.page.FooterPageProvider
import com.android.settingslib.spa.gallery.page.IllustrationPageProvider
+import com.android.settingslib.spa.gallery.page.LoadingBarPageProvider
import com.android.settingslib.spa.gallery.page.ProgressBarPageProvider
import com.android.settingslib.spa.gallery.page.SettingsPagerPageProvider
import com.android.settingslib.spa.gallery.page.SliderPageProvider
@@ -58,6 +59,7 @@
CategoryPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
ActionButtonPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
ProgressBarPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
+ LoadingBarPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
ChartPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
AlterDialogPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
)
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/LoadingBarPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/LoadingBarPage.kt
new file mode 100644
index 0000000..c354930
--- /dev/null
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/LoadingBarPage.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.gallery.page
+
+import android.os.Bundle
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Button
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
+import com.android.settingslib.spa.framework.common.SettingsPage
+import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.compose.navigator
+import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.widget.preference.Preference
+import com.android.settingslib.spa.widget.preference.PreferenceModel
+import com.android.settingslib.spa.widget.scaffold.RegularScaffold
+import com.android.settingslib.spa.widget.ui.CircularLoadingBar
+import com.android.settingslib.spa.widget.ui.LinearLoadingBar
+
+private const val TITLE = "Sample LoadingBar"
+
+object LoadingBarPageProvider : SettingsPageProvider {
+ override val name = "LoadingBar"
+
+ fun buildInjectEntry(): SettingsEntryBuilder {
+ return SettingsEntryBuilder.createInject(owner = SettingsPage.create(name))
+ .setIsAllowSearch(true)
+ .setUiLayoutFn {
+ Preference(object : PreferenceModel {
+ override val title = TITLE
+ override val onClick = navigator(name)
+ })
+ }
+ }
+
+ override fun getTitle(arguments: Bundle?): String {
+ return TITLE
+ }
+
+ @Composable
+ override fun Page(arguments: Bundle?) {
+ var loading by remember { mutableStateOf(true) }
+ RegularScaffold(title = getTitle(arguments)) {
+ Button(
+ onClick = { loading = !loading },
+ modifier = Modifier.padding(start = 20.dp)
+ ) {
+ if (loading) {
+ Text(text = "Stop")
+ } else {
+ Text(text = "Resume")
+ }
+ }
+ }
+
+ LinearLoadingBar(isLoading = loading, yOffset = 104.dp)
+ CircularLoadingBar(isLoading = loading)
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun LoadingBarPagePreview() {
+ SettingsTheme {
+ LoadingBarPageProvider.Page(null)
+ }
+}
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPage.kt
index 9136b04..1f76557 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPage.kt
@@ -27,7 +27,6 @@
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
import com.android.settingslib.spa.framework.common.SettingsPage
import com.android.settingslib.spa.framework.common.SettingsPageProvider
@@ -39,9 +38,7 @@
import com.android.settingslib.spa.widget.preference.ProgressBarPreferenceModel
import com.android.settingslib.spa.widget.preference.ProgressBarWithDataPreference
import com.android.settingslib.spa.widget.scaffold.RegularScaffold
-import com.android.settingslib.spa.widget.ui.CircularLoadingBar
import com.android.settingslib.spa.widget.ui.CircularProgressBar
-import com.android.settingslib.spa.widget.ui.LinearLoadingBar
import kotlinx.coroutines.delay
private const val TITLE = "Sample ProgressBar"
@@ -66,18 +63,10 @@
@Composable
override fun Page(arguments: Bundle?) {
- // Mocks a loading time of 2 seconds.
- var loading by remember { mutableStateOf(true) }
- LaunchedEffect(Unit) {
- delay(2000)
- loading = false
- }
-
RegularScaffold(title = getTitle(arguments)) {
// Auto update the progress and finally jump tp 0.4f.
var progress by remember { mutableStateOf(0f) }
LaunchedEffect(Unit) {
- delay(2000)
while (progress < 1f) {
delay(100)
progress += 0.01f
@@ -86,19 +75,11 @@
progress = 0.4f
}
- // Show as a placeholder for progress bar
LargeProgressBar(progress)
- // The remaining information only shows after loading complete.
- if (!loading) {
- SimpleProgressBar()
- ProgressBarWithData()
- CircularProgressBar(progress = progress, radius = 160f)
- }
+ SimpleProgressBar()
+ ProgressBarWithData()
+ CircularProgressBar(progress = progress, radius = 160f)
}
-
- // Add loading bar examples, running for 2 seconds.
- LinearLoadingBar(isLoading = loading, yOffset = 64.dp)
- CircularLoadingBar(isLoading = loading)
}
}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/scaffold/RestrictedMenuItem.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/scaffold/RestrictedMenuItem.kt
new file mode 100644
index 0000000..8c1421a
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/scaffold/RestrictedMenuItem.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.template.scaffold
+
+import android.content.Context
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalContext
+import com.android.settingslib.spa.widget.scaffold.MoreOptionsScope
+import com.android.settingslib.spaprivileged.model.enterprise.BaseUserRestricted
+import com.android.settingslib.spaprivileged.model.enterprise.BlockedByAdmin
+import com.android.settingslib.spaprivileged.model.enterprise.Restrictions
+import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProvider
+import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderImpl
+
+@Composable
+fun MoreOptionsScope.RestrictedMenuItem(
+ text: String,
+ restrictions: Restrictions,
+ onClick: () -> Unit,
+) {
+ RestrictedMenuItemImpl(text, restrictions, onClick, ::RestrictionsProviderImpl)
+}
+
+@Composable
+internal fun MoreOptionsScope.RestrictedMenuItemImpl(
+ text: String,
+ restrictions: Restrictions,
+ onClick: () -> Unit,
+ restrictionsProviderFactory: (Context, Restrictions) -> RestrictionsProvider,
+) {
+ val context = LocalContext.current
+ val restrictionsProvider = remember(restrictions) {
+ restrictionsProviderFactory(context, restrictions)
+ }
+ val restrictedMode = restrictionsProvider.restrictedModeState().value
+ MenuItem(text = text, enabled = restrictedMode !is BaseUserRestricted) {
+ when (restrictedMode) {
+ is BlockedByAdmin -> restrictedMode.sendShowAdminSupportDetailsIntent()
+ else -> onClick()
+ }
+ }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreferenceTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreferenceTest.kt
index a5352b2..7f57025 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreferenceTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreferenceTest.kt
@@ -16,7 +16,6 @@
package com.android.settingslib.spaprivileged.template.preference
-import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsEnabled
@@ -28,14 +27,12 @@
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.settingslib.spa.framework.compose.stateOf
import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
import com.android.settingslib.spaprivileged.model.enterprise.BaseUserRestricted
-import com.android.settingslib.spaprivileged.model.enterprise.BlockedByAdmin
import com.android.settingslib.spaprivileged.model.enterprise.NoRestricted
-import com.android.settingslib.spaprivileged.model.enterprise.RestrictedMode
import com.android.settingslib.spaprivileged.model.enterprise.Restrictions
-import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProvider
+import com.android.settingslib.spaprivileged.tests.testutils.FakeBlockedByAdmin
+import com.android.settingslib.spaprivileged.tests.testutils.FakeRestrictionsProvider
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
@@ -46,15 +43,7 @@
@get:Rule
val composeTestRule = createComposeRule()
- private val fakeBlockedByAdmin = object : BlockedByAdmin {
- var sendShowAdminSupportDetailsIntentIsCalled = false
-
- override fun getSummary(checked: Boolean?) = BLOCKED_BY_ADMIN_SUMMARY
-
- override fun sendShowAdminSupportDetailsIntent() {
- sendShowAdminSupportDetailsIntentIsCalled = true
- }
- }
+ private val fakeBlockedByAdmin = FakeBlockedByAdmin()
private val fakeRestrictionsProvider = FakeRestrictionsProvider()
@@ -136,7 +125,7 @@
setContent(restrictions)
composeTestRule.onNodeWithText(TITLE).assertIsDisplayed().assertIsEnabled()
- composeTestRule.onNodeWithText(BLOCKED_BY_ADMIN_SUMMARY).assertIsDisplayed()
+ composeTestRule.onNodeWithText(FakeBlockedByAdmin.SUMMARY).assertIsDisplayed()
composeTestRule.onNode(isOn()).assertIsDisplayed()
}
@@ -163,13 +152,5 @@
const val TITLE = "Title"
const val USER_ID = 0
const val RESTRICTION_KEY = "restriction_key"
- const val BLOCKED_BY_ADMIN_SUMMARY = "Blocked by admin"
}
}
-
-private class FakeRestrictionsProvider : RestrictionsProvider {
- var restrictedMode: RestrictedMode? = null
-
- @Composable
- override fun restrictedModeState() = stateOf(restrictedMode)
-}
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/scaffold/RestrictedMenuItemTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/scaffold/RestrictedMenuItemTest.kt
new file mode 100644
index 0000000..983284c
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/scaffold/RestrictedMenuItemTest.kt
@@ -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.settingslib.spaprivileged.template.scaffold
+
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsEnabled
+import androidx.compose.ui.test.assertIsNotEnabled
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performClick
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.widget.scaffold.MoreOptionsScope
+import com.android.settingslib.spaprivileged.model.enterprise.BaseUserRestricted
+import com.android.settingslib.spaprivileged.model.enterprise.NoRestricted
+import com.android.settingslib.spaprivileged.model.enterprise.Restrictions
+import com.android.settingslib.spaprivileged.tests.testutils.FakeBlockedByAdmin
+import com.android.settingslib.spaprivileged.tests.testutils.FakeRestrictionsProvider
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class RestrictedMenuItemTest {
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ private val fakeBlockedByAdmin = FakeBlockedByAdmin()
+
+ private val fakeRestrictionsProvider = FakeRestrictionsProvider()
+
+ private var menuItemOnClickIsCalled = false
+
+ @Test
+ fun whenRestrictionsKeysIsEmpty_enabled() {
+ val restrictions = Restrictions(userId = USER_ID, keys = emptyList())
+
+ setContent(restrictions)
+
+ composeTestRule.onNodeWithText(TEXT).assertIsDisplayed().assertIsEnabled()
+ }
+
+ @Test
+ fun whenRestrictionsKeysIsEmpty_clickable() {
+ val restrictions = Restrictions(userId = USER_ID, keys = emptyList())
+
+ setContent(restrictions)
+ composeTestRule.onRoot().performClick()
+
+ assertThat(menuItemOnClickIsCalled).isTrue()
+ }
+
+ @Test
+ fun whenNoRestricted_enabled() {
+ val restrictions = Restrictions(userId = USER_ID, keys = listOf(RESTRICTION_KEY))
+ fakeRestrictionsProvider.restrictedMode = NoRestricted
+
+ setContent(restrictions)
+
+ composeTestRule.onNodeWithText(TEXT).assertIsDisplayed().assertIsEnabled()
+ }
+
+ @Test
+ fun whenNoRestricted_clickable() {
+ val restrictions = Restrictions(userId = USER_ID, keys = listOf(RESTRICTION_KEY))
+ fakeRestrictionsProvider.restrictedMode = NoRestricted
+
+ setContent(restrictions)
+ composeTestRule.onRoot().performClick()
+
+ assertThat(menuItemOnClickIsCalled).isTrue()
+ }
+
+ @Test
+ fun whenBaseUserRestricted_disabled() {
+ val restrictions = Restrictions(userId = USER_ID, keys = listOf(RESTRICTION_KEY))
+ fakeRestrictionsProvider.restrictedMode = BaseUserRestricted
+
+ setContent(restrictions)
+
+ composeTestRule.onNodeWithText(TEXT).assertIsDisplayed().assertIsNotEnabled()
+ }
+
+ @Test
+ fun whenBaseUserRestricted_notClickable() {
+ val restrictions = Restrictions(userId = USER_ID, keys = listOf(RESTRICTION_KEY))
+ fakeRestrictionsProvider.restrictedMode = BaseUserRestricted
+
+ setContent(restrictions)
+ composeTestRule.onRoot().performClick()
+
+ assertThat(menuItemOnClickIsCalled).isFalse()
+ }
+
+ @Test
+ fun whenBlockedByAdmin_disabled() {
+ val restrictions = Restrictions(userId = USER_ID, keys = listOf(RESTRICTION_KEY))
+ fakeRestrictionsProvider.restrictedMode = fakeBlockedByAdmin
+
+ setContent(restrictions)
+
+ composeTestRule.onNodeWithText(TEXT).assertIsDisplayed().assertIsEnabled()
+ }
+
+ @Test
+ fun whenBlockedByAdmin_onClick_showAdminSupportDetails() {
+ val restrictions = Restrictions(userId = USER_ID, keys = listOf(RESTRICTION_KEY))
+ fakeRestrictionsProvider.restrictedMode = fakeBlockedByAdmin
+
+ setContent(restrictions)
+ composeTestRule.onRoot().performClick()
+
+ assertThat(fakeBlockedByAdmin.sendShowAdminSupportDetailsIntentIsCalled).isTrue()
+ assertThat(menuItemOnClickIsCalled).isFalse()
+ }
+
+ private fun setContent(restrictions: Restrictions) {
+ val fakeMoreOptionsScope = object : MoreOptionsScope {
+ override fun dismiss() {}
+ }
+ composeTestRule.setContent {
+ fakeMoreOptionsScope.RestrictedMenuItemImpl(
+ text = TEXT,
+ restrictions = restrictions,
+ onClick = { menuItemOnClickIsCalled = true },
+ restrictionsProviderFactory = { _, _ -> fakeRestrictionsProvider },
+ )
+ }
+ }
+
+ private companion object {
+ const val TEXT = "Text"
+ const val USER_ID = 0
+ const val RESTRICTION_KEY = "restriction_key"
+ }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/tests/testutils/RestrictedTestUtils.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/tests/testutils/RestrictedTestUtils.kt
new file mode 100644
index 0000000..93fa17d
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/tests/testutils/RestrictedTestUtils.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spaprivileged.tests.testutils
+
+import androidx.compose.runtime.Composable
+import com.android.settingslib.spa.framework.compose.stateOf
+import com.android.settingslib.spaprivileged.model.enterprise.BlockedByAdmin
+import com.android.settingslib.spaprivileged.model.enterprise.RestrictedMode
+import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProvider
+
+class FakeBlockedByAdmin : BlockedByAdmin {
+ var sendShowAdminSupportDetailsIntentIsCalled = false
+
+ override fun getSummary(checked: Boolean?) = SUMMARY
+
+ override fun sendShowAdminSupportDetailsIntent() {
+ sendShowAdminSupportDetailsIntentIsCalled = true
+ }
+
+ companion object {
+ const val SUMMARY = "Blocked by admin"
+ }
+}
+
+class FakeRestrictionsProvider : RestrictionsProvider {
+ var restrictedMode: RestrictedMode? = null
+
+ @Composable
+ override fun restrictedModeState() = stateOf(restrictedMode)
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
index 65c94ce..ca5f57d 100644
--- a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
@@ -1598,6 +1598,11 @@
*/
public boolean isHomeApp;
+ /**
+ * Whether or not it's a cloned app .
+ */
+ public boolean isCloned;
+
public String getNormalizedLabel() {
if (normalizedLabel != null) {
return normalizedLabel;
@@ -1637,7 +1642,12 @@
ThreadUtils.postOnBackgroundThread(
() -> this.ensureLabelDescriptionLocked(context));
}
- this.showInPersonalTab = shouldShowInPersonalTab(context, info.uid);
+ UserManager um = UserManager.get(context);
+ this.showInPersonalTab = shouldShowInPersonalTab(um, info.uid);
+ UserInfo userInfo = um.getUserInfo(UserHandle.getUserId(info.uid));
+ if (userInfo != null) {
+ this.isCloned = userInfo.isCloneProfile();
+ }
}
/**
@@ -1645,8 +1655,7 @@
* {@link UserProperties#SHOW_IN_SETTINGS_WITH_PARENT} set.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
- boolean shouldShowInPersonalTab(Context context, int uid) {
- UserManager userManager = UserManager.get(context);
+ boolean shouldShowInPersonalTab(UserManager userManager, int uid) {
int userId = UserHandle.getUserId(uid);
// Regardless of apk version, if the app belongs to the current user then return true.
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index 9583a59..b929f8c 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -77,9 +77,7 @@
private final LocalBluetoothProfileManager mProfileManager;
private final Object mProfileLock = new Object();
BluetoothDevice mDevice;
- private int mDeviceSide;
- private int mDeviceMode;
- private long mHiSyncId;
+ private HearingAidInfo mHearingAidInfo;
private int mGroupId;
// Need this since there is no method for getting RSSI
@@ -160,7 +158,6 @@
mProfileManager = profileManager;
mDevice = device;
fillData();
- mHiSyncId = BluetoothHearingAid.HI_SYNC_ID_INVALID;
mGroupId = BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
initDrawableCache();
mUnpairing = false;
@@ -339,32 +336,34 @@
connectDevice();
}
- public int getDeviceSide() {
- return mDeviceSide;
+ public HearingAidInfo getHearingAidInfo() {
+ return mHearingAidInfo;
}
- public void setDeviceSide(int side) {
- mDeviceSide = side;
+ public void setHearingAidInfo(HearingAidInfo hearingAidInfo) {
+ mHearingAidInfo = hearingAidInfo;
+ }
+
+ /**
+ * @return {@code true} if {@code cachedBluetoothDevice} is hearing aid device
+ */
+ public boolean isHearingAidDevice() {
+ return mHearingAidInfo != null;
+ }
+
+ public int getDeviceSide() {
+ return mHearingAidInfo != null
+ ? mHearingAidInfo.getSide() : HearingAidInfo.DeviceSide.SIDE_INVALID;
}
public int getDeviceMode() {
- return mDeviceMode;
- }
-
- public void setDeviceMode(int mode) {
- mDeviceMode = mode;
+ return mHearingAidInfo != null
+ ? mHearingAidInfo.getMode() : HearingAidInfo.DeviceMode.MODE_INVALID;
}
public long getHiSyncId() {
- return mHiSyncId;
- }
-
- public void setHiSyncId(long id) {
- mHiSyncId = id;
- }
-
- public boolean isHearingAidDevice() {
- return mHiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID;
+ return mHearingAidInfo != null
+ ? mHearingAidInfo.getHiSyncId() : BluetoothHearingAid.HI_SYNC_ID_INVALID;
}
/**
@@ -784,8 +783,6 @@
ParcelUuid[] localUuids = new ParcelUuid[uuidsList.size()];
uuidsList.toArray(localUuids);
- if (localUuids == null) return false;
-
/*
* Now we know if the device supports PBAP, update permissions...
*/
@@ -1173,9 +1170,10 @@
if (subDevice != null && subDevice.isConnected()) {
stringRes = R.string.bluetooth_hearing_aid_left_and_right_active;
} else {
- if (mDeviceSide == HearingAidProfile.DeviceSide.SIDE_LEFT) {
+ int deviceSide = getDeviceSide();
+ if (deviceSide == HearingAidInfo.DeviceSide.SIDE_LEFT) {
stringRes = R.string.bluetooth_hearing_aid_left_active;
- } else if (mDeviceSide == HearingAidProfile.DeviceSide.SIDE_RIGHT) {
+ } else if (deviceSide == HearingAidInfo.DeviceSide.SIDE_RIGHT) {
stringRes = R.string.bluetooth_hearing_aid_right_active;
} else {
stringRes = R.string.bluetooth_active_no_battery_level;
@@ -1371,15 +1369,41 @@
}
/**
- * @return {@code true} if {@code cachedBluetoothDevice} is Hearing Aid device
+ * @return {@code true} if {@code cachedBluetoothDevice} is ASHA hearing aid device
*/
- public boolean isConnectedHearingAidDevice() {
+ public boolean isConnectedAshaHearingAidDevice() {
HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile();
return hearingAidProfile != null && hearingAidProfile.getConnectionStatus(mDevice) ==
BluetoothProfile.STATE_CONNECTED;
}
/**
+ * @return {@code true} if {@code cachedBluetoothDevice} is HAP device
+ */
+ public boolean isConnectedHapClientDevice() {
+ HapClientProfile hapClientProfile = mProfileManager.getHapClientProfile();
+ return hapClientProfile != null && hapClientProfile.getConnectionStatus(mDevice)
+ == BluetoothProfile.STATE_CONNECTED;
+ }
+
+ /**
+ * @return {@code true} if {@code cachedBluetoothDevice} is LeAudio hearing aid device
+ */
+ public boolean isConnectedLeAudioHearingAidDevice() {
+ return isConnectedHapClientDevice() && isConnectedLeAudioDevice();
+ }
+
+ /**
+ * @return {@code true} if {@code cachedBluetoothDevice} is hearing aid device
+ *
+ * The device may be an ASHA hearing aid that supports {@link HearingAidProfile} or a LeAudio
+ * hearing aid that supports {@link HapClientProfile} and {@link LeAudioProfile}.
+ */
+ public boolean isConnectedHearingAidDevice() {
+ return isConnectedAshaHearingAidDevice() || isConnectedLeAudioHearingAidDevice();
+ }
+
+ /**
* @return {@code true} if {@code cachedBluetoothDevice} is LeAudio device
*/
public boolean isConnectedLeAudioDevice() {
@@ -1407,17 +1431,17 @@
BluetoothDevice tmpDevice = mDevice;
final short tmpRssi = mRssi;
final boolean tmpJustDiscovered = mJustDiscovered;
- final int tmpDeviceSide = mDeviceSide;
+ final HearingAidInfo tmpHearingAidInfo = mHearingAidInfo;
// Set main device from sub device
mDevice = mSubDevice.mDevice;
mRssi = mSubDevice.mRssi;
mJustDiscovered = mSubDevice.mJustDiscovered;
- mDeviceSide = mSubDevice.mDeviceSide;
+ mHearingAidInfo = mSubDevice.mHearingAidInfo;
// Set sub device from backup
mSubDevice.mDevice = tmpDevice;
mSubDevice.mRssi = tmpRssi;
mSubDevice.mJustDiscovered = tmpJustDiscovered;
- mSubDevice.mDeviceSide = tmpDeviceSide;
+ mSubDevice.mHearingAidInfo = tmpHearingAidInfo;
fetchActiveDevices();
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java
index cf4e1ee..ebfec0a 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java
@@ -27,7 +27,7 @@
import java.util.Set;
/**
- * HearingAidDeviceManager manages the set of remote HearingAid Bluetooth devices.
+ * HearingAidDeviceManager manages the set of remote HearingAid(ASHA) Bluetooth devices.
*/
public class HearingAidDeviceManager {
private static final String TAG = "HearingAidDeviceManager";
@@ -44,12 +44,12 @@
void initHearingAidDeviceIfNeeded(CachedBluetoothDevice newDevice) {
long hiSyncId = getHiSyncId(newDevice.getDevice());
if (isValidHiSyncId(hiSyncId)) {
- // Once hiSyncId is valid, assign hiSyncId
- newDevice.setHiSyncId(hiSyncId);
- final int side = getDeviceSide(newDevice.getDevice());
- final int mode = getDeviceMode(newDevice.getDevice());
- newDevice.setDeviceSide(side);
- newDevice.setDeviceMode(mode);
+ // Once hiSyncId is valid, assign hearing aid info
+ final HearingAidInfo.Builder infoBuilder = new HearingAidInfo.Builder()
+ .setAshaDeviceSide(getDeviceSide(newDevice.getDevice()))
+ .setAshaDeviceMode(getDeviceMode(newDevice.getDevice()))
+ .setHiSyncId(hiSyncId);
+ newDevice.setHearingAidInfo(infoBuilder.build());
}
}
@@ -123,12 +123,14 @@
final long newHiSyncId = getHiSyncId(cachedDevice.getDevice());
// Do nothing if there is no HiSyncId on Bluetooth device
if (isValidHiSyncId(newHiSyncId)) {
- cachedDevice.setHiSyncId(newHiSyncId);
+ // Once hiSyncId is valid, assign hearing aid info
+ final HearingAidInfo.Builder infoBuilder = new HearingAidInfo.Builder()
+ .setAshaDeviceSide(getDeviceSide(cachedDevice.getDevice()))
+ .setAshaDeviceMode(getDeviceMode(cachedDevice.getDevice()))
+ .setHiSyncId(newHiSyncId);
+ cachedDevice.setHearingAidInfo(infoBuilder.build());
+
newSyncIdSet.add(newHiSyncId);
- final int side = getDeviceSide(cachedDevice.getDevice());
- final int mode = getDeviceMode(cachedDevice.getDevice());
- cachedDevice.setDeviceSide(side);
- cachedDevice.setDeviceMode(mode);
}
}
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidInfo.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidInfo.java
new file mode 100644
index 0000000..ef08c92
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidInfo.java
@@ -0,0 +1,263 @@
+/*
+ * 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.bluetooth;
+
+import android.annotation.IntDef;
+import android.bluetooth.BluetoothHearingAid;
+import android.bluetooth.BluetoothLeAudio;
+import android.util.SparseIntArray;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/** Hearing aids information and constants that shared within hearing aids related profiles */
+public class HearingAidInfo {
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ DeviceSide.SIDE_INVALID,
+ DeviceSide.SIDE_LEFT,
+ DeviceSide.SIDE_RIGHT,
+ DeviceSide.SIDE_LEFT_AND_RIGHT,
+ })
+
+ /** Side definition for hearing aids. */
+ public @interface DeviceSide {
+ int SIDE_INVALID = -1;
+ int SIDE_LEFT = 0;
+ int SIDE_RIGHT = 1;
+ int SIDE_LEFT_AND_RIGHT = 2;
+ }
+
+ @Retention(java.lang.annotation.RetentionPolicy.SOURCE)
+ @IntDef({
+ DeviceMode.MODE_INVALID,
+ DeviceMode.MODE_MONAURAL,
+ DeviceMode.MODE_BINAURAL,
+ DeviceMode.MODE_BANDED,
+ })
+
+ /** Mode definition for hearing aids. */
+ public @interface DeviceMode {
+ int MODE_INVALID = -1;
+ int MODE_MONAURAL = 0;
+ int MODE_BINAURAL = 1;
+ int MODE_BANDED = 2;
+ }
+
+ private final int mSide;
+ private final int mMode;
+ private final long mHiSyncId;
+
+ private HearingAidInfo(int side, int mode, long hiSyncId) {
+ mSide = side;
+ mMode = mode;
+ mHiSyncId = hiSyncId;
+ }
+
+ @DeviceSide
+ public int getSide() {
+ return mSide;
+ }
+
+ @DeviceMode
+ public int getMode() {
+ return mMode;
+ }
+
+ public long getHiSyncId() {
+ return mHiSyncId;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof HearingAidInfo)) {
+ return false;
+ }
+ HearingAidInfo that = (HearingAidInfo) o;
+ return mSide == that.mSide && mMode == that.mMode && mHiSyncId == that.mHiSyncId;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mSide, mMode, mHiSyncId);
+ }
+
+ @Override
+ public String toString() {
+ return "HearingAidInfo{"
+ + "mSide=" + mSide
+ + ", mMode=" + mMode
+ + ", mHiSyncId=" + mHiSyncId
+ + '}';
+ }
+
+ @DeviceSide
+ private static int convertAshaDeviceSideToInternalSide(int ashaDeviceSide) {
+ return ASHA_DEVICE_SIDE_TO_INTERNAL_SIDE_MAPPING.get(
+ ashaDeviceSide, DeviceSide.SIDE_INVALID);
+ }
+
+ @DeviceMode
+ private static int convertAshaDeviceModeToInternalMode(int ashaDeviceMode) {
+ return ASHA_DEVICE_MODE_TO_INTERNAL_MODE_MAPPING.get(
+ ashaDeviceMode, DeviceMode.MODE_INVALID);
+ }
+
+ @DeviceSide
+ private static int convertLeAudioLocationToInternalSide(int leAudioLocation) {
+ boolean isLeft = (leAudioLocation & LE_AUDIO_LOCATION_LEFT) != 0;
+ boolean isRight = (leAudioLocation & LE_AUDIO_LOCATION_RIGHT) != 0;
+ if (isLeft && isRight) {
+ return DeviceSide.SIDE_LEFT_AND_RIGHT;
+ } else if (isLeft) {
+ return DeviceSide.SIDE_LEFT;
+ } else if (isRight) {
+ return DeviceSide.SIDE_RIGHT;
+ }
+ return DeviceSide.SIDE_INVALID;
+ }
+
+ @DeviceMode
+ private static int convertHapDeviceTypeToInternalMode(int hapDeviceType) {
+ return HAP_DEVICE_TYPE_TO_INTERNAL_MODE_MAPPING.get(hapDeviceType, DeviceMode.MODE_INVALID);
+ }
+
+ /** Builder class for constructing {@link HearingAidInfo} objects. */
+ public static final class Builder {
+ private int mSide = DeviceSide.SIDE_INVALID;
+ private int mMode = DeviceMode.MODE_INVALID;
+ private long mHiSyncId = BluetoothHearingAid.HI_SYNC_ID_INVALID;
+
+ /**
+ * Configure the hearing device mode.
+ * @param ashaDeviceMode one of the hearing aid device modes defined in HearingAidProfile
+ * {@link HearingAidProfile.DeviceMode}
+ */
+ public Builder setAshaDeviceMode(int ashaDeviceMode) {
+ mMode = convertAshaDeviceModeToInternalMode(ashaDeviceMode);
+ return this;
+ }
+
+ /**
+ * Configure the hearing device mode.
+ * @param hapDeviceType one of the hearing aid device types defined in HapClientProfile
+ * {@link HapClientProfile.HearingAidType}
+ */
+ public Builder setHapDeviceType(int hapDeviceType) {
+ mMode = convertHapDeviceTypeToInternalMode(hapDeviceType);
+ return this;
+ }
+
+ /**
+ * Configure the hearing device side.
+ * @param ashaDeviceSide one of the hearing aid device sides defined in HearingAidProfile
+ * {@link HearingAidProfile.DeviceSide}
+ */
+ public Builder setAshaDeviceSide(int ashaDeviceSide) {
+ mSide = convertAshaDeviceSideToInternalSide(ashaDeviceSide);
+ return this;
+ }
+
+ /**
+ * Configure the hearing device side.
+ * @param leAudioLocation one of the audio location defined in BluetoothLeAudio
+ * {@link BluetoothLeAudio.AudioLocation}
+ */
+ public Builder setLeAudioLocation(int leAudioLocation) {
+ mSide = convertLeAudioLocationToInternalSide(leAudioLocation);
+ return this;
+ }
+
+ /**
+ * Configure the hearing aid hiSyncId.
+ * @param hiSyncId the ASHA hearing aid id
+ */
+ public Builder setHiSyncId(long hiSyncId) {
+ mHiSyncId = hiSyncId;
+ return this;
+ }
+
+ /** Build the configured {@link HearingAidInfo} */
+ public HearingAidInfo build() {
+ return new HearingAidInfo(mSide, mMode, mHiSyncId);
+ }
+ }
+
+ private static final int LE_AUDIO_LOCATION_LEFT =
+ BluetoothLeAudio.AUDIO_LOCATION_FRONT_LEFT
+ | BluetoothLeAudio.AUDIO_LOCATION_BACK_LEFT
+ | BluetoothLeAudio.AUDIO_LOCATION_FRONT_LEFT_OF_CENTER
+ | BluetoothLeAudio.AUDIO_LOCATION_SIDE_LEFT
+ | BluetoothLeAudio.AUDIO_LOCATION_TOP_FRONT_LEFT
+ | BluetoothLeAudio.AUDIO_LOCATION_TOP_BACK_LEFT
+ | BluetoothLeAudio.AUDIO_LOCATION_TOP_SIDE_LEFT
+ | BluetoothLeAudio.AUDIO_LOCATION_BOTTOM_FRONT_LEFT
+ | BluetoothLeAudio.AUDIO_LOCATION_FRONT_LEFT_WIDE
+ | BluetoothLeAudio.AUDIO_LOCATION_LEFT_SURROUND;
+
+ private static final int LE_AUDIO_LOCATION_RIGHT =
+ BluetoothLeAudio.AUDIO_LOCATION_FRONT_RIGHT
+ | BluetoothLeAudio.AUDIO_LOCATION_BACK_RIGHT
+ | BluetoothLeAudio.AUDIO_LOCATION_FRONT_RIGHT_OF_CENTER
+ | BluetoothLeAudio.AUDIO_LOCATION_SIDE_RIGHT
+ | BluetoothLeAudio.AUDIO_LOCATION_TOP_FRONT_RIGHT
+ | BluetoothLeAudio.AUDIO_LOCATION_TOP_BACK_RIGHT
+ | BluetoothLeAudio.AUDIO_LOCATION_TOP_SIDE_RIGHT
+ | BluetoothLeAudio.AUDIO_LOCATION_BOTTOM_FRONT_RIGHT
+ | BluetoothLeAudio.AUDIO_LOCATION_FRONT_RIGHT_WIDE
+ | BluetoothLeAudio.AUDIO_LOCATION_RIGHT_SURROUND;
+
+ private static final SparseIntArray ASHA_DEVICE_SIDE_TO_INTERNAL_SIDE_MAPPING;
+ private static final SparseIntArray ASHA_DEVICE_MODE_TO_INTERNAL_MODE_MAPPING;
+ private static final SparseIntArray HAP_DEVICE_TYPE_TO_INTERNAL_MODE_MAPPING;
+
+ static {
+ ASHA_DEVICE_SIDE_TO_INTERNAL_SIDE_MAPPING = new SparseIntArray();
+ ASHA_DEVICE_SIDE_TO_INTERNAL_SIDE_MAPPING.put(
+ HearingAidProfile.DeviceSide.SIDE_INVALID, DeviceSide.SIDE_INVALID);
+ ASHA_DEVICE_SIDE_TO_INTERNAL_SIDE_MAPPING.put(
+ HearingAidProfile.DeviceSide.SIDE_LEFT, DeviceSide.SIDE_LEFT);
+ ASHA_DEVICE_SIDE_TO_INTERNAL_SIDE_MAPPING.put(
+ HearingAidProfile.DeviceSide.SIDE_RIGHT, DeviceSide.SIDE_RIGHT);
+
+ ASHA_DEVICE_MODE_TO_INTERNAL_MODE_MAPPING = new SparseIntArray();
+ ASHA_DEVICE_MODE_TO_INTERNAL_MODE_MAPPING.put(
+ HearingAidProfile.DeviceMode.MODE_INVALID, DeviceMode.MODE_INVALID);
+ ASHA_DEVICE_MODE_TO_INTERNAL_MODE_MAPPING.put(
+ HearingAidProfile.DeviceMode.MODE_MONAURAL, DeviceMode.MODE_MONAURAL);
+ ASHA_DEVICE_MODE_TO_INTERNAL_MODE_MAPPING.put(
+ HearingAidProfile.DeviceMode.MODE_BINAURAL, DeviceMode.MODE_BINAURAL);
+
+ HAP_DEVICE_TYPE_TO_INTERNAL_MODE_MAPPING = new SparseIntArray();
+ HAP_DEVICE_TYPE_TO_INTERNAL_MODE_MAPPING.put(
+ HapClientProfile.HearingAidType.TYPE_INVALID, DeviceMode.MODE_INVALID);
+ HAP_DEVICE_TYPE_TO_INTERNAL_MODE_MAPPING.put(
+ HapClientProfile.HearingAidType.TYPE_BINAURAL, DeviceMode.MODE_BINAURAL);
+ HAP_DEVICE_TYPE_TO_INTERNAL_MODE_MAPPING.put(
+ HapClientProfile.HearingAidType.TYPE_MONAURAL, DeviceMode.MODE_MONAURAL);
+ HAP_DEVICE_TYPE_TO_INTERNAL_MODE_MAPPING.put(
+ HapClientProfile.HearingAidType.TYPE_BANDED, DeviceMode.MODE_BANDED);
+ HAP_DEVICE_TYPE_TO_INTERNAL_MODE_MAPPING.put(
+ HapClientProfile.HearingAidType.TYPE_RFU, DeviceMode.MODE_INVALID);
+
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
index fb861da..a3c2e70 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
@@ -21,6 +21,7 @@
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothCsipSetCoordinator;
import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHapClient;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothHeadsetClient;
import android.bluetooth.BluetoothHearingAid;
@@ -103,6 +104,7 @@
private PbapClientProfile mPbapClientProfile;
private PbapServerProfile mPbapProfile;
private HearingAidProfile mHearingAidProfile;
+ private HapClientProfile mHapClientProfile;
private CsipSetCoordinatorProfile mCsipSetCoordinatorProfile;
private LeAudioProfile mLeAudioProfile;
private LocalBluetoothLeBroadcast mLeAudioBroadcast;
@@ -189,6 +191,12 @@
addProfile(mHearingAidProfile, HearingAidProfile.NAME,
BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED);
}
+ if (mHapClientProfile == null && supportedList.contains(BluetoothProfile.HAP_CLIENT)) {
+ if (DEBUG) Log.d(TAG, "Adding local HAP_CLIENT profile");
+ mHapClientProfile = new HapClientProfile(mContext, mDeviceManager, this);
+ addProfile(mHapClientProfile, HapClientProfile.NAME,
+ BluetoothHapClient.ACTION_HAP_CONNECTION_STATE_CHANGED);
+ }
if (mHidProfile == null && supportedList.contains(BluetoothProfile.HID_HOST)) {
if (DEBUG) Log.d(TAG, "Adding local HID_HOST profile");
mHidProfile = new HidProfile(mContext, mDeviceManager, this);
@@ -337,25 +345,44 @@
Log.i(TAG, "Failed to connect " + mProfile + " device");
}
- if (getHearingAidProfile() != null &&
- mProfile instanceof HearingAidProfile &&
- (newState == BluetoothProfile.STATE_CONNECTED)) {
- final int side = getHearingAidProfile().getDeviceSide(cachedDevice.getDevice());
- final int mode = getHearingAidProfile().getDeviceMode(cachedDevice.getDevice());
- cachedDevice.setDeviceSide(side);
- cachedDevice.setDeviceMode(mode);
+ if (getHearingAidProfile() != null
+ && mProfile instanceof HearingAidProfile
+ && (newState == BluetoothProfile.STATE_CONNECTED)) {
// Check if the HiSyncID has being initialized
if (cachedDevice.getHiSyncId() == BluetoothHearingAid.HI_SYNC_ID_INVALID) {
long newHiSyncId = getHearingAidProfile().getHiSyncId(cachedDevice.getDevice());
if (newHiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID) {
- cachedDevice.setHiSyncId(newHiSyncId);
+ final BluetoothDevice device = cachedDevice.getDevice();
+ final HearingAidInfo.Builder infoBuilder = new HearingAidInfo.Builder()
+ .setAshaDeviceSide(getHearingAidProfile().getDeviceSide(device))
+ .setAshaDeviceMode(getHearingAidProfile().getDeviceMode(device))
+ .setHiSyncId(newHiSyncId);
+ cachedDevice.setHearingAidInfo(infoBuilder.build());
}
}
-
HearingAidStatsLogUtils.logHearingAidInfo(cachedDevice);
}
+ final boolean isHapClientProfile = getHapClientProfile() != null
+ && mProfile instanceof HapClientProfile;
+ final boolean isLeAudioProfile = getLeAudioProfile() != null
+ && mProfile instanceof LeAudioProfile;
+ final boolean isHapClientOrLeAudioProfile = isHapClientProfile || isLeAudioProfile;
+ if (isHapClientOrLeAudioProfile && newState == BluetoothProfile.STATE_CONNECTED) {
+
+ // Checks if both profiles are connected to the device. Hearing aid info need
+ // to be retrieved from these profiles separately.
+ if (cachedDevice.isConnectedLeAudioHearingAidDevice()) {
+ final BluetoothDevice device = cachedDevice.getDevice();
+ final HearingAidInfo.Builder infoBuilder = new HearingAidInfo.Builder()
+ .setLeAudioLocation(getLeAudioProfile().getAudioLocation(device))
+ .setHapDeviceType(getHapClientProfile().getHearingAidType(device));
+ cachedDevice.setHearingAidInfo(infoBuilder.build());
+ HearingAidStatsLogUtils.logHearingAidInfo(cachedDevice);
+ }
+ }
+
if (getCsipSetCoordinatorProfile() != null
&& mProfile instanceof CsipSetCoordinatorProfile
&& newState == BluetoothProfile.STATE_CONNECTED) {
@@ -524,6 +551,10 @@
return mHearingAidProfile;
}
+ public HapClientProfile getHapClientProfile() {
+ return mHapClientProfile;
+ }
+
public LeAudioProfile getLeAudioProfile() {
return mLeAudioProfile;
}
@@ -675,6 +706,11 @@
removedProfiles.remove(mHearingAidProfile);
}
+ if (mHapClientProfile != null && ArrayUtils.contains(uuids, BluetoothUuid.HAS)) {
+ profiles.add(mHapClientProfile);
+ removedProfiles.remove(mHapClientProfile);
+ }
+
if (mSapProfile != null && ArrayUtils.contains(uuids, BluetoothUuid.SAP)) {
profiles.add(mSapProfile);
removedProfiles.remove(mSapProfile);
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaDeviceUtils.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaDeviceUtils.java
index df6929e..b3a52b9 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/MediaDeviceUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/MediaDeviceUtils.java
@@ -16,6 +16,7 @@
package com.android.settingslib.media;
import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHearingAid;
import android.media.MediaRoute2Info;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
@@ -32,7 +33,9 @@
*/
public static String getId(CachedBluetoothDevice cachedDevice) {
if (cachedDevice.isHearingAidDevice()) {
- return Long.toString(cachedDevice.getHiSyncId());
+ if (cachedDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID) {
+ return Long.toString(cachedDevice.getHiSyncId());
+ }
}
return cachedDevice.getAddress();
}
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 39875f7..96e64ea 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
@@ -755,28 +755,30 @@
@Test
public void shouldShowInPersonalTab_forCurrentUser_returnsTrue() {
+ UserManager um = RuntimeEnvironment.application.getSystemService(UserManager.class);
ApplicationInfo appInfo = createApplicationInfo(PKG_1);
AppEntry primaryUserApp = createAppEntry(appInfo, 1);
- assertThat(primaryUserApp.shouldShowInPersonalTab(mContext, appInfo.uid)).isTrue();
+ assertThat(primaryUserApp.shouldShowInPersonalTab(um, appInfo.uid)).isTrue();
}
@Test
public void shouldShowInPersonalTab_userProfilePreU_returnsFalse() {
+ UserManager um = RuntimeEnvironment.application.getSystemService(UserManager.class);
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();
+ assertThat(nonPrimaryUserApp.shouldShowInPersonalTab(um, 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));
+ UserManager um = RuntimeEnvironment.application.getSystemService(UserManager.class);
+ ShadowUserManager shadowUserManager = Shadow.extract(um);
shadowUserManager.addProfile(USER_SYSTEM, PROFILE_USERID,
CLONE_USER, 0);
shadowUserManager.addProfile(USER_SYSTEM, PROFILE_USERID2,
@@ -796,7 +798,7 @@
AppEntry nonPrimaryUserApp1 = createAppEntry(appInfo1, 1);
AppEntry nonPrimaryUserApp2 = createAppEntry(appInfo2, 2);
- assertThat(nonPrimaryUserApp1.shouldShowInPersonalTab(mContext, appInfo1.uid)).isTrue();
- assertThat(nonPrimaryUserApp2.shouldShowInPersonalTab(mContext, appInfo2.uid)).isFalse();
+ assertThat(nonPrimaryUserApp1.shouldShowInPersonalTab(um, appInfo1.uid)).isTrue();
+ assertThat(nonPrimaryUserApp2.shouldShowInPersonalTab(um, appInfo2.uid)).isFalse();
}
}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManagerTest.java
index 61802a8..f06623d 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManagerTest.java
@@ -327,8 +327,10 @@
when(mDevice1.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
CachedBluetoothDevice cachedDevice1 = mCachedDeviceManager.addDevice(mDevice1);
CachedBluetoothDevice cachedDevice2 = mCachedDeviceManager.addDevice(mDevice2);
- cachedDevice1.setHiSyncId(HISYNCID1);
- cachedDevice2.setHiSyncId(HISYNCID1);
+ cachedDevice1.setHearingAidInfo(
+ new HearingAidInfo.Builder().setHiSyncId(HISYNCID1).build());
+ cachedDevice2.setHearingAidInfo(
+ new HearingAidInfo.Builder().setHiSyncId(HISYNCID1).build());
cachedDevice1.setSubDevice(cachedDevice2);
// Call onDeviceUnpaired for the one in mCachedDevices.
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
index 79e9938..1f518ec 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
@@ -74,6 +74,10 @@
@Mock
private HearingAidProfile mHearingAidProfile;
@Mock
+ private HapClientProfile mHapClientProfile;
+ @Mock
+ private LeAudioProfile mLeAudioProfile;
+ @Mock
private BluetoothDevice mDevice;
@Mock
private BluetoothDevice mSubDevice;
@@ -354,7 +358,7 @@
assertThat(mCachedDevice.getConnectionSummary()).isNull();
// Set device as Active for Hearing Aid and test connection state summary
- mCachedDevice.setDeviceSide(HearingAidProfile.DeviceSide.SIDE_LEFT);
+ mCachedDevice.setHearingAidInfo(getLeftAshaHearingAidInfo());
mCachedDevice.onActiveDeviceChanged(true, BluetoothProfile.HEARING_AID);
assertThat(mCachedDevice.getConnectionSummary()).isEqualTo("Active, left only");
@@ -399,7 +403,7 @@
// 1. Profile: {HEARING_AID, Connected, Active, Right ear}
// 2. Audio Manager: In Call
updateProfileStatus(mHearingAidProfile, BluetoothProfile.STATE_CONNECTED);
- mCachedDevice.setDeviceSide(HearingAidProfile.DeviceSide.SIDE_RIGHT);
+ mCachedDevice.setHearingAidInfo(getRightAshaHearingAidInfo());
mCachedDevice.onActiveDeviceChanged(true, BluetoothProfile.HEARING_AID);
mAudioManager.setMode(AudioManager.MODE_IN_CALL);
@@ -413,9 +417,9 @@
// Arrange:
// 1. Profile: {HEARING_AID, Connected, Active, Both ear}
// 2. Audio Manager: In Call
- mCachedDevice.setDeviceSide(HearingAidProfile.DeviceSide.SIDE_RIGHT);
+ mCachedDevice.setHearingAidInfo(getRightAshaHearingAidInfo());
updateProfileStatus(mHearingAidProfile, BluetoothProfile.STATE_CONNECTED);
- mSubCachedDevice.setDeviceSide(HearingAidProfile.DeviceSide.SIDE_LEFT);
+ mSubCachedDevice.setHearingAidInfo(getLeftAshaHearingAidInfo());
updateSubDeviceProfileStatus(mHearingAidProfile, BluetoothProfile.STATE_CONNECTED);
mCachedDevice.setSubDevice(mSubCachedDevice);
mCachedDevice.onActiveDeviceChanged(true, BluetoothProfile.HEARING_AID);
@@ -859,21 +863,21 @@
}
@Test
- public void isConnectedHearingAidDevice_connected_returnTrue() {
+ public void isConnectedAshaHearingAidDevice_connected_returnTrue() {
when(mProfileManager.getHearingAidProfile()).thenReturn(mHearingAidProfile);
when(mHearingAidProfile.getConnectionStatus(mDevice)).
thenReturn(BluetoothProfile.STATE_CONNECTED);
- assertThat(mCachedDevice.isConnectedHearingAidDevice()).isTrue();
+ assertThat(mCachedDevice.isConnectedAshaHearingAidDevice()).isTrue();
}
@Test
- public void isConnectedHearingAidDevice_disconnected_returnFalse() {
+ public void isConnectedAshaHearingAidDevice_disconnected_returnFalse() {
when(mProfileManager.getHearingAidProfile()).thenReturn(mHearingAidProfile);
when(mHearingAidProfile.getConnectionStatus(mDevice)).
thenReturn(BluetoothProfile.STATE_DISCONNECTED);
- assertThat(mCachedDevice.isConnectedHearingAidDevice()).isFalse();
+ assertThat(mCachedDevice.isConnectedAshaHearingAidDevice()).isFalse();
}
@Test
@@ -891,10 +895,10 @@
}
@Test
- public void isConnectedHearingAidDevice_profileIsNull_returnFalse() {
+ public void isConnectedAshaHearingAidDevice_profileIsNull_returnFalse() {
when(mProfileManager.getHearingAidProfile()).thenReturn(null);
- assertThat(mCachedDevice.isConnectedHearingAidDevice()).isFalse();
+ assertThat(mCachedDevice.isConnectedAshaHearingAidDevice()).isFalse();
}
@Test
@@ -965,10 +969,10 @@
mCachedDevice.mRssi = RSSI_1;
mCachedDevice.mJustDiscovered = JUSTDISCOVERED_1;
- mCachedDevice.setDeviceSide(HearingAidProfile.DeviceSide.SIDE_LEFT);
+ mCachedDevice.setHearingAidInfo(getLeftAshaHearingAidInfo());
mSubCachedDevice.mRssi = RSSI_2;
mSubCachedDevice.mJustDiscovered = JUSTDISCOVERED_2;
- mSubCachedDevice.setDeviceSide(HearingAidProfile.DeviceSide.SIDE_RIGHT);
+ mSubCachedDevice.setHearingAidInfo(getRightAshaHearingAidInfo());
mCachedDevice.setSubDevice(mSubCachedDevice);
mCachedDevice.switchSubDeviceContent();
@@ -976,22 +980,20 @@
assertThat(mCachedDevice.mRssi).isEqualTo(RSSI_2);
assertThat(mCachedDevice.mJustDiscovered).isEqualTo(JUSTDISCOVERED_2);
assertThat(mCachedDevice.mDevice).isEqualTo(mSubDevice);
- assertThat(mCachedDevice.getDeviceSide()).isEqualTo(
- HearingAidProfile.DeviceSide.SIDE_RIGHT);
+ assertThat(mCachedDevice.getDeviceSide()).isEqualTo(HearingAidInfo.DeviceSide.SIDE_RIGHT);
assertThat(mSubCachedDevice.mRssi).isEqualTo(RSSI_1);
assertThat(mSubCachedDevice.mJustDiscovered).isEqualTo(JUSTDISCOVERED_1);
assertThat(mSubCachedDevice.mDevice).isEqualTo(mDevice);
- assertThat(mSubCachedDevice.getDeviceSide()).isEqualTo(
- HearingAidProfile.DeviceSide.SIDE_LEFT);
+ assertThat(mSubCachedDevice.getDeviceSide()).isEqualTo(HearingAidInfo.DeviceSide.SIDE_LEFT);
}
@Test
public void getConnectionSummary_profileConnectedFail_showErrorMessage() {
- final A2dpProfile profle = mock(A2dpProfile.class);
- mCachedDevice.onProfileStateChanged(profle, BluetoothProfile.STATE_CONNECTED);
+ final A2dpProfile profile = mock(A2dpProfile.class);
+ mCachedDevice.onProfileStateChanged(profile, BluetoothProfile.STATE_CONNECTED);
mCachedDevice.setProfileConnectedStatus(BluetoothProfile.A2DP, true);
- when(profle.getConnectionStatus(mDevice)).thenReturn(BluetoothProfile.STATE_CONNECTED);
+ when(profile.getConnectionStatus(mDevice)).thenReturn(BluetoothProfile.STATE_CONNECTED);
assertThat(mCachedDevice.getConnectionSummary()).isEqualTo(
mContext.getString(R.string.profile_connect_timeout_subtext));
@@ -1069,4 +1071,48 @@
assertThat(mSubCachedDevice.mDevice).isEqualTo(mDevice);
assertThat(mCachedDevice.getMemberDevice().contains(mSubCachedDevice)).isTrue();
}
+
+ @Test
+ public void isConnectedHearingAidDevice_isConnectedAshaHearingAidDevice_returnTrue() {
+ when(mProfileManager.getHearingAidProfile()).thenReturn(mHearingAidProfile);
+
+ updateProfileStatus(mHearingAidProfile, BluetoothProfile.STATE_CONNECTED);
+
+ assertThat(mCachedDevice.isConnectedHearingAidDevice()).isTrue();
+ }
+
+ @Test
+ public void isConnectedHearingAidDevice_isConnectedLeAudioHearingAidDevice_returnTrue() {
+ when(mProfileManager.getHapClientProfile()).thenReturn(mHapClientProfile);
+ when(mProfileManager.getLeAudioProfile()).thenReturn(mLeAudioProfile);
+
+ updateProfileStatus(mHapClientProfile, BluetoothProfile.STATE_CONNECTED);
+ updateProfileStatus(mLeAudioProfile, BluetoothProfile.STATE_CONNECTED);
+
+ assertThat(mCachedDevice.isConnectedHearingAidDevice()).isTrue();
+ }
+
+ @Test
+ public void isConnectedHearingAidDevice_isNotAnyConnectedHearingAidDevice_returnFalse() {
+ when(mProfileManager.getHearingAidProfile()).thenReturn(mHearingAidProfile);
+ when(mProfileManager.getHapClientProfile()).thenReturn(mHapClientProfile);
+ when(mProfileManager.getLeAudioProfile()).thenReturn(mLeAudioProfile);
+
+ updateProfileStatus(mHearingAidProfile, BluetoothProfile.STATE_DISCONNECTED);
+ updateProfileStatus(mHapClientProfile, BluetoothProfile.STATE_DISCONNECTED);
+ updateProfileStatus(mLeAudioProfile, BluetoothProfile.STATE_DISCONNECTED);
+
+ assertThat(mCachedDevice.isConnectedHearingAidDevice()).isFalse();
+ }
+
+ private HearingAidInfo getLeftAshaHearingAidInfo() {
+ return new HearingAidInfo.Builder()
+ .setAshaDeviceSide(HearingAidProfile.DeviceSide.SIDE_LEFT)
+ .build();
+ }
+ private HearingAidInfo getRightAshaHearingAidInfo() {
+ return new HearingAidInfo.Builder()
+ .setAshaDeviceSide(HearingAidProfile.DeviceSide.SIDE_RIGHT)
+ .build();
+ }
}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java
index 611b0a4..470d8e0 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java
@@ -17,6 +17,7 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
@@ -106,7 +107,7 @@
* deviceSide, deviceMode.
*/
@Test
- public void initHearingAidDeviceIfNeeded_validHiSyncId_setHearingAidInfos() {
+ public void initHearingAidDeviceIfNeeded_validHiSyncId_setHearingAidInfo() {
when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn(HISYNCID1);
when(mHearingAidProfile.getDeviceMode(mDevice1)).thenReturn(
HearingAidProfile.DeviceMode.MODE_BINAURAL);
@@ -118,21 +119,22 @@
assertThat(mCachedDevice1.getHiSyncId()).isEqualTo(HISYNCID1);
assertThat(mCachedDevice1.getDeviceMode()).isEqualTo(
- HearingAidProfile.DeviceMode.MODE_BINAURAL);
+ HearingAidInfo.DeviceMode.MODE_BINAURAL);
assertThat(mCachedDevice1.getDeviceSide()).isEqualTo(
- HearingAidProfile.DeviceSide.SIDE_RIGHT);
+ HearingAidInfo.DeviceSide.SIDE_RIGHT);
}
/**
* Test initHearingAidDeviceIfNeeded, an invalid HiSyncId will not be assigned
*/
@Test
- public void initHearingAidDeviceIfNeeded_invalidHiSyncId_notToSetHiSyncId() {
+ public void initHearingAidDeviceIfNeeded_invalidHiSyncId_notToSetHearingAidInfo() {
when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn(
BluetoothHearingAid.HI_SYNC_ID_INVALID);
+
mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(mCachedDevice1);
- verify(mCachedDevice1, never()).setHiSyncId(anyLong());
+ verify(mCachedDevice1, never()).setHearingAidInfo(any(HearingAidInfo.class));
}
/**
@@ -140,9 +142,10 @@
*/
@Test
public void setSubDeviceIfNeeded_sameHiSyncId_setSubDevice() {
- mCachedDevice1.setHiSyncId(HISYNCID1);
- mCachedDevice2.setHiSyncId(HISYNCID1);
+ mCachedDevice1.setHearingAidInfo(getLeftAshaHearingAidInfo(HISYNCID1));
+ mCachedDevice2.setHearingAidInfo(getRightAshaHearingAidInfo(HISYNCID1));
mCachedDeviceManager.mCachedDevices.add(mCachedDevice1);
+
mHearingAidDeviceManager.setSubDeviceIfNeeded(mCachedDevice2);
assertThat(mCachedDevice1.getSubDevice()).isEqualTo(mCachedDevice2);
@@ -153,9 +156,10 @@
*/
@Test
public void setSubDeviceIfNeeded_differentHiSyncId_notSetSubDevice() {
- mCachedDevice1.setHiSyncId(HISYNCID1);
- mCachedDevice2.setHiSyncId(HISYNCID2);
+ mCachedDevice1.setHearingAidInfo(getLeftAshaHearingAidInfo(HISYNCID1));
+ mCachedDevice2.setHearingAidInfo(getRightAshaHearingAidInfo(HISYNCID2));
mCachedDeviceManager.mCachedDevices.add(mCachedDevice1);
+
mHearingAidDeviceManager.setSubDeviceIfNeeded(mCachedDevice2);
assertThat(mCachedDevice1.getSubDevice()).isNull();
@@ -276,9 +280,9 @@
assertThat(mCachedDevice1.getHiSyncId()).isEqualTo(HISYNCID1);
assertThat(mCachedDevice1.getDeviceMode()).isEqualTo(
- HearingAidProfile.DeviceMode.MODE_BINAURAL);
+ HearingAidInfo.DeviceMode.MODE_BINAURAL);
assertThat(mCachedDevice1.getDeviceSide()).isEqualTo(
- HearingAidProfile.DeviceSide.SIDE_RIGHT);
+ HearingAidInfo.DeviceSide.SIDE_RIGHT);
verify(mHearingAidDeviceManager).onHiSyncIdChanged(HISYNCID1);
}
@@ -389,11 +393,9 @@
@Test
public void onProfileConnectionStateChanged_disconnected_mainDevice_subDeviceConnected_switch()
{
- when(mCachedDevice1.getHiSyncId()).thenReturn(HISYNCID1);
- mCachedDevice1.setDeviceSide(HearingAidProfile.DeviceSide.SIDE_LEFT);
- when(mCachedDevice2.getHiSyncId()).thenReturn(HISYNCID1);
+ mCachedDevice1.setHearingAidInfo(getLeftAshaHearingAidInfo(HISYNCID1));
+ mCachedDevice2.setHearingAidInfo(getRightAshaHearingAidInfo(HISYNCID1));
when(mCachedDevice2.isConnected()).thenReturn(true);
- mCachedDevice2.setDeviceSide(HearingAidProfile.DeviceSide.SIDE_RIGHT);
mCachedDeviceManager.mCachedDevices.add(mCachedDevice1);
mCachedDevice1.setSubDevice(mCachedDevice2);
@@ -404,10 +406,8 @@
assertThat(mCachedDevice1.mDevice).isEqualTo(mDevice2);
assertThat(mCachedDevice2.mDevice).isEqualTo(mDevice1);
- assertThat(mCachedDevice1.getDeviceSide()).isEqualTo(
- HearingAidProfile.DeviceSide.SIDE_RIGHT);
- assertThat(mCachedDevice2.getDeviceSide()).isEqualTo(
- HearingAidProfile.DeviceSide.SIDE_LEFT);
+ assertThat(mCachedDevice1.getDeviceSide()).isEqualTo(HearingAidInfo.DeviceSide.SIDE_RIGHT);
+ assertThat(mCachedDevice2.getDeviceSide()).isEqualTo(HearingAidInfo.DeviceSide.SIDE_LEFT);
verify(mCachedDevice1).refresh();
}
@@ -455,4 +455,18 @@
assertThat(mHearingAidDeviceManager.findMainDevice(mCachedDevice2)).
isEqualTo(mCachedDevice1);
}
+
+ private HearingAidInfo getLeftAshaHearingAidInfo(long hiSyncId) {
+ return new HearingAidInfo.Builder()
+ .setAshaDeviceSide(HearingAidInfo.DeviceSide.SIDE_LEFT)
+ .setHiSyncId(hiSyncId)
+ .build();
+ }
+
+ private HearingAidInfo getRightAshaHearingAidInfo(long hiSyncId) {
+ return new HearingAidInfo.Builder()
+ .setAshaDeviceSide(HearingAidInfo.DeviceSide.SIDE_RIGHT)
+ .setHiSyncId(hiSyncId)
+ .build();
+ }
}
diff --git a/packages/SystemUI/animation/Android.bp b/packages/SystemUI/animation/Android.bp
index f7bcf1f..5df79e1 100644
--- a/packages/SystemUI/animation/Android.bp
+++ b/packages/SystemUI/animation/Android.bp
@@ -36,8 +36,29 @@
static_libs: [
"PluginCoreLib",
+ "androidx.core_core-animation-nodeps",
],
manifest: "AndroidManifest.xml",
kotlincflags: ["-Xjvm-default=all"],
}
+
+android_test {
+ name: "SystemUIAnimationLibTests",
+
+ static_libs: [
+ "SystemUIAnimationLib",
+ "androidx.test.ext.junit",
+ "androidx.test.rules",
+ "testables",
+ ],
+ libs: [
+ "android.test.base",
+ ],
+ srcs: [
+ "**/*.java",
+ "**/*.kt",
+ ],
+ kotlincflags: ["-Xjvm-default=all"],
+ test_suites: ["general-tests"],
+}
diff --git a/packages/SystemUI/animation/TEST_MAPPING b/packages/SystemUI/animation/TEST_MAPPING
new file mode 100644
index 0000000..3dc8510
--- /dev/null
+++ b/packages/SystemUI/animation/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+ "presubmit": [
+ {
+ "name": "SystemUIAnimationLibTests"
+ }
+ ]
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/Interpolators.java b/packages/SystemUI/animation/src/com/android/systemui/animation/Interpolators.java
index 8063483..9dbb920 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/Interpolators.java
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/Interpolators.java
@@ -27,7 +27,10 @@
import android.view.animation.PathInterpolator;
/**
- * Utility class to receive interpolators from
+ * Utility class to receive interpolators from.
+ *
+ * Make sure that changes made to this class are also reflected in {@link InterpolatorsAndroidX}.
+ * Please consider using the androidx dependencies featuring better testability altogether.
*/
public class Interpolators {
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/InterpolatorsAndroidX.java b/packages/SystemUI/animation/src/com/android/systemui/animation/InterpolatorsAndroidX.java
new file mode 100644
index 0000000..8da87feb
--- /dev/null
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/InterpolatorsAndroidX.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.animation;
+
+import android.graphics.Path;
+import android.util.MathUtils;
+
+import androidx.core.animation.AccelerateDecelerateInterpolator;
+import androidx.core.animation.AccelerateInterpolator;
+import androidx.core.animation.BounceInterpolator;
+import androidx.core.animation.DecelerateInterpolator;
+import androidx.core.animation.Interpolator;
+import androidx.core.animation.LinearInterpolator;
+import androidx.core.animation.PathInterpolator;
+
+/**
+ * Utility class to receive interpolators from. (androidx compatible version)
+ *
+ * This is the androidx compatible version of {@link Interpolators}. Make sure that changes made to
+ * this class are also reflected in {@link Interpolators}.
+ *
+ * Using the androidx versions of {@link androidx.core.animation.ValueAnimator} or
+ * {@link androidx.core.animation.ObjectAnimator} improves animation testability. This file provides
+ * the androidx compatible versions of the interpolators defined in {@link Interpolators}.
+ * AnimatorTestRule can be used in Tests to manipulate the animation under test (e.g. artificially
+ * advancing the time).
+ */
+public class InterpolatorsAndroidX {
+
+ /*
+ * ============================================================================================
+ * Emphasized interpolators.
+ * ============================================================================================
+ */
+
+ /**
+ * The default emphasized interpolator. Used for hero / emphasized movement of content.
+ */
+ public static final Interpolator EMPHASIZED = createEmphasizedInterpolator();
+
+ /**
+ * The accelerated emphasized interpolator. Used for hero / emphasized movement of content that
+ * is disappearing e.g. when moving off screen.
+ */
+ public static final Interpolator EMPHASIZED_ACCELERATE = new PathInterpolator(
+ 0.3f, 0f, 0.8f, 0.15f);
+
+ /**
+ * The decelerating emphasized interpolator. Used for hero / emphasized movement of content that
+ * is appearing e.g. when coming from off screen
+ */
+ public static final Interpolator EMPHASIZED_DECELERATE = new PathInterpolator(
+ 0.05f, 0.7f, 0.1f, 1f);
+
+
+ /*
+ * ============================================================================================
+ * Standard interpolators.
+ * ============================================================================================
+ */
+
+ /**
+ * The standard interpolator that should be used on every normal animation
+ */
+ public static final Interpolator STANDARD = new PathInterpolator(
+ 0.2f, 0f, 0f, 1f);
+
+ /**
+ * The standard accelerating interpolator that should be used on every regular movement of
+ * content that is disappearing e.g. when moving off screen.
+ */
+ public static final Interpolator STANDARD_ACCELERATE = new PathInterpolator(
+ 0.3f, 0f, 1f, 1f);
+
+ /**
+ * The standard decelerating interpolator that should be used on every regular movement of
+ * content that is appearing e.g. when coming from off screen.
+ */
+ public static final Interpolator STANDARD_DECELERATE = new PathInterpolator(
+ 0f, 0f, 0f, 1f);
+
+ /*
+ * ============================================================================================
+ * Legacy
+ * ============================================================================================
+ */
+
+ /**
+ * The default legacy interpolator as defined in Material 1. Also known as FAST_OUT_SLOW_IN.
+ */
+ public static final Interpolator LEGACY = new PathInterpolator(0.4f, 0f, 0.2f, 1f);
+
+ /**
+ * The default legacy accelerating interpolator as defined in Material 1.
+ * Also known as FAST_OUT_LINEAR_IN.
+ */
+ public static final Interpolator LEGACY_ACCELERATE = new PathInterpolator(0.4f, 0f, 1f, 1f);
+
+ /**
+ * The default legacy decelerating interpolator as defined in Material 1.
+ * Also known as LINEAR_OUT_SLOW_IN.
+ */
+ public static final Interpolator LEGACY_DECELERATE = new PathInterpolator(0f, 0f, 0.2f, 1f);
+
+ /**
+ * Linear interpolator. Often used if the interpolator is for different properties who need
+ * different interpolations.
+ */
+ public static final Interpolator LINEAR = new LinearInterpolator();
+
+ /*
+ * ============================================================================================
+ * Custom interpolators
+ * ============================================================================================
+ */
+
+ public static final Interpolator FAST_OUT_SLOW_IN = LEGACY;
+ public static final Interpolator FAST_OUT_LINEAR_IN = LEGACY_ACCELERATE;
+ public static final Interpolator LINEAR_OUT_SLOW_IN = LEGACY_DECELERATE;
+
+ /**
+ * Like {@link #FAST_OUT_SLOW_IN}, but used in case the animation is played in reverse (i.e. t
+ * goes from 1 to 0 instead of 0 to 1).
+ */
+ public static final Interpolator FAST_OUT_SLOW_IN_REVERSE =
+ new PathInterpolator(0.8f, 0f, 0.6f, 1f);
+ public static final Interpolator SLOW_OUT_LINEAR_IN = new PathInterpolator(0.8f, 0f, 1f, 1f);
+ public static final Interpolator ALPHA_IN = new PathInterpolator(0.4f, 0f, 1f, 1f);
+ public static final Interpolator ALPHA_OUT = new PathInterpolator(0f, 0f, 0.8f, 1f);
+ public static final Interpolator ACCELERATE = new AccelerateInterpolator();
+ public static final Interpolator ACCELERATE_DECELERATE = new AccelerateDecelerateInterpolator();
+ public static final Interpolator DECELERATE_QUINT = new DecelerateInterpolator(2.5f);
+ public static final Interpolator CUSTOM_40_40 = new PathInterpolator(0.4f, 0f, 0.6f, 1f);
+ public static final Interpolator ICON_OVERSHOT = new PathInterpolator(0.4f, 0f, 0.2f, 1.4f);
+ public static final Interpolator ICON_OVERSHOT_LESS = new PathInterpolator(0.4f, 0f, 0.2f,
+ 1.1f);
+ public static final Interpolator PANEL_CLOSE_ACCELERATED = new PathInterpolator(0.3f, 0, 0.5f,
+ 1);
+ public static final Interpolator BOUNCE = new BounceInterpolator();
+ /**
+ * For state transitions on the control panel that lives in GlobalActions.
+ */
+ public static final Interpolator CONTROL_STATE = new PathInterpolator(0.4f, 0f, 0.2f,
+ 1.0f);
+
+ /**
+ * Interpolator to be used when animating a move based on a click. Pair with enough duration.
+ */
+ public static final Interpolator TOUCH_RESPONSE =
+ new PathInterpolator(0.3f, 0f, 0.1f, 1f);
+
+ /**
+ * Like {@link #TOUCH_RESPONSE}, but used in case the animation is played in reverse (i.e. t
+ * goes from 1 to 0 instead of 0 to 1).
+ */
+ public static final Interpolator TOUCH_RESPONSE_REVERSE =
+ new PathInterpolator(0.9f, 0f, 0.7f, 1f);
+
+ /*
+ * ============================================================================================
+ * Functions / Utilities
+ * ============================================================================================
+ */
+
+ /**
+ * Calculate the amount of overshoot using an exponential falloff function with desired
+ * properties, where the overshoot smoothly transitions at the 1.0f boundary into the
+ * overshoot, retaining its acceleration.
+ *
+ * @param progress a progress value going from 0 to 1
+ * @param overshootAmount the amount > 0 of overshoot desired. A value of 0.1 means the max
+ * value of the overall progress will be at 1.1.
+ * @param overshootStart the point in (0,1] where the result should reach 1
+ * @return the interpolated overshoot
+ */
+ public static float getOvershootInterpolation(float progress, float overshootAmount,
+ float overshootStart) {
+ if (overshootAmount == 0.0f || overshootStart == 0.0f) {
+ throw new IllegalArgumentException("Invalid values for overshoot");
+ }
+ float b = MathUtils.log((overshootAmount + 1) / (overshootAmount)) / overshootStart;
+ return MathUtils.max(0.0f,
+ (float) (1.0f - Math.exp(-b * progress)) * (overshootAmount + 1.0f));
+ }
+
+ /**
+ * Similar to {@link #getOvershootInterpolation(float, float, float)} but the overshoot
+ * starts immediately here, instead of first having a section of non-overshooting
+ *
+ * @param progress a progress value going from 0 to 1
+ */
+ public static float getOvershootInterpolation(float progress) {
+ return MathUtils.max(0.0f, (float) (1.0f - Math.exp(-4 * progress)));
+ }
+
+ // Create the default emphasized interpolator
+ private static PathInterpolator createEmphasizedInterpolator() {
+ Path path = new Path();
+ // Doing the same as fast_out_extra_slow_in
+ path.moveTo(0f, 0f);
+ path.cubicTo(0.05f, 0f, 0.133333f, 0.06f, 0.166666f, 0.4f);
+ path.cubicTo(0.208333f, 0.82f, 0.25f, 1f, 1f, 1f);
+ return new PathInterpolator(path);
+ }
+}
diff --git a/packages/SystemUI/animation/tests/com/android/systemui/animation/InterpolatorsAndroidXTest.kt b/packages/SystemUI/animation/tests/com/android/systemui/animation/InterpolatorsAndroidXTest.kt
new file mode 100644
index 0000000..389eed0
--- /dev/null
+++ b/packages/SystemUI/animation/tests/com/android/systemui/animation/InterpolatorsAndroidXTest.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.animation
+
+import androidx.test.filters.SmallTest
+import java.lang.reflect.Modifier
+import junit.framework.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class InterpolatorsAndroidXTest {
+
+ @Test
+ fun testInterpolatorsAndInterpolatorsAndroidXPublicMethodsAreEqual() {
+ assertEquals(
+ Interpolators::class.java.getPublicMethods(),
+ InterpolatorsAndroidX::class.java.getPublicMethods()
+ )
+ }
+
+ @Test
+ fun testInterpolatorsAndInterpolatorsAndroidXPublicFieldsAreEqual() {
+ assertEquals(
+ Interpolators::class.java.getPublicFields(),
+ InterpolatorsAndroidX::class.java.getPublicFields()
+ )
+ }
+
+ private fun <T> Class<T>.getPublicMethods() =
+ declaredMethods
+ .filter { Modifier.isPublic(it.modifiers) }
+ .map { it.toString().replace(name, "") }
+ .toSet()
+
+ private fun <T> Class<T>.getPublicFields() =
+ fields.filter { Modifier.isPublic(it.modifiers) }.map { it.name }.toSet()
+}
diff --git a/packages/SystemUI/res-keyguard/drawable/ic_flashlight_off.xml b/packages/SystemUI/res-keyguard/drawable/ic_flashlight_off.xml
new file mode 100644
index 0000000..e850d68
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/drawable/ic_flashlight_off.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="#1f1f1f"
+ android:pathData="M8,22V11L6,8V2H18V8L16,11V22ZM12,15.5Q11.375,15.5 10.938,15.062Q10.5,14.625 10.5,14Q10.5,13.375 10.938,12.938Q11.375,12.5 12,12.5Q12.625,12.5 13.062,12.938Q13.5,13.375 13.5,14Q13.5,14.625 13.062,15.062Q12.625,15.5 12,15.5ZM8,5H16V4H8ZM16,7H8V7.4L10,10.4V20H14V10.4L16,7.4ZM12,12Z"/>
+</vector>
diff --git a/packages/SystemUI/res-keyguard/drawable/ic_flashlight_on.xml b/packages/SystemUI/res-keyguard/drawable/ic_flashlight_on.xml
new file mode 100644
index 0000000..91b9ae5
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/drawable/ic_flashlight_on.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="#1f1f1f"
+ android:pathData="M6,5V2H18V5ZM12,15.5Q12.625,15.5 13.062,15.062Q13.5,14.625 13.5,14Q13.5,13.375 13.062,12.938Q12.625,12.5 12,12.5Q11.375,12.5 10.938,12.938Q10.5,13.375 10.5,14Q10.5,14.625 10.938,15.062Q11.375,15.5 12,15.5ZM8,22V11L6,8V7H18V8L16,11V22Z"/>
+</vector>
diff --git a/packages/SystemUI/res/layout/status_bar.xml b/packages/SystemUI/res/layout/status_bar.xml
index f7600e6..64aa629 100644
--- a/packages/SystemUI/res/layout/status_bar.xml
+++ b/packages/SystemUI/res/layout/status_bar.xml
@@ -55,6 +55,7 @@
android:id="@+id/status_bar_start_side_container"
android:layout_height="match_parent"
android:layout_width="0dp"
+ android:clipChildren="false"
android:layout_weight="1">
<!-- Container that is wrapped around the views on the start half of the status bar.
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt b/packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt
index 4dfcd63..66e5d7c4 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt
@@ -30,6 +30,7 @@
import android.service.controls.ControlsProviderService
import androidx.annotation.WorkerThread
import com.android.settingslib.applications.DefaultAppInfo
+import com.android.systemui.R
import java.util.Objects
class ControlsServiceInfo(
@@ -59,7 +60,8 @@
* instead of using the controls rendered by SystemUI.
*
* The activity must be in the same package, exported, enabled and protected by the
- * [Manifest.permission.BIND_CONTROLS] permission.
+ * [Manifest.permission.BIND_CONTROLS] permission. Additionally, only packages declared in
+ * [R.array.config_controlsPreferredPackages] can declare activities for use as a panel.
*/
var panelActivity: ComponentName? = null
private set
@@ -70,6 +72,9 @@
fun resolvePanelActivity() {
if (resolved) return
resolved = true
+ val validPackages = context.resources
+ .getStringArray(R.array.config_controlsPreferredPackages)
+ if (componentName.packageName !in validPackages) return
panelActivity = _panelActivity?.let {
val resolveInfos = mPm.queryIntentActivitiesAsUser(
Intent().setComponent(it),
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index e2f86bd..bbaf47d 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -366,6 +366,11 @@
@JvmField
val NEW_BACK_AFFORDANCE = unreleasedFlag(1203, "new_back_affordance", teamfood = false)
+ // TODO(b/255854141): Tracking Bug
+ @JvmField
+ val WM_ENABLE_PREDICTIVE_BACK_SYSUI =
+ unreleasedFlag(1204, "persist.wm.debug.predictive_back_sysui_enable", teamfood = false)
+
// 1300 - screenshots
// TODO(b/254512719): Tracking Bug
@JvmField val SCREENSHOT_REQUEST_PROCESSOR = releasedFlag(1300, "screenshot_request_processor")
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt
index f5220b8..73dbeab 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt
@@ -25,6 +25,7 @@
object BuiltInKeyguardQuickAffordanceKeys {
// Please keep alphabetical order of const names to simplify future maintenance.
const val CAMERA = "camera"
+ const val FLASHLIGHT = "flashlight"
const val HOME_CONTROLS = "home"
const val QR_CODE_SCANNER = "qr_code_scanner"
const val QUICK_ACCESS_WALLET = "wallet"
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt
new file mode 100644
index 0000000..49527d3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.keyguard.data.quickaffordance
+
+import android.content.Context
+import com.android.systemui.R
+import com.android.systemui.animation.Expandable
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
+import com.android.systemui.statusbar.policy.FlashlightController
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import javax.inject.Inject
+
+@SysUISingleton
+class FlashlightQuickAffordanceConfig @Inject constructor(
+ @Application private val context: Context,
+ private val flashlightController: FlashlightController,
+) : KeyguardQuickAffordanceConfig {
+
+ private sealed class FlashlightState {
+
+ abstract fun toLockScreenState(): KeyguardQuickAffordanceConfig.LockScreenState
+
+ object On: FlashlightState() {
+ override fun toLockScreenState(): KeyguardQuickAffordanceConfig.LockScreenState =
+ KeyguardQuickAffordanceConfig.LockScreenState.Visible(
+ Icon.Resource(
+ R.drawable.ic_flashlight_on,
+ ContentDescription.Resource(R.string.quick_settings_flashlight_label)
+ ),
+ ActivationState.Active
+ )
+ }
+
+ object OffAvailable: FlashlightState() {
+ override fun toLockScreenState(): KeyguardQuickAffordanceConfig.LockScreenState =
+ KeyguardQuickAffordanceConfig.LockScreenState.Visible(
+ Icon.Resource(
+ R.drawable.ic_flashlight_off,
+ ContentDescription.Resource(R.string.quick_settings_flashlight_label)
+ ),
+ ActivationState.Inactive
+ )
+ }
+
+ object Unavailable: FlashlightState() {
+ override fun toLockScreenState(): KeyguardQuickAffordanceConfig.LockScreenState =
+ KeyguardQuickAffordanceConfig.LockScreenState.Hidden
+ }
+ }
+
+ override val key: String
+ get() = BuiltInKeyguardQuickAffordanceKeys.FLASHLIGHT
+
+ override val pickerName: String
+ get() = context.getString(R.string.quick_settings_flashlight_label)
+
+ override val pickerIconResourceId: Int
+ get() = if (flashlightController.isEnabled) {
+ R.drawable.ic_flashlight_on
+ } else {
+ R.drawable.ic_flashlight_off
+ }
+
+ override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> =
+ conflatedCallbackFlow {
+ val flashlightCallback = object : FlashlightController.FlashlightListener {
+ override fun onFlashlightChanged(enabled: Boolean) {
+ trySendWithFailureLogging(
+ if (enabled) {
+ FlashlightState.On.toLockScreenState()
+ } else {
+ FlashlightState.OffAvailable.toLockScreenState()
+ },
+ TAG
+ )
+ }
+
+ override fun onFlashlightError() {
+ trySendWithFailureLogging(FlashlightState.OffAvailable.toLockScreenState(), TAG)
+ }
+
+ override fun onFlashlightAvailabilityChanged(available: Boolean) {
+ trySendWithFailureLogging(
+ if (!available) {
+ FlashlightState.Unavailable.toLockScreenState()
+ } else {
+ if (flashlightController.isEnabled) {
+ FlashlightState.On.toLockScreenState()
+ } else {
+ FlashlightState.OffAvailable.toLockScreenState()
+ }
+ },
+ TAG
+ )
+ }
+ }
+
+ flashlightController.addCallback(flashlightCallback)
+
+ awaitClose {
+ flashlightController.removeCallback(flashlightCallback)
+ }
+ }
+
+ override fun onTriggered(expandable: Expandable?):
+ KeyguardQuickAffordanceConfig.OnTriggeredResult {
+ flashlightController
+ .setFlashlight(flashlightController.isAvailable && !flashlightController.isEnabled)
+ return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
+ }
+
+ override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState =
+ if (flashlightController.isAvailable) {
+ KeyguardQuickAffordanceConfig.PickerScreenState.Default
+ } else {
+ KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice
+ }
+
+ companion object {
+ private const val TAG = "FlashlightQuickAffordanceConfig"
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt
index f7225a2..3013227c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt
@@ -26,6 +26,7 @@
@Provides
@ElementsIntoSet
fun quickAffordanceConfigs(
+ flashlight: FlashlightQuickAffordanceConfig,
home: HomeControlsKeyguardQuickAffordanceConfig,
quickAccessWallet: QuickAccessWalletKeyguardQuickAffordanceConfig,
qrCodeScanner: QrCodeScannerKeyguardQuickAffordanceConfig,
@@ -33,6 +34,7 @@
): Set<KeyguardQuickAffordanceConfig> {
return setOf(
camera,
+ flashlight,
home,
quickAccessWallet,
qrCodeScanner,
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt b/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt
index f9e341c..d6e29e0 100644
--- a/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt
@@ -16,9 +16,9 @@
package com.android.systemui.log
-import android.app.ActivityManager
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dump.DumpManager
+import com.android.systemui.log.LogBufferHelper.Companion.adjustMaxSize
import com.android.systemui.plugins.log.LogBuffer
import com.android.systemui.plugins.log.LogcatEchoTracker
@@ -29,15 +29,6 @@
private val dumpManager: DumpManager,
private val logcatEchoTracker: LogcatEchoTracker
) {
- /* limitiometricMessageDeferralLogger the size of maxPoolSize for low ram (Go) devices */
- private fun adjustMaxSize(requestedMaxSize: Int): Int {
- return if (ActivityManager.isLowRamDeviceStatic()) {
- minOf(requestedMaxSize, 20) /* low ram max log size*/
- } else {
- requestedMaxSize
- }
- }
-
@JvmOverloads
fun create(
name: String,
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogBufferHelper.kt b/packages/SystemUI/src/com/android/systemui/log/LogBufferHelper.kt
new file mode 100644
index 0000000..619eac1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/LogBufferHelper.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.log
+
+import android.app.ActivityManager
+
+class LogBufferHelper {
+ companion object {
+ /** If necessary, returns a limited maximum size for low ram (Go) devices */
+ fun adjustMaxSize(requestedMaxSize: Int): Int {
+ return if (ActivityManager.isLowRamDeviceStatic()) {
+ minOf(requestedMaxSize, 20) /* low ram max log size*/
+ } else {
+ requestedMaxSize
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/log/table/Diffable.kt b/packages/SystemUI/src/com/android/systemui/log/table/Diffable.kt
new file mode 100644
index 0000000..c27bfa3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/table/Diffable.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.log.table
+
+import com.android.systemui.util.kotlin.pairwiseBy
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * An interface that enables logging the difference between values in table format.
+ *
+ * Many objects that we want to log are data-y objects with a collection of fields. When logging
+ * these objects, we want to log each field separately. This allows ABT (Android Bug Tool) to easily
+ * highlight changes in individual fields.
+ *
+ * See [TableLogBuffer].
+ */
+interface Diffable<T> {
+ /**
+ * Finds the differences between [prevVal] and [this] and logs those diffs to [row].
+ *
+ * Each implementer should determine which individual fields have changed between [prevVal] and
+ * [this], and only log the fields that have actually changed. This helps save buffer space.
+ *
+ * For example, if:
+ * - prevVal = Object(val1=100, val2=200, val3=300)
+ * - this = Object(val1=100, val2=200, val3=333)
+ *
+ * Then only the val3 change should be logged.
+ */
+ fun logDiffs(prevVal: T, row: TableRowLogger)
+}
+
+/**
+ * Each time the flow is updated with a new value, logs the differences between the previous value
+ * and the new value to the given [tableLogBuffer].
+ *
+ * The new value's [Diffable.logDiffs] method will be used to log the differences to the table.
+ *
+ * @param columnPrefix a prefix that will be applied to every column name that gets logged.
+ */
+fun <T : Diffable<T>> Flow<T>.logDiffsForTable(
+ tableLogBuffer: TableLogBuffer,
+ columnPrefix: String,
+ initialValue: T,
+): Flow<T> {
+ return this.pairwiseBy(initialValue) { prevVal, newVal ->
+ tableLogBuffer.logDiffs(columnPrefix, prevVal, newVal)
+ newVal
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/log/table/TableChange.kt b/packages/SystemUI/src/com/android/systemui/log/table/TableChange.kt
new file mode 100644
index 0000000..68c297f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/table/TableChange.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.log.table
+
+/**
+ * A object used with [TableLogBuffer] to store changes in variables over time. Is recyclable.
+ *
+ * Each message represents a change to exactly 1 type, specified by [DataType].
+ */
+data class TableChange(
+ var timestamp: Long = 0,
+ var columnPrefix: String = "",
+ var columnName: String = "",
+ var type: DataType = DataType.EMPTY,
+ var bool: Boolean = false,
+ var int: Int = 0,
+ var str: String? = null,
+) {
+ /** Resets to default values so that the object can be recycled. */
+ fun reset(timestamp: Long, columnPrefix: String, columnName: String) {
+ this.timestamp = timestamp
+ this.columnPrefix = columnPrefix
+ this.columnName = columnName
+ this.type = DataType.EMPTY
+ this.bool = false
+ this.int = 0
+ this.str = null
+ }
+
+ /** Sets this to store a string change. */
+ fun set(value: String?) {
+ type = DataType.STRING
+ str = value
+ }
+
+ /** Sets this to store a boolean change. */
+ fun set(value: Boolean) {
+ type = DataType.BOOLEAN
+ bool = value
+ }
+
+ /** Sets this to store an int change. */
+ fun set(value: Int) {
+ type = DataType.INT
+ int = value
+ }
+
+ /** Returns true if this object has a change. */
+ fun hasData(): Boolean {
+ return columnName.isNotBlank() && type != DataType.EMPTY
+ }
+
+ fun getName(): String {
+ return if (columnPrefix.isNotBlank()) {
+ "$columnPrefix.$columnName"
+ } else {
+ columnName
+ }
+ }
+
+ fun getVal(): String {
+ return when (type) {
+ DataType.EMPTY -> null
+ DataType.STRING -> str
+ DataType.INT -> int
+ DataType.BOOLEAN -> bool
+ }.toString()
+ }
+
+ enum class DataType {
+ STRING,
+ BOOLEAN,
+ INT,
+ EMPTY,
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/log/table/TableLogBuffer.kt b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBuffer.kt
new file mode 100644
index 0000000..429637a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBuffer.kt
@@ -0,0 +1,264 @@
+/*
+ * 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.log.table
+
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.Dumpable
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.dump.DumpsysTableLogger
+import com.android.systemui.plugins.util.RingBuffer
+import com.android.systemui.util.time.SystemClock
+import java.io.PrintWriter
+import java.text.SimpleDateFormat
+import java.util.Locale
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * A logger that logs changes in table format.
+ *
+ * Some parts of System UI maintain a lot of pieces of state at once.
+ * [com.android.systemui.plugins.log.LogBuffer] allows us to easily log change events:
+ *
+ * - 10-10 10:10:10.456: state2 updated to newVal2
+ * - 10-10 10:11:00.000: stateN updated to StateN(val1=true, val2=1)
+ * - 10-10 10:11:02.123: stateN updated to StateN(val1=true, val2=2)
+ * - 10-10 10:11:05.123: state1 updated to newVal1
+ * - 10-10 10:11:06.000: stateN updated to StateN(val1=false, val2=3)
+ *
+ * However, it can sometimes be more useful to view the state changes in table format:
+ *
+ * - timestamp--------- | state1- | state2- | ... | stateN.val1 | stateN.val2
+ * - -------------------------------------------------------------------------
+ * - 10-10 10:10:10.123 | val1--- | val2--- | ... | false------ | 0-----------
+ * - 10-10 10:10:10.456 | val1--- | newVal2 | ... | false------ | 0-----------
+ * - 10-10 10:11:00.000 | val1--- | newVal2 | ... | true------- | 1-----------
+ * - 10-10 10:11:02.123 | val1--- | newVal2 | ... | true------- | 2-----------
+ * - 10-10 10:11:05.123 | newVal1 | newVal2 | ... | true------- | 2-----------
+ * - 10-10 10:11:06.000 | newVal1 | newVal2 | ... | false------ | 3-----------
+ *
+ * This class enables easy logging of the state changes in both change event format and table
+ * format.
+ *
+ * This class also enables easy logging of states that are a collection of fields. For example,
+ * stateN in the above example consists of two fields -- val1 and val2. It's useful to put each
+ * field into its own column so that ABT (Android Bug Tool) can easily highlight changes to
+ * individual fields.
+ *
+ * How it works:
+ *
+ * 1) Create an instance of this buffer via [TableLogBufferFactory].
+ *
+ * 2) For any states being logged, implement [Diffable]. Implementing [Diffable] allows the state to
+ * only log the fields that have *changed* since the previous update, instead of always logging all
+ * fields.
+ *
+ * 3) Each time a change in a state happens, call [logDiffs]. If your state is emitted using a
+ * [Flow], you should use the [logDiffsForTable] extension function to automatically log diffs any
+ * time your flow emits a new value.
+ *
+ * When a dump occurs, there will be two dumps:
+ *
+ * 1) The change events under the dumpable name "$name-changes".
+ *
+ * 2) This class will coalesce all the diffs into a table format and log them under the dumpable
+ * name "$name-table".
+ *
+ * @param maxSize the maximum size of the buffer. Must be > 0.
+ */
+class TableLogBuffer(
+ maxSize: Int,
+ private val name: String,
+ private val systemClock: SystemClock,
+) {
+ init {
+ if (maxSize <= 0) {
+ throw IllegalArgumentException("maxSize must be > 0")
+ }
+ }
+
+ private val buffer = RingBuffer(maxSize) { TableChange() }
+
+ // A [TableRowLogger] object, re-used each time [logDiffs] is called.
+ // (Re-used to avoid object allocation.)
+ private val tempRow = TableRowLoggerImpl(0, columnPrefix = "", this)
+
+ /**
+ * Log the differences between [prevVal] and [newVal].
+ *
+ * The [newVal] object's method [Diffable.logDiffs] will be used to fetch the diffs.
+ *
+ * @param columnPrefix a prefix that will be applied to every column name that gets logged. This
+ * ensures that all the columns related to the same state object will be grouped together in the
+ * table.
+ */
+ @Synchronized
+ fun <T : Diffable<T>> logDiffs(columnPrefix: String, prevVal: T, newVal: T) {
+ val row = tempRow
+ row.timestamp = systemClock.currentTimeMillis()
+ row.columnPrefix = columnPrefix
+ newVal.logDiffs(prevVal, row)
+ }
+
+ // Keep these individual [logChange] methods private (don't let clients give us their own
+ // timestamps.)
+
+ private fun logChange(timestamp: Long, prefix: String, columnName: String, value: String?) {
+ val change = obtain(timestamp, prefix, columnName)
+ change.set(value)
+ }
+
+ private fun logChange(timestamp: Long, prefix: String, columnName: String, value: Boolean) {
+ val change = obtain(timestamp, prefix, columnName)
+ change.set(value)
+ }
+
+ private fun logChange(timestamp: Long, prefix: String, columnName: String, value: Int) {
+ val change = obtain(timestamp, prefix, columnName)
+ change.set(value)
+ }
+
+ // TODO(b/259454430): Add additional change types here.
+
+ @Synchronized
+ private fun obtain(timestamp: Long, prefix: String, columnName: String): TableChange {
+ val tableChange = buffer.advance()
+ tableChange.reset(timestamp, prefix, columnName)
+ return tableChange
+ }
+
+ /**
+ * Registers this buffer as dumpables in [dumpManager]. Must be called for the table to be
+ * dumped.
+ *
+ * This will be automatically called in [TableLogBufferFactory.create].
+ */
+ fun registerDumpables(dumpManager: DumpManager) {
+ dumpManager.registerNormalDumpable("$name-changes", changeDumpable)
+ dumpManager.registerNormalDumpable("$name-table", tableDumpable)
+ }
+
+ private val changeDumpable = Dumpable { pw, _ -> dumpChanges(pw) }
+ private val tableDumpable = Dumpable { pw, _ -> dumpTable(pw) }
+
+ /** Dumps the list of [TableChange] objects. */
+ @Synchronized
+ @VisibleForTesting
+ fun dumpChanges(pw: PrintWriter) {
+ for (i in 0 until buffer.size) {
+ buffer[i].dump(pw)
+ }
+ }
+
+ /** Dumps an individual [TableChange]. */
+ private fun TableChange.dump(pw: PrintWriter) {
+ if (!this.hasData()) {
+ return
+ }
+ val formattedTimestamp = TABLE_LOG_DATE_FORMAT.format(timestamp)
+ pw.print(formattedTimestamp)
+ pw.print(" ")
+ pw.print(this.getName())
+ pw.print("=")
+ pw.print(this.getVal())
+ pw.println()
+ }
+
+ /**
+ * Coalesces all the [TableChange] objects into a table of values of time and dumps the table.
+ */
+ // TODO(b/259454430): Since this is an expensive process, it could cause the bug report dump to
+ // fail and/or not dump anything else. We should move this processing to ABT (Android Bug
+ // Tool), where we have unlimited time to process.
+ @Synchronized
+ @VisibleForTesting
+ fun dumpTable(pw: PrintWriter) {
+ val messages = buffer.iterator().asSequence().toList()
+
+ if (messages.isEmpty()) {
+ return
+ }
+
+ // Step 1: Create list of column headers
+ val headerSet = mutableSetOf<String>()
+ messages.forEach { headerSet.add(it.getName()) }
+ val headers: MutableList<String> = headerSet.toList().sorted().toMutableList()
+ headers.add(0, "timestamp")
+
+ // Step 2: Create a list with the current values for each column. Will be updated with each
+ // change.
+ val currentRow: MutableList<String> = MutableList(headers.size) { DEFAULT_COLUMN_VALUE }
+
+ // Step 3: For each message, make the correct update to [currentRow] and save it to [rows].
+ val columnIndices: Map<String, Int> =
+ headers.mapIndexed { index, headerName -> headerName to index }.toMap()
+ val allRows = mutableListOf<List<String>>()
+
+ messages.forEach {
+ if (!it.hasData()) {
+ return@forEach
+ }
+
+ val formattedTimestamp = TABLE_LOG_DATE_FORMAT.format(it.timestamp)
+ if (formattedTimestamp != currentRow[0]) {
+ // The timestamp has updated, so save the previous row and continue to the next row
+ allRows.add(currentRow.toList())
+ currentRow[0] = formattedTimestamp
+ }
+ val columnIndex = columnIndices[it.getName()]!!
+ currentRow[columnIndex] = it.getVal()
+ }
+ // Add the last row
+ allRows.add(currentRow.toList())
+
+ // Step 4: Dump the rows
+ DumpsysTableLogger(
+ name,
+ headers,
+ allRows,
+ )
+ .printTableData(pw)
+ }
+
+ /**
+ * A private implementation of [TableRowLogger].
+ *
+ * Used so that external clients can't modify [timestamp].
+ */
+ private class TableRowLoggerImpl(
+ var timestamp: Long,
+ var columnPrefix: String,
+ val tableLogBuffer: TableLogBuffer,
+ ) : TableRowLogger {
+ /** Logs a change to a string value. */
+ override fun logChange(columnName: String, value: String?) {
+ tableLogBuffer.logChange(timestamp, columnPrefix, columnName, value)
+ }
+
+ /** Logs a change to a boolean value. */
+ override fun logChange(columnName: String, value: Boolean) {
+ tableLogBuffer.logChange(timestamp, columnPrefix, columnName, value)
+ }
+
+ /** Logs a change to an int value. */
+ override fun logChange(columnName: String, value: Int) {
+ tableLogBuffer.logChange(timestamp, columnPrefix, columnName, value)
+ }
+ }
+}
+
+val TABLE_LOG_DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US)
+private const val DEFAULT_COLUMN_VALUE = "UNKNOWN"
diff --git a/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt
new file mode 100644
index 0000000..f1f906f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.log.table
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.log.LogBufferHelper.Companion.adjustMaxSize
+import com.android.systemui.util.time.SystemClock
+import javax.inject.Inject
+
+@SysUISingleton
+class TableLogBufferFactory
+@Inject
+constructor(
+ private val dumpManager: DumpManager,
+ private val systemClock: SystemClock,
+) {
+ fun create(
+ name: String,
+ maxSize: Int,
+ ): TableLogBuffer {
+ val tableBuffer = TableLogBuffer(adjustMaxSize(maxSize), name, systemClock)
+ tableBuffer.registerDumpables(dumpManager)
+ return tableBuffer
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/log/table/TableRowLogger.kt b/packages/SystemUI/src/com/android/systemui/log/table/TableRowLogger.kt
new file mode 100644
index 0000000..a7ba13b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/table/TableRowLogger.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.log.table
+
+/**
+ * A class that logs a row to [TableLogBuffer].
+ *
+ * Objects that implement [Diffable] will receive an instance of this class, and can log any changes
+ * to individual fields using the [logChange] methods. All logged changes will be associated with
+ * the same timestamp.
+ */
+interface TableRowLogger {
+ /** Logs a change to a string value. */
+ fun logChange(columnName: String, value: String?)
+
+ /** Logs a change to a boolean value. */
+ fun logChange(columnName: String, value: Boolean)
+
+ /** Logs a change to an int value. */
+ fun logChange(columnName: String, value: Int)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt
index cbb670e..b252be1 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt
@@ -799,6 +799,16 @@
}
if (
+ desiredLocation == LOCATION_QS &&
+ previousLocation == LOCATION_LOCKSCREEN &&
+ statusbarState == StatusBarState.SHADE
+ ) {
+ // This is an invalid transition, can happen when tapping on home control and the UMO
+ // while being on landscape orientation in tablet.
+ return false
+ }
+
+ if (
statusbarState == StatusBarState.KEYGUARD &&
(currentLocation == LOCATION_LOCKSCREEN || previousLocation == LOCATION_LOCKSCREEN)
) {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt
index 31e4464..5e47d6d 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt
@@ -19,6 +19,7 @@
import android.annotation.IdRes
import android.app.StatusBarManager
import android.content.res.Configuration
+import android.os.Bundle
import android.os.Trace
import android.os.Trace.TRACE_TAG_APP
import android.util.Pair
@@ -34,6 +35,8 @@
import com.android.systemui.animation.ShadeInterpolation
import com.android.systemui.battery.BatteryMeterView
import com.android.systemui.battery.BatteryMeterViewController
+import com.android.systemui.demomode.DemoMode
+import com.android.systemui.demomode.DemoModeController
import com.android.systemui.dump.DumpManager
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
@@ -53,6 +56,7 @@
import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent.CentralSurfacesScope
import com.android.systemui.statusbar.phone.dagger.StatusBarViewModule.LARGE_SCREEN_BATTERY_CONTROLLER
import com.android.systemui.statusbar.phone.dagger.StatusBarViewModule.LARGE_SCREEN_SHADE_HEADER
+import com.android.systemui.statusbar.policy.Clock
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.statusbar.policy.VariableDateView
import com.android.systemui.statusbar.policy.VariableDateViewController
@@ -89,7 +93,8 @@
private val dumpManager: DumpManager,
private val featureFlags: FeatureFlags,
private val qsCarrierGroupControllerBuilder: QSCarrierGroupController.Builder,
- private val combinedShadeHeadersConstraintManager: CombinedShadeHeadersConstraintManager
+ private val combinedShadeHeadersConstraintManager: CombinedShadeHeadersConstraintManager,
+ private val demoModeController: DemoModeController
) : ViewController<View>(header), Dumpable {
companion object {
@@ -126,7 +131,7 @@
private lateinit var qsCarrierGroupController: QSCarrierGroupController
private val batteryIcon: BatteryMeterView = header.findViewById(R.id.batteryRemainingIcon)
- private val clock: TextView = header.findViewById(R.id.clock)
+ private val clock: Clock = header.findViewById(R.id.clock)
private val date: TextView = header.findViewById(R.id.date)
private val iconContainer: StatusIconContainer = header.findViewById(R.id.statusIcons)
private val qsCarrierGroup: QSCarrierGroup = header.findViewById(R.id.carrier_group)
@@ -212,6 +217,14 @@
view.onApplyWindowInsets(insets)
}
+ private val demoModeReceiver = object : DemoMode {
+ override fun demoCommands() = listOf(DemoMode.COMMAND_CLOCK)
+ override fun dispatchDemoCommand(command: String, args: Bundle) =
+ clock.dispatchDemoCommand(command, args)
+ override fun onDemoModeStarted() = clock.onDemoModeStarted()
+ override fun onDemoModeFinished() = clock.onDemoModeFinished()
+ }
+
private val chipVisibilityListener: ChipVisibilityListener = object : ChipVisibilityListener {
override fun onChipVisibilityRefreshed(visible: Boolean) {
if (header is MotionLayout) {
@@ -300,6 +313,7 @@
dumpManager.registerDumpable(this)
configurationController.addCallback(configurationControllerListener)
+ demoModeController.addCallback(demoModeReceiver)
updateVisibility()
updateTransition()
@@ -309,6 +323,7 @@
privacyIconsController.chipVisibilityListener = null
dumpManager.unregisterDumpable(this::class.java.simpleName)
configurationController.removeCallback(configurationControllerListener)
+ demoModeController.removeCallback(demoModeReceiver)
}
fun disable(state1: Int, state2: Int, animate: Boolean) {
@@ -521,4 +536,7 @@
updateConstraints(LARGE_SCREEN_HEADER_CONSTRAINT, updates.largeScreenConstraintsChanges)
}
}
+
+ @VisibleForTesting
+ internal fun simulateViewDetached() = this.onViewDetached()
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 7fbdeca..a3d9965 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -2072,6 +2072,13 @@
mInitialTouchX = x;
initVelocityTracker();
trackMovement(event);
+ float qsExpansionFraction = computeQsExpansionFraction();
+ // Intercept the touch if QS is between fully collapsed and fully expanded state
+ if (qsExpansionFraction > 0.0 && qsExpansionFraction < 1.0) {
+ mShadeLog.logMotionEvent(event,
+ "onQsIntercept: down action, QS partially expanded/collapsed");
+ return true;
+ }
if (mKeyguardShowing
&& shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, 0)) {
// Dragging down on the lockscreen statusbar should prohibit other interactions
@@ -2324,6 +2331,13 @@
if (!isFullyCollapsed()) {
handleQsDown(event);
}
+ // defer touches on QQS to shade while shade is collapsing. Added margin for error
+ // as sometimes the qsExpansionFraction can be a tiny value instead of 0 when in QQS.
+ if (computeQsExpansionFraction() <= 0.01 && getExpandedFraction() < 1.0) {
+ mShadeLog.logMotionEvent(event,
+ "handleQsTouch: QQS touched while shade collapsing");
+ mQsTracking = false;
+ }
if (!mQsExpandImmediate && mQsTracking) {
onQsTouch(event);
if (!mConflictingQsExpansionGesture && !mSplitShadeEnabled) {
@@ -2564,7 +2578,6 @@
// Reset scroll position and apply that position to the expanded height.
float height = mQsExpansionHeight;
setQsExpansionHeight(height);
- updateExpandedHeightToMaxHeight();
mNotificationStackScrollLayoutController.checkSnoozeLeavebehind();
// When expanding QS, let's authenticate the user if possible,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
index be08183..01ca667 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
@@ -122,6 +122,7 @@
ActivityOptions options = getDefaultActivityOptions(animationAdapter);
options.setLaunchDisplayId(displayId);
options.setCallerDisplayId(displayId);
+ options.setPendingIntentBackgroundActivityLaunchAllowed(true);
return options.toBundle();
}
@@ -145,6 +146,7 @@
: ActivityOptions.SourceInfo.TYPE_NOTIFICATION, eventTime);
options.setLaunchDisplayId(displayId);
options.setCallerDisplayId(displayId);
+ options.setPendingIntentBackgroundActivityLaunchAllowed(true);
return options.toBundle();
}
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 5efd460..32ea8d6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -977,6 +977,11 @@
// Lastly, call to the icon policy to install/update all the icons.
mIconPolicy.init();
+ // Based on teamfood flag, turn predictive back dispatch on at runtime.
+ if (mFeatureFlags.isEnabled(Flags.WM_ENABLE_PREDICTIVE_BACK_SYSUI)) {
+ mContext.getApplicationInfo().setEnableOnBackInvokedCallback(true);
+ }
+
mKeyguardStateController.addCallback(new KeyguardStateController.Callback() {
@Override
public void onUnlockedChanged() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
index fcd1b8a..0662fb3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
@@ -16,6 +16,9 @@
package com.android.systemui.statusbar.pipeline.dagger
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.TableLogBufferFactory
import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository
import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepositoryImpl
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
@@ -32,6 +35,7 @@
import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl
import dagger.Binds
import dagger.Module
+import dagger.Provides
@Module
abstract class StatusBarPipelineModule {
@@ -57,4 +61,15 @@
@Binds
abstract fun mobileIconsInteractor(impl: MobileIconsInteractorImpl): MobileIconsInteractor
+
+ @Module
+ companion object {
+ @JvmStatic
+ @Provides
+ @SysUISingleton
+ @WifiTableLog
+ fun provideWifiTableLogBuffer(factory: TableLogBufferFactory): TableLogBuffer {
+ return factory.create("WifiTableLog", 100)
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/WifiTableLog.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/WifiTableLog.kt
new file mode 100644
index 0000000..ac395a9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/WifiTableLog.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.dagger
+
+import javax.inject.Qualifier
+
+/** Wifi logs in table format. */
+@Qualifier
+@MustBeDocumented
+@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
+annotation class WifiTableLog
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt
index 062c3d1..8436b13 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt
@@ -17,12 +17,30 @@
package com.android.systemui.statusbar.pipeline.wifi.data.model
import androidx.annotation.VisibleForTesting
+import com.android.systemui.log.table.TableRowLogger
+import com.android.systemui.log.table.Diffable
/** Provides information about the current wifi network. */
-sealed class WifiNetworkModel {
+sealed class WifiNetworkModel : Diffable<WifiNetworkModel> {
+
/** A model representing that we have no active wifi network. */
object Inactive : WifiNetworkModel() {
override fun toString() = "WifiNetwork.Inactive"
+
+ override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) {
+ if (prevVal is Inactive) {
+ return
+ }
+ row.logChange(COL_NETWORK_TYPE, TYPE_INACTIVE)
+
+ if (prevVal is CarrierMerged) {
+ // The only difference between CarrierMerged and Inactive is the type
+ return
+ }
+
+ // When changing from Active to Inactive, we need to log diffs to all the fields.
+ logDiffsFromActiveToNotActive(prevVal as Active, row)
+ }
}
/**
@@ -33,6 +51,21 @@
*/
object CarrierMerged : WifiNetworkModel() {
override fun toString() = "WifiNetwork.CarrierMerged"
+
+ override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) {
+ if (prevVal is CarrierMerged) {
+ return
+ }
+ row.logChange(COL_NETWORK_TYPE, TYPE_CARRIER_MERGED)
+
+ if (prevVal is Inactive) {
+ // The only difference between CarrierMerged and Inactive is the type.
+ return
+ }
+
+ // When changing from Active to CarrierMerged, we need to log diffs to all the fields.
+ logDiffsFromActiveToNotActive(prevVal as Active, row)
+ }
}
/** Provides information about an active wifi network. */
@@ -76,6 +109,41 @@
}
}
+ override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) {
+ if (prevVal !is Active) {
+ row.logChange(COL_NETWORK_TYPE, TYPE_ACTIVE)
+ }
+
+ if (prevVal !is Active || prevVal.networkId != networkId) {
+ row.logChange(COL_NETWORK_ID, networkId)
+ }
+ if (prevVal !is Active || prevVal.isValidated != isValidated) {
+ row.logChange(COL_VALIDATED, isValidated)
+ }
+ if (prevVal !is Active || prevVal.level != level) {
+ row.logChange(COL_LEVEL, level ?: LEVEL_DEFAULT)
+ }
+ if (prevVal !is Active || prevVal.ssid != ssid) {
+ row.logChange(COL_SSID, ssid)
+ }
+
+ // TODO(b/238425913): The passpoint-related values are frequently never used, so it
+ // would be great to not log them when they're not used.
+ if (prevVal !is Active || prevVal.isPasspointAccessPoint != isPasspointAccessPoint) {
+ row.logChange(COL_PASSPOINT_ACCESS_POINT, isPasspointAccessPoint)
+ }
+ if (prevVal !is Active ||
+ prevVal.isOnlineSignUpForPasspointAccessPoint !=
+ isOnlineSignUpForPasspointAccessPoint) {
+ row.logChange(COL_ONLINE_SIGN_UP, isOnlineSignUpForPasspointAccessPoint)
+ }
+ if (prevVal !is Active ||
+ prevVal.passpointProviderFriendlyName != passpointProviderFriendlyName) {
+ row.logChange(COL_PASSPOINT_NAME, passpointProviderFriendlyName)
+ }
+ }
+
+
override fun toString(): String {
// Only include the passpoint-related values in the string if we have them. (Most
// networks won't have them so they'll be mostly clutter.)
@@ -101,4 +169,37 @@
internal const val MAX_VALID_LEVEL = 4
}
}
+
+ internal fun logDiffsFromActiveToNotActive(prevActive: Active, row: TableRowLogger) {
+ row.logChange(COL_NETWORK_ID, NETWORK_ID_DEFAULT)
+ row.logChange(COL_VALIDATED, false)
+ row.logChange(COL_LEVEL, LEVEL_DEFAULT)
+ row.logChange(COL_SSID, null)
+
+ if (prevActive.isPasspointAccessPoint) {
+ row.logChange(COL_PASSPOINT_ACCESS_POINT, false)
+ }
+ if (prevActive.isOnlineSignUpForPasspointAccessPoint) {
+ row.logChange(COL_ONLINE_SIGN_UP, false)
+ }
+ if (prevActive.passpointProviderFriendlyName != null) {
+ row.logChange(COL_PASSPOINT_NAME, null)
+ }
+ }
}
+
+const val TYPE_CARRIER_MERGED = "CarrierMerged"
+const val TYPE_INACTIVE = "Inactive"
+const val TYPE_ACTIVE = "Active"
+
+const val COL_NETWORK_TYPE = "type"
+const val COL_NETWORK_ID = "networkId"
+const val COL_VALIDATED = "isValidated"
+const val COL_LEVEL = "level"
+const val COL_SSID = "ssid"
+const val COL_PASSPOINT_ACCESS_POINT = "isPasspointAccessPoint"
+const val COL_ONLINE_SIGN_UP = "isOnlineSignUpForPasspointAccessPoint"
+const val COL_PASSPOINT_NAME = "passpointProviderFriendlyName"
+
+const val LEVEL_DEFAULT = -1
+const val NETWORK_ID_DEFAULT = -1
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
index 93448c1d..a663536 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
@@ -36,6 +36,9 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.logDiffsForTable
+import com.android.systemui.statusbar.pipeline.dagger.WifiTableLog
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.SB_LOGGING_TAG
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange
@@ -82,6 +85,7 @@
broadcastDispatcher: BroadcastDispatcher,
connectivityManager: ConnectivityManager,
logger: ConnectivityPipelineLogger,
+ @WifiTableLog wifiTableLogBuffer: TableLogBuffer,
@Main mainExecutor: Executor,
@Application scope: CoroutineScope,
wifiManager: WifiManager?,
@@ -199,6 +203,12 @@
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
}
+ .distinctUntilChanged()
+ .logDiffsForTable(
+ wifiTableLogBuffer,
+ columnPrefix = "wifiNetwork",
+ initialValue = WIFI_NETWORK_DEFAULT,
+ )
// There will be multiple wifi icons in different places that will frequently
// subscribe/unsubscribe to flows as the views attach/detach. Using [stateIn] ensures that
// new subscribes will get the latest value immediately upon subscription. Otherwise, the
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsListingControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsListingControllerImplTest.kt
index 98ff8d1..c677f19 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsListingControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsListingControllerImplTest.kt
@@ -31,6 +31,7 @@
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.settingslib.applications.ServiceListing
+import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.controls.ControlsServiceInfo
import com.android.systemui.dump.DumpManager
@@ -110,6 +111,12 @@
.thenReturn(PackageManager.COMPONENT_ENABLED_STATE_DISABLED)
mContext.setMockPackageManager(packageManager)
+ mContext.orCreateTestableResources
+ .addOverride(
+ R.array.config_controlsPreferredPackages,
+ arrayOf(componentName.packageName)
+ )
+
// Return true by default, we'll test the false path
`when`(featureFlags.isEnabled(USE_APP_PANELS)).thenReturn(true)
@@ -482,6 +489,35 @@
}
@Test
+ fun testPackageNotPreferred_nullPanel() {
+ mContext.orCreateTestableResources
+ .addOverride(R.array.config_controlsPreferredPackages, arrayOf<String>())
+
+ val serviceInfo = ServiceInfo(
+ componentName,
+ activityName
+ )
+
+ `when`(packageManager.getComponentEnabledSetting(eq(activityName)))
+ .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_ENABLED)
+
+ setUpQueryResult(listOf(
+ ActivityInfo(
+ activityName,
+ exported = true,
+ permission = Manifest.permission.BIND_CONTROLS
+ )
+ ))
+
+ val list = listOf(serviceInfo)
+ serviceListingCallbackCaptor.value.onServicesReloaded(list)
+
+ executor.runAllReady()
+
+ assertNull(controller.getCurrentServices()[0].panelActivity)
+ }
+
+ @Test
fun testListingsNotModifiedByCallback() {
// This test checks that if the list passed to the callback is modified, it has no effect
// in the resulting services
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfigTest.kt
new file mode 100644
index 0000000..cda7018
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfigTest.kt
@@ -0,0 +1,193 @@
+/*
+ * 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.data.quickaffordance
+
+import android.content.Context
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
+import com.android.systemui.statusbar.policy.FlashlightController
+import com.android.systemui.utils.leaks.FakeFlashlightController
+import com.android.systemui.utils.leaks.LeakCheckedTest
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+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.MockitoAnnotations
+
+@SmallTest
+@RunWith(JUnit4::class)
+class FlashlightQuickAffordanceConfigTest : LeakCheckedTest() {
+
+ @Mock private lateinit var context: Context
+ private lateinit var flashlightController: FakeFlashlightController
+ private lateinit var underTest : FlashlightQuickAffordanceConfig
+
+ @Before
+ fun setUp() {
+ injectLeakCheckedDependency(FlashlightController::class.java)
+ MockitoAnnotations.initMocks(this)
+
+ flashlightController = SysuiLeakCheck().getLeakChecker(FlashlightController::class.java) as FakeFlashlightController
+ underTest = FlashlightQuickAffordanceConfig(context, flashlightController)
+ }
+
+ @Test
+ fun `flashlight is off -- triggered -- icon is on and active`() = runTest {
+ //given
+ flashlightController.isEnabled = false
+ flashlightController.isAvailable = true
+ val values = mutableListOf<KeyguardQuickAffordanceConfig.LockScreenState>()
+ val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values)}
+
+ //when
+ underTest.onTriggered(null)
+ val lastValue = values.last()
+
+ //then
+ assertTrue(lastValue is KeyguardQuickAffordanceConfig.LockScreenState.Visible)
+ assertEquals(R.drawable.ic_flashlight_on,
+ ((lastValue as KeyguardQuickAffordanceConfig.LockScreenState.Visible).icon as? Icon.Resource)?.res)
+ job.cancel()
+ }
+
+ @Test
+ fun `flashlight is on -- triggered -- icon is off and inactive`() = runTest {
+ //given
+ flashlightController.isEnabled = true
+ flashlightController.isAvailable = true
+ val values = mutableListOf<KeyguardQuickAffordanceConfig.LockScreenState>()
+ val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values)}
+
+ //when
+ underTest.onTriggered(null)
+ val lastValue = values.last()
+
+ //then
+ assertTrue(lastValue is KeyguardQuickAffordanceConfig.LockScreenState.Visible)
+ assertEquals(R.drawable.ic_flashlight_off,
+ ((lastValue as KeyguardQuickAffordanceConfig.LockScreenState.Visible).icon as? Icon.Resource)?.res)
+ job.cancel()
+ }
+
+ @Test
+ fun `flashlight is on -- receives error -- icon is off and inactive`() = runTest {
+ //given
+ flashlightController.isEnabled = true
+ flashlightController.isAvailable = false
+ val values = mutableListOf<KeyguardQuickAffordanceConfig.LockScreenState>()
+ val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values)}
+
+ //when
+ flashlightController.onFlashlightError()
+ val lastValue = values.last()
+
+ //then
+ assertTrue(lastValue is KeyguardQuickAffordanceConfig.LockScreenState.Visible)
+ assertEquals(R.drawable.ic_flashlight_off,
+ ((lastValue as KeyguardQuickAffordanceConfig.LockScreenState.Visible).icon as? Icon.Resource)?.res)
+ job.cancel()
+ }
+
+ @Test
+ fun `flashlight availability now off -- hidden`() = runTest {
+ //given
+ flashlightController.isEnabled = true
+ flashlightController.isAvailable = false
+ val values = mutableListOf<KeyguardQuickAffordanceConfig.LockScreenState>()
+ val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values)}
+
+ //when
+ flashlightController.onFlashlightAvailabilityChanged(false)
+ val lastValue = values.last()
+
+ //then
+ assertTrue(lastValue is KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
+ job.cancel()
+ }
+
+ @Test
+ fun `flashlight availability now on -- flashlight on -- inactive and icon off`() = runTest {
+ //given
+ flashlightController.isEnabled = true
+ flashlightController.isAvailable = false
+ val values = mutableListOf<KeyguardQuickAffordanceConfig.LockScreenState>()
+ val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values)}
+
+ //when
+ flashlightController.onFlashlightAvailabilityChanged(true)
+ val lastValue = values.last()
+
+ //then
+ assertTrue(lastValue is KeyguardQuickAffordanceConfig.LockScreenState.Visible)
+ assertTrue((lastValue as KeyguardQuickAffordanceConfig.LockScreenState.Visible).activationState is ActivationState.Active)
+ assertEquals(R.drawable.ic_flashlight_on, (lastValue.icon as? Icon.Resource)?.res)
+ job.cancel()
+ }
+
+ @Test
+ fun `flashlight availability now on -- flashlight off -- inactive and icon off`() = runTest {
+ //given
+ flashlightController.isEnabled = false
+ flashlightController.isAvailable = false
+ val values = mutableListOf<KeyguardQuickAffordanceConfig.LockScreenState>()
+ val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values)}
+
+ //when
+ flashlightController.onFlashlightAvailabilityChanged(true)
+ val lastValue = values.last()
+
+ //then
+ assertTrue(lastValue is KeyguardQuickAffordanceConfig.LockScreenState.Visible)
+ assertTrue((lastValue as KeyguardQuickAffordanceConfig.LockScreenState.Visible).activationState is ActivationState.Inactive)
+ assertEquals(R.drawable.ic_flashlight_off, (lastValue.icon as? Icon.Resource)?.res)
+ job.cancel()
+ }
+
+ @Test
+ fun `flashlight available -- picker state default`() = runTest {
+ //given
+ flashlightController.isAvailable = true
+
+ //when
+ val result = underTest.getPickerScreenState()
+
+ //then
+ assertTrue(result is KeyguardQuickAffordanceConfig.PickerScreenState.Default)
+ }
+
+ @Test
+ fun `flashlight not available -- picker state unavailable`() = runTest {
+ //given
+ flashlightController.isAvailable = false
+
+ //when
+ val result = underTest.getPickerScreenState()
+
+ //then
+ assertTrue(result is KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice)
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/table/TableChangeTest.kt b/packages/SystemUI/tests/src/com/android/systemui/log/table/TableChangeTest.kt
new file mode 100644
index 0000000..432764a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/log/table/TableChangeTest.kt
@@ -0,0 +1,102 @@
+/*
+ * 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.log.table
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+@SmallTest
+class TableChangeTest : SysuiTestCase() {
+
+ @Test
+ fun setString_isString() {
+ val underTest = TableChange()
+
+ underTest.reset(timestamp = 100, columnPrefix = "", columnName = "fakeName")
+ underTest.set("fakeValue")
+
+ assertThat(underTest.hasData()).isTrue()
+ assertThat(underTest.getVal()).isEqualTo("fakeValue")
+ }
+
+ @Test
+ fun setBoolean_isBoolean() {
+ val underTest = TableChange()
+
+ underTest.reset(timestamp = 100, columnPrefix = "", columnName = "fakeName")
+ underTest.set(true)
+
+ assertThat(underTest.hasData()).isTrue()
+ assertThat(underTest.getVal()).isEqualTo("true")
+ }
+
+ @Test
+ fun setInt_isInt() {
+ val underTest = TableChange()
+
+ underTest.reset(timestamp = 100, columnPrefix = "", columnName = "fakeName")
+ underTest.set(8900)
+
+ assertThat(underTest.hasData()).isTrue()
+ assertThat(underTest.getVal()).isEqualTo("8900")
+ }
+
+ @Test
+ fun setThenReset_isEmpty() {
+ val underTest = TableChange()
+
+ underTest.reset(timestamp = 100, columnPrefix = "", columnName = "fakeName")
+ underTest.set(8900)
+ underTest.reset(timestamp = 0, columnPrefix = "prefix", columnName = "name")
+
+ assertThat(underTest.hasData()).isFalse()
+ assertThat(underTest.getVal()).isEqualTo("null")
+ }
+
+ @Test
+ fun getName_hasPrefix() {
+ val underTest = TableChange(columnPrefix = "fakePrefix", columnName = "fakeName")
+
+ assertThat(underTest.getName()).contains("fakePrefix")
+ assertThat(underTest.getName()).contains("fakeName")
+ }
+
+ @Test
+ fun getName_noPrefix() {
+ val underTest = TableChange(columnPrefix = "", columnName = "fakeName")
+
+ assertThat(underTest.getName()).contains("fakeName")
+ }
+
+ @Test
+ fun resetThenSet_hasNewValue() {
+ val underTest = TableChange()
+
+ underTest.reset(timestamp = 100, columnPrefix = "prefix", columnName = "original")
+ underTest.set("fakeValue")
+ underTest.reset(timestamp = 0, columnPrefix = "", columnName = "updated")
+ underTest.set(8900)
+
+ assertThat(underTest.hasData()).isTrue()
+ assertThat(underTest.getName()).contains("updated")
+ assertThat(underTest.getName()).doesNotContain("prefix")
+ assertThat(underTest.getName()).doesNotContain("original")
+ assertThat(underTest.getVal()).isEqualTo("8900")
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferTest.kt b/packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferTest.kt
new file mode 100644
index 0000000..688c66a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferTest.kt
@@ -0,0 +1,260 @@
+/*
+ * 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.log.table
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import java.io.PrintWriter
+import java.io.StringWriter
+import org.junit.Before
+import org.junit.Test
+
+@SmallTest
+class TableLogBufferTest : SysuiTestCase() {
+ private lateinit var underTest: TableLogBuffer
+
+ private lateinit var systemClock: FakeSystemClock
+ private lateinit var outputWriter: StringWriter
+
+ @Before
+ fun setup() {
+ systemClock = FakeSystemClock()
+ outputWriter = StringWriter()
+
+ underTest = TableLogBuffer(MAX_SIZE, NAME, systemClock)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun maxSizeZero_throwsException() {
+ TableLogBuffer(maxSize = 0, "name", systemClock)
+ }
+
+ @Test
+ fun dumpChanges_strChange_logsFromNext() {
+ systemClock.setCurrentTimeMillis(100L)
+
+ val prevDiffable =
+ object : TestDiffable() {
+ override fun logDiffs(prevVal: TestDiffable, row: TableRowLogger) {
+ row.logChange("stringValChange", "prevStringVal")
+ }
+ }
+ val nextDiffable =
+ object : TestDiffable() {
+ override fun logDiffs(prevVal: TestDiffable, row: TableRowLogger) {
+ row.logChange("stringValChange", "newStringVal")
+ }
+ }
+
+ underTest.logDiffs("prefix", prevDiffable, nextDiffable)
+
+ val dumpedString = dumpChanges()
+
+ assertThat(dumpedString).contains("prefix")
+ assertThat(dumpedString).contains("stringValChange")
+ assertThat(dumpedString).contains("newStringVal")
+ assertThat(dumpedString).doesNotContain("prevStringVal")
+ assertThat(dumpedString).contains(TABLE_LOG_DATE_FORMAT.format(100L))
+ }
+
+ @Test
+ fun dumpChanges_boolChange_logsFromNext() {
+ systemClock.setCurrentTimeMillis(100L)
+
+ val prevDiffable =
+ object : TestDiffable() {
+ override fun logDiffs(prevVal: TestDiffable, row: TableRowLogger) {
+ row.logChange("booleanValChange", false)
+ }
+ }
+ val nextDiffable =
+ object : TestDiffable() {
+ override fun logDiffs(prevVal: TestDiffable, row: TableRowLogger) {
+ row.logChange("booleanValChange", true)
+ }
+ }
+
+ underTest.logDiffs("prefix", prevDiffable, nextDiffable)
+
+ val dumpedString = dumpChanges()
+
+ assertThat(dumpedString).contains("prefix")
+ assertThat(dumpedString).contains("booleanValChange")
+ assertThat(dumpedString).contains("true")
+ assertThat(dumpedString).doesNotContain("false")
+ assertThat(dumpedString).contains(TABLE_LOG_DATE_FORMAT.format(100L))
+ }
+
+ @Test
+ fun dumpChanges_intChange_logsFromNext() {
+ systemClock.setCurrentTimeMillis(100L)
+
+ val prevDiffable =
+ object : TestDiffable() {
+ override fun logDiffs(prevVal: TestDiffable, row: TableRowLogger) {
+ row.logChange("intValChange", 12345)
+ }
+ }
+ val nextDiffable =
+ object : TestDiffable() {
+ override fun logDiffs(prevVal: TestDiffable, row: TableRowLogger) {
+ row.logChange("intValChange", 67890)
+ }
+ }
+
+ underTest.logDiffs("prefix", prevDiffable, nextDiffable)
+
+ val dumpedString = dumpChanges()
+
+ assertThat(dumpedString).contains("prefix")
+ assertThat(dumpedString).contains("intValChange")
+ assertThat(dumpedString).contains("67890")
+ assertThat(dumpedString).doesNotContain("12345")
+ assertThat(dumpedString).contains(TABLE_LOG_DATE_FORMAT.format(100L))
+ }
+
+ @Test
+ fun dumpChanges_noPrefix() {
+ systemClock.setCurrentTimeMillis(100L)
+
+ val prevDiffable =
+ object : TestDiffable() {
+ override fun logDiffs(prevVal: TestDiffable, row: TableRowLogger) {
+ row.logChange("booleanValChange", false)
+ }
+ }
+ val nextDiffable =
+ object : TestDiffable() {
+ override fun logDiffs(prevVal: TestDiffable, row: TableRowLogger) {
+ row.logChange("booleanValChange", true)
+ }
+ }
+
+ // WHEN there's a blank prefix
+ underTest.logDiffs("", prevDiffable, nextDiffable)
+
+ val dumpedString = dumpChanges()
+
+ // THEN the dump still works
+ assertThat(dumpedString).contains("booleanValChange")
+ assertThat(dumpedString).contains("true")
+ assertThat(dumpedString).contains(TABLE_LOG_DATE_FORMAT.format(100L))
+ }
+
+ @Test
+ fun dumpChanges_multipleChangesForSameColumn_logs() {
+ lateinit var valToDump: String
+
+ val diffable =
+ object : TestDiffable() {
+ override fun logDiffs(prevVal: TestDiffable, row: TableRowLogger) {
+ row.logChange("valChange", valToDump)
+ }
+ }
+
+ systemClock.setCurrentTimeMillis(12000L)
+ valToDump = "stateValue12"
+ underTest.logDiffs(columnPrefix = "", diffable, diffable)
+
+ systemClock.setCurrentTimeMillis(20000L)
+ valToDump = "stateValue20"
+ underTest.logDiffs(columnPrefix = "", diffable, diffable)
+
+ systemClock.setCurrentTimeMillis(40000L)
+ valToDump = "stateValue40"
+ underTest.logDiffs(columnPrefix = "", diffable, diffable)
+
+ systemClock.setCurrentTimeMillis(45000L)
+ valToDump = "stateValue45"
+ underTest.logDiffs(columnPrefix = "", diffable, diffable)
+
+ val dumpedString = dumpChanges()
+
+ assertThat(dumpedString).contains("valChange")
+ assertThat(dumpedString).contains("stateValue12")
+ assertThat(dumpedString).contains("stateValue20")
+ assertThat(dumpedString).contains("stateValue40")
+ assertThat(dumpedString).contains("stateValue45")
+ assertThat(dumpedString).contains(TABLE_LOG_DATE_FORMAT.format(12000L))
+ assertThat(dumpedString).contains(TABLE_LOG_DATE_FORMAT.format(20000L))
+ assertThat(dumpedString).contains(TABLE_LOG_DATE_FORMAT.format(40000L))
+ assertThat(dumpedString).contains(TABLE_LOG_DATE_FORMAT.format(45000L))
+ }
+
+ @Test
+ fun dumpChanges_multipleChangesAtOnce_logs() {
+ systemClock.setCurrentTimeMillis(100L)
+
+ val prevDiffable = object : TestDiffable() {}
+ val nextDiffable =
+ object : TestDiffable() {
+ override fun logDiffs(prevVal: TestDiffable, row: TableRowLogger) {
+ row.logChange("status", "in progress")
+ row.logChange("connected", false)
+ }
+ }
+
+ underTest.logDiffs(columnPrefix = "", prevDiffable, nextDiffable)
+
+ val dumpedString = dumpChanges()
+
+ assertThat(dumpedString).contains("status")
+ assertThat(dumpedString).contains("in progress")
+ assertThat(dumpedString).contains("connected")
+ assertThat(dumpedString).contains("false")
+ }
+
+ @Test
+ fun dumpChanges_rotatesIfBufferIsFull() {
+ lateinit var valToDump: String
+
+ val prevDiffable = object : TestDiffable() {}
+ val nextDiffable =
+ object : TestDiffable() {
+ override fun logDiffs(prevVal: TestDiffable, row: TableRowLogger) {
+ row.logChange("status", valToDump)
+ }
+ }
+
+ for (i in 0 until MAX_SIZE + 3) {
+ valToDump = "testString[$i]"
+ underTest.logDiffs(columnPrefix = "", prevDiffable, nextDiffable)
+ }
+
+ val dumpedString = dumpChanges()
+
+ assertThat(dumpedString).doesNotContain("testString[0]")
+ assertThat(dumpedString).doesNotContain("testString[1]")
+ assertThat(dumpedString).doesNotContain("testString[2]")
+ assertThat(dumpedString).contains("testString[3]")
+ assertThat(dumpedString).contains("testString[${MAX_SIZE + 2}]")
+ }
+
+ private fun dumpChanges(): String {
+ underTest.dumpChanges(PrintWriter(outputWriter))
+ return outputWriter.toString()
+ }
+
+ private abstract class TestDiffable : Diffable<TestDiffable> {
+ override fun logDiffs(prevVal: TestDiffable, row: TableRowLogger) {}
+ }
+}
+
+private const val NAME = "TestTableBuffer"
+private const val MAX_SIZE = 10
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt
index e1007fa..858d0e7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt
@@ -35,6 +35,8 @@
import com.android.systemui.animation.ShadeInterpolation
import com.android.systemui.battery.BatteryMeterView
import com.android.systemui.battery.BatteryMeterViewController
+import com.android.systemui.demomode.DemoMode
+import com.android.systemui.demomode.DemoModeController
import com.android.systemui.dump.DumpManager
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
@@ -50,10 +52,12 @@
import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
import com.android.systemui.statusbar.phone.StatusBarIconController
import com.android.systemui.statusbar.phone.StatusIconContainer
+import com.android.systemui.statusbar.policy.Clock
import com.android.systemui.statusbar.policy.FakeConfigurationController
import com.android.systemui.statusbar.policy.VariableDateView
import com.android.systemui.statusbar.policy.VariableDateViewController
import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
@@ -104,7 +108,7 @@
@Mock
private lateinit var featureFlags: FeatureFlags
@Mock
- private lateinit var clock: TextView
+ private lateinit var clock: Clock
@Mock
private lateinit var date: VariableDateView
@Mock
@@ -138,6 +142,7 @@
private lateinit var qsConstraints: ConstraintSet
@Mock
private lateinit var largeScreenConstraints: ConstraintSet
+ @Mock private lateinit var demoModeController: DemoModeController
@JvmField @Rule
val mockitoRule = MockitoJUnit.rule()
@@ -146,10 +151,12 @@
private lateinit var controller: LargeScreenShadeHeaderController
private lateinit var carrierIconSlots: List<String>
private val configurationController = FakeConfigurationController()
+ private lateinit var demoModeControllerCapture: ArgumentCaptor<DemoMode>
@Before
fun setUp() {
- whenever<TextView>(view.findViewById(R.id.clock)).thenReturn(clock)
+ demoModeControllerCapture = argumentCaptor<DemoMode>()
+ whenever<Clock>(view.findViewById(R.id.clock)).thenReturn(clock)
whenever(clock.context).thenReturn(mockedContext)
whenever<TextView>(view.findViewById(R.id.date)).thenReturn(date)
@@ -195,7 +202,8 @@
dumpManager,
featureFlags,
qsCarrierGroupControllerBuilder,
- combinedShadeHeadersConstraintManager
+ combinedShadeHeadersConstraintManager,
+ demoModeController
)
whenever(view.isAttachedToWindow).thenReturn(true)
controller.init()
@@ -617,6 +625,21 @@
}
@Test
+ fun demoMode_attachDemoMode() {
+ verify(demoModeController).addCallback(capture(demoModeControllerCapture))
+ demoModeControllerCapture.value.onDemoModeStarted()
+ verify(clock).onDemoModeStarted()
+ }
+
+ @Test
+ fun demoMode_detachDemoMode() {
+ controller.simulateViewDetached()
+ verify(demoModeController).removeCallback(capture(demoModeControllerCapture))
+ demoModeControllerCapture.value.onDemoModeFinished()
+ verify(clock).onDemoModeFinished()
+ }
+
+ @Test
fun animateOutOnStartCustomizing() {
val animator = Mockito.mock(ViewPropertyAnimator::class.java, Answers.RETURNS_SELF)
val duration = 1000L
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt
index 90ae693..b4c8f98 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt
@@ -13,6 +13,8 @@
import com.android.systemui.animation.ShadeInterpolation
import com.android.systemui.battery.BatteryMeterView
import com.android.systemui.battery.BatteryMeterViewController
+import com.android.systemui.demomode.DemoMode
+import com.android.systemui.demomode.DemoModeController
import com.android.systemui.dump.DumpManager
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
@@ -22,9 +24,12 @@
import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
import com.android.systemui.statusbar.phone.StatusBarIconController
import com.android.systemui.statusbar.phone.StatusIconContainer
+import com.android.systemui.statusbar.policy.Clock
import com.android.systemui.statusbar.policy.FakeConfigurationController
import com.android.systemui.statusbar.policy.VariableDateViewController
import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.capture
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Before
@@ -52,7 +57,7 @@
@Mock private lateinit var qsCarrierGroupController: QSCarrierGroupController
@Mock private lateinit var qsCarrierGroupControllerBuilder: QSCarrierGroupController.Builder
@Mock private lateinit var featureFlags: FeatureFlags
- @Mock private lateinit var clock: TextView
+ @Mock private lateinit var clock: Clock
@Mock private lateinit var date: TextView
@Mock private lateinit var carrierGroup: QSCarrierGroup
@Mock private lateinit var batteryMeterView: BatteryMeterView
@@ -66,6 +71,7 @@
CombinedShadeHeadersConstraintManager
@Mock private lateinit var mockedContext: Context
+ @Mock private lateinit var demoModeController: DemoModeController
@JvmField @Rule val mockitoRule = MockitoJUnit.rule()
var viewVisibility = View.GONE
@@ -76,7 +82,7 @@
@Before
fun setup() {
- whenever<TextView>(view.findViewById(R.id.clock)).thenReturn(clock)
+ whenever<Clock>(view.findViewById(R.id.clock)).thenReturn(clock)
whenever(clock.context).thenReturn(mockedContext)
whenever<TextView>(view.findViewById(R.id.date)).thenReturn(date)
whenever(date.context).thenReturn(mockedContext)
@@ -111,8 +117,9 @@
dumpManager,
featureFlags,
qsCarrierGroupControllerBuilder,
- combinedShadeHeadersConstraintManager
- )
+ combinedShadeHeadersConstraintManager,
+ demoModeController
+ )
whenever(view.isAttachedToWindow).thenReturn(true)
mLargeScreenShadeHeaderController.init()
carrierIconSlots = listOf(
@@ -230,4 +237,21 @@
verify(animator).setInterpolator(Interpolators.ALPHA_IN)
verify(animator).start()
}
+
+ @Test
+ fun demoMode_attachDemoMode() {
+ val cb = argumentCaptor<DemoMode>()
+ verify(demoModeController).addCallback(capture(cb))
+ cb.value.onDemoModeStarted()
+ verify(clock).onDemoModeStarted()
+ }
+
+ @Test
+ fun demoMode_detachDemoMode() {
+ mLargeScreenShadeHeaderController.simulateViewDetached()
+ val cb = argumentCaptor<DemoMode>()
+ verify(demoModeController).removeCallback(capture(cb))
+ cb.value.onDemoModeFinished()
+ verify(clock).onDemoModeFinished()
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt
index 3d29d2b..30fd308 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt
@@ -18,8 +18,10 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.log.table.TableRowLogger
import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel.Active.Companion.MAX_VALID_LEVEL
import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel.Active.Companion.MIN_VALID_LEVEL
+import com.google.common.truth.Truth.assertThat
import org.junit.Test
@SmallTest
@@ -48,6 +50,125 @@
WifiNetworkModel.Active(NETWORK_ID, level = MAX_VALID_LEVEL + 1)
}
+ // Non-exhaustive logDiffs test -- just want to make sure the logging logic isn't totally broken
+
+ @Test
+ fun logDiffs_inactiveToActive_logsAllActiveFields() {
+ val logger = TestLogger()
+ val activeNetwork =
+ WifiNetworkModel.Active(
+ networkId = 5,
+ isValidated = true,
+ level = 3,
+ ssid = "Test SSID"
+ )
+
+ activeNetwork.logDiffs(prevVal = WifiNetworkModel.Inactive, logger)
+
+ assertThat(logger.changes).contains(Pair(COL_NETWORK_TYPE, TYPE_ACTIVE))
+ assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, "5"))
+ assertThat(logger.changes).contains(Pair(COL_VALIDATED, "true"))
+ assertThat(logger.changes).contains(Pair(COL_LEVEL, "3"))
+ assertThat(logger.changes).contains(Pair(COL_SSID, "Test SSID"))
+ }
+ @Test
+ fun logDiffs_activeToInactive_resetsAllActiveFields() {
+ val logger = TestLogger()
+ val activeNetwork =
+ WifiNetworkModel.Active(
+ networkId = 5,
+ isValidated = true,
+ level = 3,
+ ssid = "Test SSID"
+ )
+
+ WifiNetworkModel.Inactive.logDiffs(prevVal = activeNetwork, logger)
+
+ assertThat(logger.changes).contains(Pair(COL_NETWORK_TYPE, TYPE_INACTIVE))
+ assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, NETWORK_ID_DEFAULT.toString()))
+ assertThat(logger.changes).contains(Pair(COL_VALIDATED, "false"))
+ assertThat(logger.changes).contains(Pair(COL_LEVEL, LEVEL_DEFAULT.toString()))
+ assertThat(logger.changes).contains(Pair(COL_SSID, "null"))
+ }
+
+ @Test
+ fun logDiffs_carrierMergedToActive_logsAllActiveFields() {
+ val logger = TestLogger()
+ val activeNetwork =
+ WifiNetworkModel.Active(
+ networkId = 5,
+ isValidated = true,
+ level = 3,
+ ssid = "Test SSID"
+ )
+
+ activeNetwork.logDiffs(prevVal = WifiNetworkModel.CarrierMerged, logger)
+
+ assertThat(logger.changes).contains(Pair(COL_NETWORK_TYPE, TYPE_ACTIVE))
+ assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, "5"))
+ assertThat(logger.changes).contains(Pair(COL_VALIDATED, "true"))
+ assertThat(logger.changes).contains(Pair(COL_LEVEL, "3"))
+ assertThat(logger.changes).contains(Pair(COL_SSID, "Test SSID"))
+ }
+ @Test
+ fun logDiffs_activeToCarrierMerged_resetsAllActiveFields() {
+ val logger = TestLogger()
+ val activeNetwork =
+ WifiNetworkModel.Active(
+ networkId = 5,
+ isValidated = true,
+ level = 3,
+ ssid = "Test SSID"
+ )
+
+ WifiNetworkModel.CarrierMerged.logDiffs(prevVal = activeNetwork, logger)
+
+ assertThat(logger.changes).contains(Pair(COL_NETWORK_TYPE, TYPE_CARRIER_MERGED))
+ assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, NETWORK_ID_DEFAULT.toString()))
+ assertThat(logger.changes).contains(Pair(COL_VALIDATED, "false"))
+ assertThat(logger.changes).contains(Pair(COL_LEVEL, LEVEL_DEFAULT.toString()))
+ assertThat(logger.changes).contains(Pair(COL_SSID, "null"))
+ }
+
+ @Test
+ fun logDiffs_activeChangesLevel_onlyLevelLogged() {
+ val logger = TestLogger()
+ val prevActiveNetwork =
+ WifiNetworkModel.Active(
+ networkId = 5,
+ isValidated = true,
+ level = 3,
+ ssid = "Test SSID"
+ )
+ val newActiveNetwork =
+ WifiNetworkModel.Active(
+ networkId = 5,
+ isValidated = true,
+ level = 2,
+ ssid = "Test SSID"
+ )
+
+ newActiveNetwork.logDiffs(prevActiveNetwork, logger)
+
+ assertThat(logger.changes).isEqualTo(listOf(Pair(COL_LEVEL, "2")))
+ }
+
+ private class TestLogger : TableRowLogger {
+ val changes = mutableListOf<Pair<String, String>>()
+
+ override fun logChange(columnName: String, value: String?) {
+ changes.add(Pair(columnName, value.toString()))
+ }
+
+ override fun logChange(columnName: String, value: Int) {
+ changes.add(Pair(columnName, value.toString()))
+ }
+
+ override fun logChange(columnName: String, value: Boolean) {
+ changes.add(Pair(columnName, value.toString()))
+ }
+ }
+
companion object {
private const val NETWORK_ID = 2
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt
index a64a4bd..800f3c0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt
@@ -29,6 +29,7 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl.Companion.ACTIVITY_DEFAULT
@@ -69,6 +70,7 @@
@Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
@Mock private lateinit var logger: ConnectivityPipelineLogger
+ @Mock private lateinit var tableLogger: TableLogBuffer
@Mock private lateinit var connectivityManager: ConnectivityManager
@Mock private lateinit var wifiManager: WifiManager
private lateinit var executor: Executor
@@ -804,6 +806,7 @@
broadcastDispatcher,
connectivityManager,
logger,
+ tableLogger,
executor,
scope,
wifiManagerToUse,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeFlashlightController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeFlashlightController.java
index f6fd2cb..f68baf5 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeFlashlightController.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeFlashlightController.java
@@ -16,32 +16,71 @@
import android.testing.LeakCheck;
+import androidx.annotation.VisibleForTesting;
+
import com.android.systemui.statusbar.policy.FlashlightController;
import com.android.systemui.statusbar.policy.FlashlightController.FlashlightListener;
+import java.util.ArrayList;
+import java.util.List;
+
public class FakeFlashlightController extends BaseLeakChecker<FlashlightListener>
implements FlashlightController {
+
+ private final List<FlashlightListener> callbacks = new ArrayList<>();
+
+ @VisibleForTesting
+ public boolean isAvailable;
+ @VisibleForTesting
+ public boolean isEnabled;
+ @VisibleForTesting
+ public boolean hasFlashlight;
+
public FakeFlashlightController(LeakCheck test) {
super(test, "flashlight");
}
+ @VisibleForTesting
+ public void onFlashlightAvailabilityChanged(boolean newValue) {
+ callbacks.forEach(
+ flashlightListener -> flashlightListener.onFlashlightAvailabilityChanged(newValue)
+ );
+ }
+
+ @VisibleForTesting
+ public void onFlashlightError() {
+ callbacks.forEach(FlashlightListener::onFlashlightError);
+ }
+
@Override
public boolean hasFlashlight() {
- return false;
+ return hasFlashlight;
}
@Override
public void setFlashlight(boolean newState) {
-
+ callbacks.forEach(flashlightListener -> flashlightListener.onFlashlightChanged(newState));
}
@Override
public boolean isAvailable() {
- return false;
+ return isAvailable;
}
@Override
public boolean isEnabled() {
- return false;
+ return isEnabled;
+ }
+
+ @Override
+ public void addCallback(FlashlightListener listener) {
+ super.addCallback(listener);
+ callbacks.add(listener);
+ }
+
+ @Override
+ public void removeCallback(FlashlightListener listener) {
+ super.removeCallback(listener);
+ callbacks.remove(listener);
}
}
diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
index 3cfae60..8baae53a 100644
--- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
+++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
@@ -35,6 +35,7 @@
import android.app.AppGlobals;
import android.app.AppOpsManager;
import android.app.AppOpsManagerInternal;
+import android.app.BroadcastOptions;
import android.app.IApplicationThread;
import android.app.IServiceConnection;
import android.app.KeyguardManager;
@@ -257,6 +258,9 @@
private boolean mIsProviderInfoPersisted;
private boolean mIsCombinedBroadcastEnabled;
+ // Mark widget lifecycle broadcasts as 'interactive'
+ private Bundle mInteractiveBroadcast;
+
AppWidgetServiceImpl(Context context) {
mContext = context;
}
@@ -286,6 +290,11 @@
Slog.d(TAG, "App widget provider info will not be persisted on this device");
}
+ BroadcastOptions opts = BroadcastOptions.makeBasic();
+ opts.setBackgroundActivityStartsAllowed(false);
+ opts.setInteractive(true);
+ mInteractiveBroadcast = opts.toBundle();
+
computeMaximumWidgetBitmapMemory();
registerBroadcastReceiver();
registerOnCrossProfileProvidersChangedListener();
@@ -2379,33 +2388,40 @@
Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_ENABLE_AND_UPDATE);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds);
intent.setComponent(p.id.componentName);
- sendBroadcastAsUser(intent, p.id.getProfile());
+ // Placing a widget is something users expect to be UX-responsive, so mark this
+ // broadcast as interactive
+ sendBroadcastAsUser(intent, p.id.getProfile(), true);
}
private void sendEnableIntentLocked(Provider p) {
Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_ENABLED);
intent.setComponent(p.id.componentName);
- sendBroadcastAsUser(intent, p.id.getProfile());
+ // Enabling the widget is something users expect to be UX-responsive, so mark this
+ // broadcast as interactive
+ sendBroadcastAsUser(intent, p.id.getProfile(), true);
}
private void sendUpdateIntentLocked(Provider provider, int[] appWidgetIds) {
Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds);
intent.setComponent(provider.id.componentName);
- sendBroadcastAsUser(intent, provider.id.getProfile());
+ // Periodic background widget update heartbeats are not an interactive use case
+ sendBroadcastAsUser(intent, provider.id.getProfile(), false);
}
private void sendDeletedIntentLocked(Widget widget) {
Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_DELETED);
intent.setComponent(widget.provider.id.componentName);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widget.appWidgetId);
- sendBroadcastAsUser(intent, widget.provider.id.getProfile());
+ // Cleanup after deletion isn't an interactive UX case
+ sendBroadcastAsUser(intent, widget.provider.id.getProfile(), false);
}
private void sendDisabledIntentLocked(Provider provider) {
Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_DISABLED);
intent.setComponent(provider.id.componentName);
- sendBroadcastAsUser(intent, provider.id.getProfile());
+ // Cleanup after disable isn't an interactive UX case
+ sendBroadcastAsUser(intent, provider.id.getProfile(), false);
}
public void sendOptionsChangedIntentLocked(Widget widget) {
@@ -2413,7 +2429,9 @@
intent.setComponent(widget.provider.id.componentName);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widget.appWidgetId);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS, widget.options);
- sendBroadcastAsUser(intent, widget.provider.id.getProfile());
+ // The user's changed the options, so seeing them take effect promptly is
+ // an interactive UX expectation
+ sendBroadcastAsUser(intent, widget.provider.id.getProfile(), true);
}
@GuardedBy("mLock")
@@ -3666,10 +3684,17 @@
return null;
}
- private void sendBroadcastAsUser(Intent intent, UserHandle userHandle) {
+ /**
+ * Sends a widget lifecycle broadcast within the specified user. If {@code isInteractive}
+ * is specified as {@code true}, the broadcast dispatch mechanism will be told that it
+ * is related to a UX flow with user-visible expectations about timely dispatch. This
+ * should only be used for broadcast flows that do have such expectations.
+ */
+ private void sendBroadcastAsUser(Intent intent, UserHandle userHandle, boolean isInteractive) {
final long identity = Binder.clearCallingIdentity();
try {
- mContext.sendBroadcastAsUser(intent, userHandle);
+ mContext.sendBroadcastAsUser(intent, userHandle, null,
+ isInteractive ? mInteractiveBroadcast : null);
} finally {
Binder.restoreCallingIdentity(identity);
}
@@ -5008,18 +5033,20 @@
private void sendWidgetRestoreBroadcastLocked(String action, Provider provider,
Host host, int[] oldIds, int[] newIds, UserHandle userHandle) {
+ // Users expect restore to emplace widgets properly ASAP, so flag these as
+ // being interactive broadcast dispatches
Intent intent = new Intent(action);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_OLD_IDS, oldIds);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, newIds);
if (provider != null) {
intent.setComponent(provider.id.componentName);
- sendBroadcastAsUser(intent, userHandle);
+ sendBroadcastAsUser(intent, userHandle, true);
}
if (host != null) {
intent.setComponent(null);
intent.setPackage(host.id.packageName);
intent.putExtra(AppWidgetManager.EXTRA_HOST_ID, host.id.hostId);
- sendBroadcastAsUser(intent, userHandle);
+ sendBroadcastAsUser(intent, userHandle, true);
}
}
diff --git a/services/backup/java/com/android/server/backup/KeyValueAdbBackupEngine.java b/services/backup/java/com/android/server/backup/KeyValueAdbBackupEngine.java
index 0fe90b1..f5d6836 100644
--- a/services/backup/java/com/android/server/backup/KeyValueAdbBackupEngine.java
+++ b/services/backup/java/com/android/server/backup/KeyValueAdbBackupEngine.java
@@ -9,7 +9,7 @@
import android.app.ApplicationThreadConstants;
import android.app.IBackupAgent;
-import android.app.backup.BackupManager;
+import android.app.backup.BackupAnnotations;
import android.app.backup.FullBackup;
import android.app.backup.FullBackupDataOutput;
import android.app.backup.IBackupCallback;
@@ -148,7 +148,7 @@
try {
return mBackupManagerService.bindToAgentSynchronous(targetApp,
ApplicationThreadConstants.BACKUP_MODE_INCREMENTAL,
- BackupManager.OperationType.BACKUP);
+ BackupAnnotations.BackupDestination.CLOUD);
} catch (SecurityException e) {
Slog.e(TAG, "error in binding to agent for package " + targetApp.packageName
+ ". " + e);
diff --git a/services/backup/java/com/android/server/backup/UserBackupManagerService.java b/services/backup/java/com/android/server/backup/UserBackupManagerService.java
index 4cf63b3..ce3e628 100644
--- a/services/backup/java/com/android/server/backup/UserBackupManagerService.java
+++ b/services/backup/java/com/android/server/backup/UserBackupManagerService.java
@@ -46,8 +46,8 @@
import android.app.IBackupAgent;
import android.app.PendingIntent;
import android.app.backup.BackupAgent;
+import android.app.backup.BackupAnnotations.BackupDestination;
import android.app.backup.BackupManager;
-import android.app.backup.BackupManager.OperationType;
import android.app.backup.BackupManagerMonitor;
import android.app.backup.FullBackup;
import android.app.backup.IBackupManager;
@@ -405,7 +405,7 @@
private long mAncestralToken = 0;
private long mCurrentToken = 0;
@Nullable private File mAncestralSerialNumberFile;
- @OperationType private volatile long mAncestralOperationType;
+ @BackupDestination private volatile long mAncestralBackupDestination;
private final ContentObserver mSetupObserver;
private final BroadcastReceiver mRunInitReceiver;
@@ -550,7 +550,7 @@
mActivityManager = ActivityManager.getService();
mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
mScheduledBackupEligibility = getEligibilityRules(mPackageManager, userId,
- OperationType.BACKUP);
+ BackupDestination.CLOUD);
mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
@@ -844,8 +844,8 @@
mAncestralToken = ancestralToken;
}
- public void setAncestralOperationType(@OperationType int operationType) {
- mAncestralOperationType = operationType;
+ public void setAncestralBackupDestination(@BackupDestination int backupDestination) {
+ mAncestralBackupDestination = backupDestination;
}
public long getCurrentToken() {
@@ -1619,14 +1619,14 @@
/** Fires off a backup agent, blocking until it attaches or times out. */
@Nullable
public IBackupAgent bindToAgentSynchronous(ApplicationInfo app, int mode,
- @OperationType int operationType) {
+ @BackupDestination int backupDestination) {
IBackupAgent agent = null;
synchronized (mAgentConnectLock) {
mConnecting = true;
mConnectedAgent = null;
try {
if (mActivityManager.bindBackupAgent(app.packageName, mode, mUserId,
- operationType)) {
+ backupDestination)) {
Slog.d(TAG, addUserIdToLogMessage(mUserId, "awaiting agent for " + app));
// success; wait for the agent to arrive
@@ -1776,8 +1776,9 @@
}
private BackupEligibilityRules getEligibilityRulesForRestoreAtInstall(long restoreToken) {
- if (mAncestralOperationType == OperationType.MIGRATION && restoreToken == mAncestralToken) {
- return getEligibilityRulesForOperation(OperationType.MIGRATION);
+ if (mAncestralBackupDestination == BackupDestination.DEVICE_TRANSFER
+ && restoreToken == mAncestralToken) {
+ return getEligibilityRulesForOperation(BackupDestination.DEVICE_TRANSFER);
} else {
// If we're not using the ancestral data set, it means we're restoring from a backup
// that happened on this device.
@@ -1856,14 +1857,14 @@
final TransportConnection transportConnection;
final String transportDirName;
- int operationType;
+ int backupDestination;
try {
transportDirName =
mTransportManager.getTransportDirName(
mTransportManager.getCurrentTransportName());
transportConnection =
mTransportManager.getCurrentTransportClientOrThrow("BMS.requestBackup()");
- operationType = getOperationTypeFromTransport(transportConnection);
+ backupDestination = getBackupDestinationFromTransport(transportConnection);
} catch (TransportNotRegisteredException | TransportNotAvailableException
| RemoteException e) {
BackupObserverUtils.sendBackupFinished(observer, BackupManager.ERROR_TRANSPORT_ABORTED);
@@ -1876,7 +1877,7 @@
OnTaskFinishedListener listener =
caller -> mTransportManager.disposeOfTransportClient(transportConnection, caller);
BackupEligibilityRules backupEligibilityRules = getEligibilityRulesForOperation(
- operationType);
+ backupDestination);
Message msg = mBackupHandler.obtainMessage(MSG_REQUEST_BACKUP);
msg.obj = getRequestBackupParams(packages, observer, monitor, flags, backupEligibilityRules,
@@ -2373,7 +2374,7 @@
/* monitor */ null,
/* userInitiated */ false,
"BMS.beginFullBackup()",
- getEligibilityRulesForOperation(OperationType.BACKUP));
+ getEligibilityRulesForOperation(BackupDestination.CLOUD));
} catch (IllegalStateException e) {
Slog.w(TAG, "Failed to start backup", e);
runBackup = false;
@@ -2835,7 +2836,7 @@
Slog.i(TAG, addUserIdToLogMessage(mUserId, "Beginning adb backup..."));
BackupEligibilityRules eligibilityRules = getEligibilityRulesForOperation(
- OperationType.ADB_BACKUP);
+ BackupDestination.ADB_BACKUP);
AdbBackupParams params = new AdbBackupParams(fd, includeApks, includeObbs,
includeShared, doWidgets, doAllApps, includeSystem, compress, doKeyValue,
pkgList, eligibilityRules);
@@ -2924,7 +2925,7 @@
/* monitor */ null,
/* userInitiated */ false,
"BMS.fullTransportBackup()",
- getEligibilityRulesForOperation(OperationType.BACKUP));
+ getEligibilityRulesForOperation(BackupDestination.CLOUD));
// Acquiring wakelock for PerformFullTransportBackupTask before its start.
mWakelock.acquire();
(new Thread(task, "full-transport-master")).start();
@@ -3917,12 +3918,12 @@
}
}
- int operationType;
+ int backupDestination;
TransportConnection transportConnection = null;
try {
transportConnection = mTransportManager.getTransportClientOrThrow(
transport, /* caller */"BMS.beginRestoreSession");
- operationType = getOperationTypeFromTransport(transportConnection);
+ backupDestination = getBackupDestinationFromTransport(transportConnection);
} catch (TransportNotAvailableException | TransportNotRegisteredException
| RemoteException e) {
Slog.w(TAG, "Failed to get operation type from transport: " + e);
@@ -3951,7 +3952,7 @@
return null;
}
mActiveRestoreSession = new ActiveRestoreSession(this, packageName, transport,
- getEligibilityRulesForOperation(operationType));
+ getEligibilityRulesForOperation(backupDestination));
mBackupHandler.sendEmptyMessageDelayed(MSG_RESTORE_SESSION_TIMEOUT,
mAgentTimeoutParameters.getRestoreSessionTimeoutMillis());
}
@@ -4037,14 +4038,14 @@
}
public BackupEligibilityRules getEligibilityRulesForOperation(
- @OperationType int operationType) {
- return getEligibilityRules(mPackageManager, mUserId, operationType);
+ @BackupDestination int backupDestination) {
+ return getEligibilityRules(mPackageManager, mUserId, backupDestination);
}
private static BackupEligibilityRules getEligibilityRules(PackageManager packageManager,
- int userId, @OperationType int operationType) {
+ int userId, @BackupDestination int backupDestination) {
return new BackupEligibilityRules(packageManager,
- LocalServices.getService(PackageManagerInternal.class), userId, operationType);
+ LocalServices.getService(PackageManagerInternal.class), userId, backupDestination);
}
/** Prints service state for 'dumpsys backup'. */
@@ -4200,21 +4201,22 @@
}
@VisibleForTesting
- @OperationType int getOperationTypeFromTransport(TransportConnection transportConnection)
+ @BackupDestination int getBackupDestinationFromTransport(
+ TransportConnection transportConnection)
throws TransportNotAvailableException, RemoteException {
if (!shouldUseNewBackupEligibilityRules()) {
// Return the default to stick to the legacy behaviour.
- return OperationType.BACKUP;
+ return BackupDestination.CLOUD;
}
final long oldCallingId = Binder.clearCallingIdentity();
try {
BackupTransportClient transport = transportConnection.connectOrThrow(
- /* caller */ "BMS.getOperationTypeFromTransport");
+ /* caller */ "BMS.getBackupDestinationFromTransport");
if ((transport.getTransportFlags() & BackupAgent.FLAG_DEVICE_TO_DEVICE_TRANSFER) != 0) {
- return OperationType.MIGRATION;
+ return BackupDestination.DEVICE_TRANSFER;
} else {
- return OperationType.BACKUP;
+ return BackupDestination.CLOUD;
}
} finally {
Binder.restoreCallingIdentity(oldCallingId);
diff --git a/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java b/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java
index 379ae52..65682f4 100644
--- a/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java
+++ b/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java
@@ -315,7 +315,7 @@
mAgent =
backupManagerService.bindToAgentSynchronous(
mPkg.applicationInfo, ApplicationThreadConstants.BACKUP_MODE_FULL,
- mBackupEligibilityRules.getOperationType());
+ mBackupEligibilityRules.getBackupDestination());
}
return mAgent != null;
}
diff --git a/services/backup/java/com/android/server/backup/internal/BackupHandler.java b/services/backup/java/com/android/server/backup/internal/BackupHandler.java
index 95cc289..3ff6ba7 100644
--- a/services/backup/java/com/android/server/backup/internal/BackupHandler.java
+++ b/services/backup/java/com/android/server/backup/internal/BackupHandler.java
@@ -20,7 +20,7 @@
import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
import static com.android.server.backup.BackupManagerService.TAG;
-import android.app.backup.BackupManager.OperationType;
+import android.app.backup.BackupAnnotations.BackupDestination;
import android.app.backup.IBackupManagerMonitor;
import android.app.backup.RestoreSet;
import android.os.Handler;
@@ -240,7 +240,7 @@
/* userInitiated */ false,
/* nonIncremental */ false,
backupManagerService.getEligibilityRulesForOperation(
- OperationType.BACKUP));
+ BackupDestination.CLOUD));
} catch (Exception e) {
// unable to ask the transport its dir name -- transient failure, since
// the above check succeeded. Try again next time.
diff --git a/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java b/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java
index fd9c834..ca92b69 100644
--- a/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java
+++ b/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java
@@ -741,7 +741,7 @@
agent =
mBackupManagerService.bindToAgentSynchronous(
packageInfo.applicationInfo, BACKUP_MODE_INCREMENTAL,
- mBackupEligibilityRules.getOperationType());
+ mBackupEligibilityRules.getBackupDestination());
if (agent == null) {
mReporter.onAgentError(packageName);
throw AgentException.transitory();
diff --git a/services/backup/java/com/android/server/backup/restore/ActiveRestoreSession.java b/services/backup/java/com/android/server/backup/restore/ActiveRestoreSession.java
index 8b1d561..d3e4f13 100644
--- a/services/backup/java/com/android/server/backup/restore/ActiveRestoreSession.java
+++ b/services/backup/java/com/android/server/backup/restore/ActiveRestoreSession.java
@@ -16,8 +16,6 @@
package com.android.server.backup.restore;
-import static android.app.backup.BackupManager.OperationType;
-
import static com.android.server.backup.BackupManagerService.DEBUG;
import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
import static com.android.server.backup.internal.BackupHandler.MSG_RESTORE_SESSION_TIMEOUT;
@@ -26,6 +24,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.backup.BackupAnnotations.BackupDestination;
import android.app.backup.IBackupManagerMonitor;
import android.app.backup.IRestoreObserver;
import android.app.backup.IRestoreSession;
@@ -299,9 +298,9 @@
private BackupEligibilityRules getBackupEligibilityRules(RestoreSet restoreSet) {
// TODO(b/182986784): Remove device name comparison once a designated field for operation
// type is added to RestoreSet object.
- int operationType = DEVICE_NAME_FOR_D2D_SET.equals(restoreSet.device)
- ? OperationType.MIGRATION : OperationType.BACKUP;
- return mBackupManagerService.getEligibilityRulesForOperation(operationType);
+ int backupDestination = DEVICE_NAME_FOR_D2D_SET.equals(restoreSet.device)
+ ? BackupDestination.DEVICE_TRANSFER : BackupDestination.CLOUD;
+ return mBackupManagerService.getEligibilityRulesForOperation(backupDestination);
}
public synchronized int restorePackage(String packageName, IRestoreObserver observer,
diff --git a/services/backup/java/com/android/server/backup/restore/FullRestoreEngine.java b/services/backup/java/com/android/server/backup/restore/FullRestoreEngine.java
index e78c8d1..b042c30 100644
--- a/services/backup/java/com/android/server/backup/restore/FullRestoreEngine.java
+++ b/services/backup/java/com/android/server/backup/restore/FullRestoreEngine.java
@@ -28,6 +28,7 @@
import android.app.ApplicationThreadConstants;
import android.app.IBackupAgent;
import android.app.backup.BackupAgent;
+import android.app.backup.BackupAnnotations;
import android.app.backup.BackupManager;
import android.app.backup.FullBackup;
import android.app.backup.IBackupManagerMonitor;
@@ -398,7 +399,7 @@
FullBackup.KEY_VALUE_DATA_TOKEN.equals(info.domain)
? ApplicationThreadConstants.BACKUP_MODE_INCREMENTAL
: ApplicationThreadConstants.BACKUP_MODE_RESTORE_FULL,
- mBackupEligibilityRules.getOperationType());
+ mBackupEligibilityRules.getBackupDestination());
mAgentPackage = pkg;
} catch (IOException | NameNotFoundException e) {
// fall through to error handling
@@ -707,7 +708,8 @@
}
private boolean isRestorableFile(FileMetadata info) {
- if (mBackupEligibilityRules.getOperationType() == BackupManager.OperationType.MIGRATION) {
+ if (mBackupEligibilityRules.getBackupDestination()
+ == BackupAnnotations.BackupDestination.DEVICE_TRANSFER) {
// Everything is eligible for device-to-device migration.
return true;
}
diff --git a/services/backup/java/com/android/server/backup/restore/PerformAdbRestoreTask.java b/services/backup/java/com/android/server/backup/restore/PerformAdbRestoreTask.java
index 22af19e..515a172 100644
--- a/services/backup/java/com/android/server/backup/restore/PerformAdbRestoreTask.java
+++ b/services/backup/java/com/android/server/backup/restore/PerformAdbRestoreTask.java
@@ -24,7 +24,7 @@
import static com.android.server.backup.UserBackupManagerService.BACKUP_FILE_HEADER_MAGIC;
import static com.android.server.backup.UserBackupManagerService.BACKUP_FILE_VERSION;
-import android.app.backup.BackupManager;
+import android.app.backup.BackupAnnotations.BackupDestination;
import android.app.backup.IFullBackupRestoreObserver;
import android.content.pm.PackageManagerInternal;
import android.os.ParcelFileDescriptor;
@@ -112,7 +112,7 @@
BackupEligibilityRules eligibilityRules = new BackupEligibilityRules(
mBackupManagerService.getPackageManager(),
LocalServices.getService(PackageManagerInternal.class),
- mBackupManagerService.getUserId(), BackupManager.OperationType.ADB_BACKUP);
+ mBackupManagerService.getUserId(), BackupDestination.ADB_BACKUP);
FullRestoreEngine mEngine = new FullRestoreEngine(mBackupManagerService,
mOperationStorage, null, mObserver, null, null,
true, 0 /*unused*/, true, eligibilityRules);
diff --git a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
index 9f89339..18e28de 100644
--- a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
+++ b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
@@ -675,7 +675,7 @@
mAgent = backupManagerService.bindToAgentSynchronous(
mCurrentPackage.applicationInfo,
ApplicationThreadConstants.BACKUP_MODE_INCREMENTAL,
- mBackupEligibilityRules.getOperationType());
+ mBackupEligibilityRules.getBackupDestination());
if (mAgent == null) {
Slog.w(TAG, "Can't find backup agent for " + packageName);
mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
@@ -1160,8 +1160,8 @@
if (mIsSystemRestore && mPmAgent != null) {
backupManagerService.setAncestralPackages(mPmAgent.getRestoredPackages());
backupManagerService.setAncestralToken(mToken);
- backupManagerService.setAncestralOperationType(
- mBackupEligibilityRules.getOperationType());
+ backupManagerService.setAncestralBackupDestination(
+ mBackupEligibilityRules.getBackupDestination());
backupManagerService.writeRestoreTokens();
}
diff --git a/services/backup/java/com/android/server/backup/utils/BackupEligibilityRules.java b/services/backup/java/com/android/server/backup/utils/BackupEligibilityRules.java
index d0300ff..7f0b56f 100644
--- a/services/backup/java/com/android/server/backup/utils/BackupEligibilityRules.java
+++ b/services/backup/java/com/android/server/backup/utils/BackupEligibilityRules.java
@@ -23,7 +23,7 @@
import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME;
import android.annotation.Nullable;
-import android.app.backup.BackupManager.OperationType;
+import android.app.backup.BackupAnnotations.BackupDestination;
import android.app.backup.BackupTransport;
import android.app.compat.CompatChanges;
import android.compat.annotation.ChangeId;
@@ -61,7 +61,7 @@
private final PackageManager mPackageManager;
private final PackageManagerInternal mPackageManagerInternal;
private final int mUserId;
- @OperationType private final int mOperationType;
+ @BackupDestination private final int mBackupDestination;
/**
* When this change is enabled, {@code adb backup} is automatically turned on for apps
@@ -85,17 +85,17 @@
PackageManagerInternal packageManagerInternal,
int userId) {
return new BackupEligibilityRules(packageManager, packageManagerInternal, userId,
- OperationType.BACKUP);
+ BackupDestination.CLOUD);
}
public BackupEligibilityRules(PackageManager packageManager,
PackageManagerInternal packageManagerInternal,
int userId,
- @OperationType int operationType) {
+ @BackupDestination int backupDestination) {
mPackageManager = packageManager;
mPackageManagerInternal = packageManagerInternal;
mUserId = userId;
- mOperationType = operationType;
+ mBackupDestination = backupDestination;
}
/**
@@ -111,7 +111,7 @@
* </ol>
*
* However, the above eligibility rules are ignored for non-system apps in in case of
- * device-to-device migration, see {@link OperationType}.
+ * device-to-device migration, see {@link BackupDestination}.
*/
@VisibleForTesting
public boolean appIsEligibleForBackup(ApplicationInfo app) {
@@ -152,22 +152,22 @@
/**
* Check if this app allows backup. Apps can opt out of backup by stating
* android:allowBackup="false" in their manifest. However, this flag is ignored for non-system
- * apps during device-to-device migrations, see {@link OperationType}.
+ * apps during device-to-device migrations, see {@link BackupDestination}.
*
* @param app The app under check.
* @return boolean indicating whether backup is allowed.
*/
public boolean isAppBackupAllowed(ApplicationInfo app) {
boolean allowBackup = (app.flags & ApplicationInfo.FLAG_ALLOW_BACKUP) != 0;
- switch (mOperationType) {
- case OperationType.MIGRATION:
+ switch (mBackupDestination) {
+ case BackupDestination.DEVICE_TRANSFER:
// Backup / restore of all non-system apps is force allowed during
// device-to-device migration.
boolean isSystemApp = (app.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
boolean ignoreAllowBackup = !isSystemApp && CompatChanges.isChangeEnabled(
IGNORE_ALLOW_BACKUP_IN_D2D, app.packageName, UserHandle.of(mUserId));
return ignoreAllowBackup || allowBackup;
- case OperationType.ADB_BACKUP:
+ case BackupDestination.ADB_BACKUP:
String packageName = app.packageName;
if (packageName == null) {
Slog.w(TAG, "Invalid ApplicationInfo object");
@@ -207,10 +207,10 @@
// All other apps can use adb backup only when running in debuggable mode.
return isDebuggable;
}
- case OperationType.BACKUP:
+ case BackupDestination.CLOUD:
return allowBackup;
default:
- Slog.w(TAG, "Unknown operation type:" + mOperationType);
+ Slog.w(TAG, "Unknown operation type:" + mBackupDestination);
return false;
}
}
@@ -398,7 +398,7 @@
}
}
- public int getOperationType() {
- return mOperationType;
+ public int getBackupDestination() {
+ return mBackupDestination;
}
}
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index dd3020a..4539e9e 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -33,6 +33,7 @@
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST;
import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE;
import static android.os.PowerExemptionManager.REASON_ACTIVE_DEVICE_ADMIN;
import static android.os.PowerExemptionManager.REASON_ACTIVITY_STARTER;
import static android.os.PowerExemptionManager.REASON_ACTIVITY_VISIBILITY_GRACE_PERIOD;
@@ -227,7 +228,8 @@
private static final boolean DEBUG_DELAYED_SERVICE = DEBUG_SERVICE;
private static final boolean DEBUG_DELAYED_STARTS = DEBUG_DELAYED_SERVICE;
- private static final boolean DEBUG_SHORT_SERVICE = DEBUG_SERVICE;
+ // STOPSHIP(b/260012573) turn it off.
+ private static final boolean DEBUG_SHORT_SERVICE = true; // DEBUG_SERVICE;
private static final boolean LOG_SERVICE_START_STOP = DEBUG_SERVICE;
@@ -1286,6 +1288,8 @@
return;
}
+ maybeStopShortFgsTimeoutLocked(service);
+
final int uid = service.appInfo.uid;
final String packageName = service.name.getPackageName();
final String serviceName = service.name.getClassName();
@@ -1469,6 +1473,8 @@
}
}
+ maybeStopShortFgsTimeoutLocked(r);
+
final int uid = r.appInfo.uid;
final String packageName = r.name.getPackageName();
final String serviceName = r.name.getClassName();
@@ -1836,6 +1842,14 @@
+ String.format("0x%08X", manifestType)
+ " in service element of manifest file");
}
+ if ((foregroundServiceType & FOREGROUND_SERVICE_TYPE_SHORT_SERVICE) != 0
+ && foregroundServiceType != FOREGROUND_SERVICE_TYPE_SHORT_SERVICE) {
+ Slog.w(TAG_SERVICE, "startForeground(): FOREGROUND_SERVICE_TYPE_SHORT_SERVICE"
+ + " is combined with other types. SHORT_SERVICE will be ignored.");
+ // In this case, the service will be handled as a non-short, regular FGS
+ // anyway, so we just remove the SHORT_SERVICE type.
+ foregroundServiceType &= ~FOREGROUND_SERVICE_TYPE_SHORT_SERVICE;
+ }
}
boolean alreadyStartedOp = false;
@@ -1886,14 +1900,51 @@
int fgsTypeCheckCode = FGS_TYPE_POLICY_CHECK_UNKNOWN;
if (!ignoreForeground) {
- // TODO(short-service): There's a known long-standing bug that allows
- // a abound service to become "foreground" if setForeground() is called
- // (without actually "starting" it).
- // Unfortunately we can't just "fix" it because some apps are relying on it,
- // but this will cause a problem to short-fgs, so we should disallow it if
- // this happens and the type is SHORT_SERVICE.
- //
- // OTOH, if a valid short-service (which has to be "started"), happens to
+ if (foregroundServiceType == FOREGROUND_SERVICE_TYPE_SHORT_SERVICE
+ && !r.startRequested) {
+ // There's a long standing bug that allows a bound service to become
+ // a foreground service *even when it's not started*.
+ // Unfortunately, there are apps relying on this behavior, so we can't just
+ // suddenly disallow it.
+ // However, this would be very problematic if used with a short-FGS, so we
+ // explicitly disallow this combination.
+ // TODO(short-service): Change to another exception type?
+ throw new IllegalStateException(
+ "startForeground(SHORT_SERVICE) called on a service that's not"
+ + " started.");
+ }
+ // If the service is already an FGS, and the type is changing, then we
+ // may need to do some extra work here.
+ if (r.isForeground && (r.foregroundServiceType != foregroundServiceType)) {
+ // TODO(short-service): Consider transitions:
+ // A. Short -> other types:
+ // Apply the BG restriction again. Don't just allow it.
+ // i.e. unless the app is in a situation where it's allowed to start
+ // a FGS, this transition shouldn't be allowed.
+ // ... But think about it more, there may be a case this should be
+ // allowed.
+ //
+ // If the transition is allowed, stop the timeout.
+ // If the transition is _not_ allowed... keep the timeout?
+ //
+ // B. Short -> Short:
+ // Allowed, but the timeout won't reset. The original timeout is used.
+ // C. Other -> short:
+ // This should always be allowed.
+ // A timeout should start.
+
+ // For now, let's just disallow transition from / to SHORT_SERVICE.
+ final boolean isNewTypeShortFgs =
+ foregroundServiceType == FOREGROUND_SERVICE_TYPE_SHORT_SERVICE;
+ if (r.isShortFgs() != isNewTypeShortFgs) {
+ // TODO(short-service): We should (probably) allow it.
+ throw new IllegalArgumentException(
+ "setForeground(): Changing foreground service type from / to "
+ + " SHORT_SERVICE is now allowed");
+ }
+ }
+
+ // If a valid short-service (which has to be "started"), happens to
// also be bound, then we still _will_ apply a timeout, because it still has
// to be stopped.
if (r.mStartForegroundCount == 0) {
@@ -1932,24 +1983,6 @@
// on the same sarvice after it's created, regardless of whether
// stopForeground() has been called or not.
- // TODO(short-service): Consider transitions:
- // A. Short -> other types:
- // Apply the BG restriction again. Don't just allow it.
- // i.e. unless the app is in a situation where it's allowed to start
- // a FGS, this transition shouldn't be allowed.
- // ... But think about it more, there may be a case this should be
- // allowed.
- //
- // If the transition is allowed, stop the timeout.
- // If the transition is _not_ allowed... keep the timeout?
- //
- // B. Short -> Short:
- // This should be the same as case A
- // If this is allowed, the new timeout should start.
- // C. Other -> short:
- // This should always be allowed.
- // A timeout should start.
-
// The second or later time startForeground() is called after service is
// started. Check for app state again.
setFgsRestrictionLocked(r.serviceInfo.packageName, r.app.getPid(),
@@ -2106,8 +2139,11 @@
mAm.notifyPackageUse(r.serviceInfo.packageName,
PackageManager.NOTIFY_PACKAGE_USE_FOREGROUND_SERVICE);
- // TODO(short-service): Start counting a timeout.
-
+ // Note, we'll get here if setForeground(SHORT_SERVICE) is called on a
+ // already short-fgs.
+ // In that case, because ShortFgsInfo is already set, this method
+ // will be noop.
+ maybeStartShortFgsTimeoutAndUpdateShortFgsInfoLocked(r);
} else {
if (DEBUG_FOREGROUND_SERVICE) {
Slog.d(TAG, "Suppressing startForeground() for FAS " + r);
@@ -2141,7 +2177,7 @@
decActiveForegroundAppLocked(smap, r);
}
- // TODO(short-service): Stop the timeout. (any better place to do it?)
+ maybeStopShortFgsTimeoutLocked(r);
// Adjust notification handling before setting isForeground to false, because
// that state is relevant to the notification policy side.
@@ -2886,6 +2922,90 @@
psr.setHasReportedForegroundServices(anyForeground);
}
+ void unscheduleShortFgsTimeoutLocked(ServiceRecord sr) {
+ mAm.mHandler.removeMessages(ActivityManagerService.SERVICE_SHORT_FGS_ANR_TIMEOUT_MSG, sr);
+ mAm.mHandler.removeMessages(ActivityManagerService.SERVICE_SHORT_FGS_TIMEOUT_MSG, sr);
+ }
+
+ /**
+ * If {@code sr} is of a short-fgs, start a short-FGS timeout.
+ */
+ private void maybeStartShortFgsTimeoutAndUpdateShortFgsInfoLocked(ServiceRecord sr) {
+ if (!sr.isShortFgs()) {
+ return;
+ }
+ if (DEBUG_SHORT_SERVICE) {
+ Slog.i(TAG_SERVICE, "Short FGS started: " + sr);
+ }
+ if (sr.hasShortFgsInfo()) {
+ sr.getShortFgsInfo().update();
+ } else {
+ sr.setShortFgsInfo(SystemClock.uptimeMillis());
+ }
+ unscheduleShortFgsTimeoutLocked(sr); // Do it just in case
+
+ final Message msg = mAm.mHandler.obtainMessage(
+ ActivityManagerService.SERVICE_SHORT_FGS_TIMEOUT_MSG, sr);
+ mAm.mHandler.sendMessageAtTime(msg, sr.getShortFgsInfo().getTimeoutTime());
+ }
+
+ /**
+ * Stop the timeout for a ServiceRecord, if it's of a short-FGS.
+ */
+ private void maybeStopShortFgsTimeoutLocked(ServiceRecord sr) {
+ if (!sr.isShortFgs()) {
+ return;
+ }
+ if (DEBUG_SHORT_SERVICE) {
+ Slog.i(TAG_SERVICE, "Stop short FGS timeout: " + sr);
+ }
+ sr.clearShortFgsInfo();
+ unscheduleShortFgsTimeoutLocked(sr);
+ }
+
+ void onShortFgsTimeout(ServiceRecord sr) {
+ synchronized (mAm) {
+ if (!sr.shouldTriggerShortFgsTimeout()) {
+ return;
+ }
+ Slog.e(TAG_SERVICE, "Short FGS timed out: " + sr);
+ try {
+ sr.app.getThread().scheduleTimeoutService(sr, sr.getShortFgsInfo().getStartId());
+ } catch (RemoteException e) {
+ // TODO(short-service): Anything to do here?
+ }
+ // Schedule the ANR timeout.
+ final Message msg = mAm.mHandler.obtainMessage(
+ ActivityManagerService.SERVICE_SHORT_FGS_ANR_TIMEOUT_MSG, sr);
+ mAm.mHandler.sendMessageAtTime(msg, sr.getShortFgsInfo().getAnrTime());
+ }
+ }
+
+ void onShortFgsAnrTimeout(ServiceRecord sr) {
+ final String reason = "A foreground service of FOREGROUND_SERVICE_TYPE_SHORT_SERVICE"
+ + " did not stop within a timeout: " + sr.getComponentName();
+
+ final TimeoutRecord tr = TimeoutRecord.forShortFgsTimeout(reason);
+
+ // TODO(short-service): TODO Add SHORT_FGS_TIMEOUT to AnrLatencyTracker
+ tr.mLatencyTracker.waitingOnAMSLockStarted();
+ synchronized (mAm) {
+ tr.mLatencyTracker.waitingOnAMSLockEnded();
+
+ if (!sr.shouldTriggerShortFgsAnr()) {
+ return;
+ }
+
+ final String message = "Short FGS ANR'ed: " + sr;
+ if (DEBUG_SHORT_SERVICE) {
+ Slog.wtf(TAG_SERVICE, message);
+ } else {
+ Slog.e(TAG_SERVICE, message);
+ }
+ mAm.appNotResponding(sr.app, tr);
+ }
+ }
+
private void updateAllowlistManagerLocked(ProcessServiceRecord psr) {
psr.mAllowlistManager = false;
for (int i = psr.numberOfRunningServices() - 1; i >= 0; i--) {
@@ -2897,6 +3017,7 @@
}
}
+ // TODO(short-service): Hmm what is it? Should we stop the timeout here?
private void stopServiceAndUpdateAllowlistManagerLocked(ServiceRecord service) {
final ProcessServiceRecord psr = service.app.mServices;
psr.stopService(service);
@@ -4178,7 +4299,7 @@
/**
* Reschedule service restarts based on if the extra delays are enabled or not.
*
- * @param prevEnable The previous state of whether or not it's enabled.
+ * @param prevEnabled The previous state of whether or not it's enabled.
* @param curEnabled The current state of whether or not it's enabled.
* @param now The uptimeMillis
*/
@@ -4863,13 +4984,6 @@
Slog.i(TAG, "Bring down service for " + debugReason + " :" + r.toString());
}
- // TODO(short-service): Hmm, when the app stops a short-fgs, we should stop the timeout
- // here.
- // However we have a couple if's here and if these conditions are met, we stop here
- // without bringing down the service.
- // We need to make sure this can't be used (somehow) to keep having a short-FGS running
- // while having the timeout stopped.
-
if (isServiceNeededLocked(r, knowConn, hasConn)) {
return;
}
@@ -4886,6 +5000,13 @@
//Slog.i(TAG, "Bring down service:");
//r.dump(" ");
+ if (r.isShortFgs()) {
+ // FGS can be stopped without the app calling stopService() or stopSelf(),
+ // due to force-app-standby, or from Task Manager.
+ Slog.w(TAG_SERVICE, "Short FGS brought down without stopping: " + r);
+ maybeStopShortFgsTimeoutLocked(r);
+ }
+
// Report to all of the connections that the service is no longer
// available.
ArrayMap<IBinder, ArrayList<ConnectionRecord>> connections = r.getConnections();
diff --git a/services/core/java/com/android/server/am/ActivityManagerConstants.java b/services/core/java/com/android/server/am/ActivityManagerConstants.java
index 046403d..2d69667 100644
--- a/services/core/java/com/android/server/am/ActivityManagerConstants.java
+++ b/services/core/java/com/android/server/am/ActivityManagerConstants.java
@@ -954,7 +954,7 @@
static final long DEFAULT_SHORT_FGS_TIMEOUT_DURATION = 60_000;
/** @see #KEY_SHORT_FGS_TIMEOUT_DURATION */
- public static volatile long mShortFgsTimeoutDuration = DEFAULT_SHORT_FGS_TIMEOUT_DURATION;
+ public volatile long mShortFgsTimeoutDuration = DEFAULT_SHORT_FGS_TIMEOUT_DURATION;
/**
* If a "short service" doesn't finish within this after the timeout (
@@ -967,7 +967,7 @@
static final long DEFAULT_SHORT_FGS_PROC_STATE_EXTRA_WAIT_DURATION = 5_000;
/** @see #KEY_SHORT_FGS_PROC_STATE_EXTRA_WAIT_DURATION */
- public static volatile long mShortFgsProcStateExtraWaitDuration =
+ public volatile long mShortFgsProcStateExtraWaitDuration =
DEFAULT_SHORT_FGS_PROC_STATE_EXTRA_WAIT_DURATION;
/**
@@ -983,7 +983,7 @@
static final long DEFAULT_SHORT_FGS_ANR_EXTRA_WAIT_DURATION = 10_000;
/** @see #KEY_SHORT_FGS_ANR_EXTRA_WAIT_DURATION */
- public static volatile long mShortFgsAnrExtraWaitDuration =
+ public volatile long mShortFgsAnrExtraWaitDuration =
DEFAULT_SHORT_FGS_ANR_EXTRA_WAIT_DURATION;
private final OnPropertiesChangedListener mOnDeviceConfigChangedListener =
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index c779ea9..35b46c1 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -206,7 +206,7 @@
import android.app.SyncNotedAppOp;
import android.app.WaitResult;
import android.app.assist.ActivityId;
-import android.app.backup.BackupManager.OperationType;
+import android.app.backup.BackupAnnotations.BackupDestination;
import android.app.backup.IBackupManager;
import android.app.compat.CompatChanges;
import android.app.job.JobParameters;
@@ -1563,6 +1563,8 @@
static final int WAIT_FOR_CONTENT_PROVIDER_TIMEOUT_MSG = 73;
static final int DISPATCH_SENDING_BROADCAST_EVENT = 74;
static final int DISPATCH_BINDING_SERVICE_EVENT = 75;
+ static final int SERVICE_SHORT_FGS_TIMEOUT_MSG = 76;
+ static final int SERVICE_SHORT_FGS_ANR_TIMEOUT_MSG = 77;
static final int FIRST_BROADCAST_QUEUE_MSG = 200;
@@ -1897,6 +1899,12 @@
mBindServiceEventListeners.forEach(l ->
l.onBindingService((String) msg.obj, msg.arg1));
} break;
+ case SERVICE_SHORT_FGS_TIMEOUT_MSG: {
+ mServices.onShortFgsTimeout((ServiceRecord) msg.obj);
+ } break;
+ case SERVICE_SHORT_FGS_ANR_TIMEOUT_MSG: {
+ mServices.onShortFgsAnrTimeout((ServiceRecord) msg.obj);
+ } break;
}
}
}
@@ -5179,7 +5187,8 @@
PackageManager.NOTIFY_PACKAGE_USE_BACKUP);
try {
thread.scheduleCreateBackupAgent(backupTarget.appInfo,
- backupTarget.backupMode, backupTarget.userId, backupTarget.operationType);
+ backupTarget.backupMode, backupTarget.userId,
+ backupTarget.backupDestination);
} catch (Exception e) {
Slog.wtf(TAG, "Exception thrown creating backup agent in " + app, e);
badApp = true;
@@ -6141,6 +6150,7 @@
/**
* This can be called with or without the global lock held.
*/
+ @PermissionMethod(anyOf = true)
private void enforceCallingHasAtLeastOnePermission(String func, String... permissions) {
for (String permission : permissions) {
if (checkCallingPermission(permission) == PackageManager.PERMISSION_GRANTED) {
@@ -13129,7 +13139,7 @@
// instantiated. The backup agent will invoke backupAgentCreated() on the
// activity manager to announce its creation.
public boolean bindBackupAgent(String packageName, int backupMode, int targetUserId,
- @OperationType int operationType) {
+ @BackupDestination int backupDestination) {
if (DEBUG_BACKUP) {
Slog.v(TAG, "bindBackupAgent: app=" + packageName + " mode=" + backupMode
+ " targetUserId=" + targetUserId + " callingUid = " + Binder.getCallingUid()
@@ -13196,7 +13206,7 @@
+ app.packageName + ": " + e);
}
- BackupRecord r = new BackupRecord(app, backupMode, targetUserId, operationType);
+ BackupRecord r = new BackupRecord(app, backupMode, targetUserId, backupDestination);
ComponentName hostingName =
(backupMode == ApplicationThreadConstants.BACKUP_MODE_INCREMENTAL)
? new ComponentName(app.packageName, app.backupAgentName)
@@ -13238,7 +13248,7 @@
if (DEBUG_BACKUP) Slog.v(TAG_BACKUP, "Agent proc already running: " + proc);
try {
thread.scheduleCreateBackupAgent(app, backupMode, targetUserId,
- operationType);
+ backupDestination);
} catch (RemoteException e) {
// Will time out on the backup manager side
}
diff --git a/services/core/java/com/android/server/am/BackupRecord.java b/services/core/java/com/android/server/am/BackupRecord.java
index d419856..0b056d7 100644
--- a/services/core/java/com/android/server/am/BackupRecord.java
+++ b/services/core/java/com/android/server/am/BackupRecord.java
@@ -16,8 +16,7 @@
package com.android.server.am;
-import android.app.backup.BackupManager;
-import android.app.backup.BackupManager.OperationType;
+import android.app.backup.BackupAnnotations.BackupDestination;
import android.content.pm.ApplicationInfo;
/** @hide */
@@ -32,16 +31,16 @@
final ApplicationInfo appInfo; // information about BackupAgent's app
final int userId; // user for which backup is performed
final int backupMode; // full backup / incremental / restore
- @OperationType final int operationType; // see BackupManager#OperationType
+ @BackupDestination final int backupDestination; // see BackupAnnotations#BackupDestination
ProcessRecord app; // where this agent is running or null
// ----- Implementation -----
- BackupRecord(ApplicationInfo _appInfo, int _backupMode, int _userId, int _operationType) {
+ BackupRecord(ApplicationInfo _appInfo, int _backupMode, int _userId, int _backupDestination) {
appInfo = _appInfo;
backupMode = _backupMode;
userId = _userId;
- operationType = _operationType;
+ backupDestination = _backupDestination;
}
public String toString() {
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index 8082e45..66a8bab 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -1818,21 +1818,31 @@
newAdj = PERCEPTIBLE_APP_ADJ;
newProcState = PROCESS_STATE_IMPORTANT_FOREGROUND;
- } else if (psr.hasForegroundServices() && !psr.hasNonShortForegroundServices()) {
- // For short FGS.
- adjType = "fg-service-short";
- // We use MEDIUM_APP_ADJ + 1 so we can tell apart EJ (which uses MEDIUM_APP_ADJ + 1)
- // from short-FGS.
- // (We use +1 and +2, not +0 and +1, to be consistent with the following
- // RECENT_FOREGROUND_APP_ADJ tweak)
- newAdj = PERCEPTIBLE_MEDIUM_APP_ADJ + 1;
+ } else if (psr.hasForegroundServices()) {
+ // If we get here, hasNonShortForegroundServices() must be false.
- // Short-FGS gets a below-BFGS procstate, so it can't start another FGS from it.
- newProcState = PROCESS_STATE_IMPORTANT_FOREGROUND;
+ // TODO(short-service): Proactively run OomAjudster when the grace period finish.
+ if (psr.areAllShortForegroundServicesProcstateTimedOut(now)) {
+ // All the short-FGSes within this process are timed out. Don't promote to FGS.
+ // TODO(short-service): Should we set some unique oom-adj to make it detectable,
+ // in a long trace?
+ } else {
+ // For short FGS.
+ adjType = "fg-service-short";
+ // We use MEDIUM_APP_ADJ + 1 so we can tell apart EJ
+ // (which uses MEDIUM_APP_ADJ + 1)
+ // from short-FGS.
+ // (We use +1 and +2, not +0 and +1, to be consistent with the following
+ // RECENT_FOREGROUND_APP_ADJ tweak)
+ newAdj = PERCEPTIBLE_MEDIUM_APP_ADJ + 1;
- // Same as EJ, we explicitly grant network access to short FGS,
- // even when battery saver or data saver is enabled.
- capabilityFromFGS |= PROCESS_CAPABILITY_NETWORK;
+ // Short-FGS gets a below-BFGS procstate, so it can't start another FGS from it.
+ newProcState = PROCESS_STATE_IMPORTANT_FOREGROUND;
+
+ // Same as EJ, we explicitly grant network access to short FGS,
+ // even when battery saver or data saver is enabled.
+ capabilityFromFGS |= PROCESS_CAPABILITY_NETWORK;
+ }
}
if (adjType != null) {
diff --git a/services/core/java/com/android/server/am/ProcessServiceRecord.java b/services/core/java/com/android/server/am/ProcessServiceRecord.java
index 13264db..df442e8 100644
--- a/services/core/java/com/android/server/am/ProcessServiceRecord.java
+++ b/services/core/java/com/android/server/am/ProcessServiceRecord.java
@@ -175,6 +175,9 @@
}
}
+ /**
+ * @return true if this process has any foreground services (even timed-out short-FGS)
+ */
boolean hasForegroundServices() {
return mHasForegroundServices;
}
@@ -224,6 +227,33 @@
return mFgServiceTypes != ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE;
}
+ /**
+ * @return if this process:
+ * - has at least one short-FGS
+ * - has no other types of FGS
+ * - and all the short-FGSes are procstate-timed out.
+ */
+ boolean areAllShortForegroundServicesProcstateTimedOut(long nowUptime) {
+ if (!mHasForegroundServices) { // Process has no FGS?
+ return false;
+ }
+ if (hasNonShortForegroundServices()) { // Any non-short FGS running?
+ return false;
+ }
+ // Now we need to look at all short-FGS within the process and see if all of them are
+ // procstate-timed-out or not.
+ for (int i = mServices.size() - 1; i >= 0; i--) {
+ final ServiceRecord sr = mServices.valueAt(i);
+ if (!sr.isShortFgs() || !sr.hasShortFgsInfo()) {
+ continue;
+ }
+ if (sr.getShortFgsInfo().getProcStateDemoteTime() >= nowUptime) {
+ return false;
+ }
+ }
+ return true;
+ }
+
int getReportedForegroundServiceTypes() {
return mRepFgServiceTypes;
}
diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java
index 0468152..547c20b 100644
--- a/services/core/java/com/android/server/am/ServiceRecord.java
+++ b/services/core/java/com/android/server/am/ServiceRecord.java
@@ -315,6 +315,87 @@
final ArrayList<StartItem> pendingStarts = new ArrayList<StartItem>();
// start() arguments that haven't yet been delivered.
+ /**
+ * Information specific to "SHORT_SERVICE" FGS.
+ */
+ class ShortFgsInfo {
+ /** Time FGS started */
+ private final long mStartTime;
+
+ /**
+ * Copied from {@link #mStartForegroundCount}. If this is different from the parent's,
+ * that means this instance is stale.
+ */
+ private int mStartForegroundCount;
+
+ /** Service's "start ID" when this short-service started. */
+ private int mStartId;
+
+ ShortFgsInfo(long startTime) {
+ mStartTime = startTime;
+ update();
+ }
+
+ /**
+ * Update {@link #mStartForegroundCount} and {@link #mStartId}.
+ * (but not {@link #mStartTime})
+ */
+ public void update() {
+ this.mStartForegroundCount = ServiceRecord.this.mStartForegroundCount;
+ this.mStartId = getLastStartId();
+ }
+
+ long getStartTime() {
+ return mStartTime;
+ }
+
+ int getStartForegroundCount() {
+ return mStartForegroundCount;
+ }
+
+ int getStartId() {
+ return mStartId;
+ }
+
+ /**
+ * @return whether this {@link ShortFgsInfo} is still "current" or not -- i.e.
+ * it's "start foreground count" is the same as that of the ServiceRecord's.
+ *
+ * Note, we do _not_ check the "start id" here, because the start id increments if the
+ * app calls startService() or startForegroundService() on the same service,
+ * but that will _not_ update the ShortFgsInfo, and will not extend the timeout.
+ *
+ * TODO(short-service): Make sure, calling startService will not extend or remove the
+ * timeout, in CTS.
+ */
+ boolean isCurrent() {
+ return this.mStartForegroundCount == ServiceRecord.this.mStartForegroundCount;
+ }
+
+ /** Time when Service.onTimeout() should be called */
+ long getTimeoutTime() {
+ return mStartTime + ams.mConstants.mShortFgsTimeoutDuration;
+ }
+
+ /** Time when the procstate should be lowered. */
+ long getProcStateDemoteTime() {
+ return mStartTime + ams.mConstants.mShortFgsTimeoutDuration
+ + ams.mConstants.mShortFgsProcStateExtraWaitDuration;
+ }
+
+ /** Time when the app should be declared ANR. */
+ long getAnrTime() {
+ return mStartTime + ams.mConstants.mShortFgsTimeoutDuration
+ + ams.mConstants.mShortFgsAnrExtraWaitDuration;
+ }
+ }
+
+ /**
+ * Keep track of short-fgs specific information. This field gets cleared when the timeout
+ * stops.
+ */
+ private ShortFgsInfo mShortFgsInfo;
+
void dumpStartList(PrintWriter pw, String prefix, List<StartItem> list, long now) {
final int N = list.size();
for (int i=0; i<N; i++) {
@@ -456,6 +537,8 @@
}
}
proto.end(token);
+
+ // TODO(short-service) Add FGS info
}
void dump(PrintWriter pw, String prefix) {
@@ -508,8 +591,25 @@
}
if (isForeground || foregroundId != 0) {
pw.print(prefix); pw.print("isForeground="); pw.print(isForeground);
- pw.print(" foregroundId="); pw.print(foregroundId);
- pw.print(" foregroundNoti="); pw.println(foregroundNoti);
+ pw.print(" foregroundId="); pw.print(foregroundId);
+ pw.printf(" types=%08X", foregroundServiceType);
+ pw.print(" foregroundNoti="); pw.println(foregroundNoti);
+
+ if (isShortFgs() && mShortFgsInfo != null) {
+ pw.print(prefix); pw.print("isShortFgs=true");
+ pw.print(" startId="); pw.print(mShortFgsInfo.getStartId());
+ pw.print(" startForegroundCount=");
+ pw.print(mShortFgsInfo.getStartForegroundCount());
+ pw.print(" startTime=");
+ TimeUtils.formatDuration(mShortFgsInfo.getStartTime(), now, pw);
+ pw.print(" timeout=");
+ TimeUtils.formatDuration(mShortFgsInfo.getTimeoutTime(), now, pw);
+ pw.print(" demoteTime=");
+ TimeUtils.formatDuration(mShortFgsInfo.getProcStateDemoteTime(), now, pw);
+ pw.print(" anrTime=");
+ TimeUtils.formatDuration(mShortFgsInfo.getAnrTime(), now, pw);
+ pw.println();
+ }
}
if (mIsFgsDelegate) {
pw.print(prefix); pw.print("isFgsDelegate="); pw.println(mIsFgsDelegate);
@@ -590,6 +690,32 @@
}
}
+ /** Used only for tests */
+ private ServiceRecord(ActivityManagerService ams) {
+ this.ams = ams;
+ name = null;
+ instanceName = null;
+ shortInstanceName = null;
+ definingPackageName = null;
+ definingUid = 0;
+ intent = null;
+ serviceInfo = null;
+ userId = 0;
+ packageName = null;
+ processName = null;
+ permission = null;
+ exported = false;
+ restarter = null;
+ createRealTime = 0;
+ isSdkSandbox = false;
+ sdkSandboxClientAppUid = 0;
+ sdkSandboxClientAppPackage = null;
+ }
+
+ public static ServiceRecord newEmptyInstanceForTest(ActivityManagerService ams) {
+ return new ServiceRecord(ams);
+ }
+
ServiceRecord(ActivityManagerService ams, ComponentName name,
ComponentName instanceName, String definingPackageName, int definingUid,
Intent.FilterComparison intent, ServiceInfo sInfo, boolean callerIsFg,
@@ -1238,4 +1364,66 @@
return isForeground
&& (foregroundServiceType == ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE);
}
+
+ public ShortFgsInfo getShortFgsInfo() {
+ return isShortFgs() ? mShortFgsInfo : null;
+ }
+
+ /**
+ * Call it when a short FGS starts.
+ */
+ public void setShortFgsInfo(long uptimeNow) {
+ this.mShortFgsInfo = new ShortFgsInfo(uptimeNow);
+ }
+
+ /** @return whether {@link #mShortFgsInfo} is set or not. */
+ public boolean hasShortFgsInfo() {
+ return mShortFgsInfo != null;
+ }
+
+ /**
+ * Call it when a short FGS stops.
+ */
+ public void clearShortFgsInfo() {
+ this.mShortFgsInfo = null;
+ }
+
+ /**
+ * @return true if it's a short FGS that's still up and running, and should be timed out.
+ */
+ public boolean shouldTriggerShortFgsTimeout() {
+ if (!isAppAlive()) {
+ return false;
+ }
+ if (!this.startRequested || !isShortFgs() || mShortFgsInfo == null
+ || !mShortFgsInfo.isCurrent()) {
+ return false;
+ }
+ return mShortFgsInfo.getTimeoutTime() < SystemClock.uptimeMillis();
+ }
+
+ /**
+ * @return true if it's a short FGS that's still up and running, and should be declared
+ * an ANR.
+ */
+ public boolean shouldTriggerShortFgsAnr() {
+ if (!isAppAlive()) {
+ return false;
+ }
+ if (!this.startRequested || !isShortFgs() || mShortFgsInfo == null
+ || !mShortFgsInfo.isCurrent()) {
+ return false;
+ }
+ return mShortFgsInfo.getAnrTime() < SystemClock.uptimeMillis();
+ }
+
+ private boolean isAppAlive() {
+ if (app == null) {
+ return false;
+ }
+ if (app.getThread() == null || app.isKilled() || app.isKilledByAm()) {
+ return false;
+ }
+ return true;
+ }
}
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 3231240..ae929c4 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -7180,6 +7180,24 @@
state == CONNECTION_STATE_CONNECTED ? "connected" : "disconnected")
.record();
mDeviceBroker.setWiredDeviceConnectionState(attributes, state, caller);
+ // The Dynamic Soundbar mode feature introduces dynamic presence for an HDMI Audio System
+ // Client. For example, the device can start with the Audio System Client unavailable.
+ // When the feature is activated the client becomes available, therefore Audio Service
+ // requests a new HDMI Audio System Client instance when the ARC status is changed.
+ if (attributes.getInternalType() == AudioSystem.DEVICE_IN_HDMI_ARC) {
+ updateHdmiAudioSystemClient();
+ }
+ }
+
+ /**
+ * Replace the current HDMI Audio System Client.
+ * See {@link #setWiredDeviceConnectionState(AudioDeviceAttributes, int, String)}.
+ */
+ private void updateHdmiAudioSystemClient() {
+ Slog.d(TAG, "Hdmi Audio System Client is updated");
+ synchronized (mHdmiClientLock) {
+ mHdmiAudioSystemClient = mHdmiManager.getAudioSystemClient();
+ }
}
/** @see AudioManager#setTestDeviceConnectionState(AudioDeviceAttributes, boolean) */
diff --git a/services/core/java/com/android/server/backup/SystemBackupAgent.java b/services/core/java/com/android/server/backup/SystemBackupAgent.java
index d39d2d1..1b20e43 100644
--- a/services/core/java/com/android/server/backup/SystemBackupAgent.java
+++ b/services/core/java/com/android/server/backup/SystemBackupAgent.java
@@ -18,9 +18,9 @@
import android.app.IWallpaperManager;
import android.app.backup.BackupAgentHelper;
+import android.app.backup.BackupAnnotations.BackupDestination;
import android.app.backup.BackupDataInput;
import android.app.backup.BackupHelper;
-import android.app.backup.BackupManager;
import android.app.backup.FullBackup;
import android.app.backup.FullBackupDataOutput;
import android.app.backup.WallpaperBackupHelper;
@@ -89,8 +89,8 @@
private int mUserId = UserHandle.USER_SYSTEM;
@Override
- public void onCreate(UserHandle user, @BackupManager.OperationType int operationType) {
- super.onCreate(user, operationType);
+ public void onCreate(UserHandle user, @BackupDestination int backupDestination) {
+ super.onCreate(user, backupDestination);
mUserId = user.getIdentifier();
diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java
index 9ded42a..1f58a1c 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController2.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController2.java
@@ -299,7 +299,6 @@
private boolean mAppliedAutoBrightness;
private boolean mAppliedDimming;
private boolean mAppliedLowPower;
- private boolean mAppliedTemporaryBrightness;
private boolean mAppliedTemporaryAutoBrightnessAdjustment;
private boolean mAppliedBrightnessBoost;
private boolean mAppliedThrottling;
@@ -395,11 +394,6 @@
// behalf of the user.
private float mCurrentScreenBrightnessSetting;
- // The temporary screen brightness. Typically set when a user is interacting with the
- // brightness slider but hasn't settled on a choice yet. Set to
- // PowerManager.BRIGHTNESS_INVALID_FLOAT when there's no temporary brightness set.
- private float mTemporaryScreenBrightness;
-
// The current screen brightness while in VR mode.
private float mScreenBrightnessForVr;
@@ -566,7 +560,6 @@
mCurrentScreenBrightnessSetting = getScreenBrightnessSetting();
mScreenBrightnessForVr = getScreenBrightnessForVrSetting();
mAutoBrightnessAdjustment = getAutoBrightnessAdjustmentSetting();
- mTemporaryScreenBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT;
mPendingScreenBrightnessSetting = PowerManager.BRIGHTNESS_INVALID_FLOAT;
mTemporaryAutoBrightnessAdjustment = PowerManager.BRIGHTNESS_INVALID_FLOAT;
mPendingAutoBrightnessAdjustment = PowerManager.BRIGHTNESS_INVALID_FLOAT;
@@ -1218,16 +1211,6 @@
final boolean userSetBrightnessChanged = updateUserSetScreenBrightness();
- // Use the temporary screen brightness if there isn't an override, either from
- // WindowManager or based on the display state.
- if (isValidBrightnessValue(mTemporaryScreenBrightness)) {
- brightnessState = mTemporaryScreenBrightness;
- mAppliedTemporaryBrightness = true;
- mBrightnessReasonTemp.setReason(BrightnessReason.REASON_TEMPORARY);
- } else {
- mAppliedTemporaryBrightness = false;
- }
-
final boolean autoBrightnessAdjustmentChanged = updateAutoBrightnessAdjustment();
// Use the autobrightness adjustment override if set.
@@ -1414,7 +1397,8 @@
// Skip the animation when the screen is off or suspended or transition to/from VR.
boolean brightnessAdjusted = false;
final boolean brightnessIsTemporary =
- mAppliedTemporaryBrightness || mAppliedTemporaryAutoBrightnessAdjustment;
+ (mBrightnessReason.getReason() == BrightnessReason.REASON_TEMPORARY)
+ || mAppliedTemporaryAutoBrightnessAdjustment;
if (!mPendingScreenOff) {
if (mSkipScreenOnBrightnessRamp) {
if (state == Display.STATE_ON) {
@@ -2202,13 +2186,15 @@
}
if (mCurrentScreenBrightnessSetting == mPendingScreenBrightnessSetting) {
mPendingScreenBrightnessSetting = PowerManager.BRIGHTNESS_INVALID_FLOAT;
- mTemporaryScreenBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT;
+ mDisplayBrightnessController
+ .setTemporaryBrightness(PowerManager.BRIGHTNESS_INVALID_FLOAT);
return false;
}
setCurrentScreenBrightness(mPendingScreenBrightnessSetting);
mLastUserSetScreenBrightness = mPendingScreenBrightnessSetting;
mPendingScreenBrightnessSetting = PowerManager.BRIGHTNESS_INVALID_FLOAT;
- mTemporaryScreenBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT;
+ mDisplayBrightnessController
+ .setTemporaryBrightness(PowerManager.BRIGHTNESS_INVALID_FLOAT);
return true;
}
@@ -2291,7 +2277,6 @@
pw.println(" mLastUserSetScreenBrightness=" + mLastUserSetScreenBrightness);
pw.println(" mPendingScreenBrightnessSetting="
+ mPendingScreenBrightnessSetting);
- pw.println(" mTemporaryScreenBrightness=" + mTemporaryScreenBrightness);
pw.println(" mAutoBrightnessAdjustment=" + mAutoBrightnessAdjustment);
pw.println(" mBrightnessReason=" + mBrightnessReason);
pw.println(" mTemporaryAutoBrightnessAdjustment=" + mTemporaryAutoBrightnessAdjustment);
@@ -2301,7 +2286,6 @@
pw.println(" mAppliedDimming=" + mAppliedDimming);
pw.println(" mAppliedLowPower=" + mAppliedLowPower);
pw.println(" mAppliedThrottling=" + mAppliedThrottling);
- pw.println(" mAppliedTemporaryBrightness=" + mAppliedTemporaryBrightness);
pw.println(" mAppliedTemporaryAutoBrightnessAdjustment="
+ mAppliedTemporaryAutoBrightnessAdjustment);
pw.println(" mAppliedBrightnessBoost=" + mAppliedBrightnessBoost);
@@ -2541,7 +2525,8 @@
case MSG_SET_TEMPORARY_BRIGHTNESS:
// TODO: Should we have a a timeout for the temporary brightness?
- mTemporaryScreenBrightness = Float.intBitsToFloat(msg.arg1);
+ mDisplayBrightnessController
+ .setTemporaryBrightness(Float.intBitsToFloat(msg.arg1));
updatePowerState();
break;
diff --git a/services/core/java/com/android/server/display/brightness/BrightnessUtils.java b/services/core/java/com/android/server/display/brightness/BrightnessUtils.java
index d62b1ee..fd4e296 100644
--- a/services/core/java/com/android/server/display/brightness/BrightnessUtils.java
+++ b/services/core/java/com/android/server/display/brightness/BrightnessUtils.java
@@ -28,7 +28,7 @@
* Checks whether the brightness is within the valid brightness range, not including off.
*/
public static boolean isValidBrightnessValue(float brightness) {
- return brightness >= PowerManager.BRIGHTNESS_MIN
+ return !Float.isNaN(brightness) && brightness >= PowerManager.BRIGHTNESS_MIN
&& brightness <= PowerManager.BRIGHTNESS_MAX;
}
diff --git a/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java b/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java
index 80b5e65..bdc8d9d 100644
--- a/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java
+++ b/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java
@@ -68,6 +68,21 @@
}
/**
+ * Sets the temporary brightness
+ */
+ public void setTemporaryBrightness(Float temporaryBrightness) {
+ mDisplayBrightnessStrategySelector.getTemporaryDisplayBrightnessStrategy()
+ .setTemporaryScreenBrightness(temporaryBrightness);
+ }
+
+ /**
+ * Returns the current selected DisplayBrightnessStrategy
+ */
+ public DisplayBrightnessStrategy getCurrentDisplayBrightnessStrategy() {
+ return mDisplayBrightnessStrategy;
+ }
+
+ /**
* Returns a boolean flag indicating if the light sensor is to be used to decide the screen
* brightness when dozing
*/
diff --git a/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java b/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java
index b83b13b..4759b7d 100644
--- a/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java
+++ b/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java
@@ -19,6 +19,7 @@
import android.annotation.NonNull;
import android.content.Context;
import android.hardware.display.DisplayManagerInternal;
+import android.util.IndentingPrintWriter;
import android.util.Slog;
import android.view.Display;
@@ -29,6 +30,7 @@
import com.android.server.display.brightness.strategy.InvalidBrightnessStrategy;
import com.android.server.display.brightness.strategy.OverrideBrightnessStrategy;
import com.android.server.display.brightness.strategy.ScreenOffBrightnessStrategy;
+import com.android.server.display.brightness.strategy.TemporaryBrightnessStrategy;
import java.io.PrintWriter;
@@ -48,7 +50,9 @@
// The brightness strategy used to manage the brightness state when the request state is
// invalid.
private final OverrideBrightnessStrategy mOverrideBrightnessStrategy;
- // The brightness strategy used to manage the brightness state request is invalid.
+ // The brightness strategy used to manage the brightness state in temporary state
+ private final TemporaryBrightnessStrategy mTemporaryBrightnessStrategy;
+ // The brightness strategy used to manage the brightness state when the request is invalid.
private final InvalidBrightnessStrategy mInvalidBrightnessStrategy;
// We take note of the old brightness strategy so that we can know when the strategy changes.
@@ -67,6 +71,7 @@
mDozeBrightnessStrategy = injector.getDozeBrightnessStrategy();
mScreenOffBrightnessStrategy = injector.getScreenOffBrightnessStrategy();
mOverrideBrightnessStrategy = injector.getOverrideBrightnessStrategy();
+ mTemporaryBrightnessStrategy = injector.getTemporaryBrightnessStrategy();
mInvalidBrightnessStrategy = injector.getInvalidBrightnessStrategy();
mAllowAutoBrightnessWhileDozingConfig = context.getResources().getBoolean(
R.bool.config_allowAutoBrightnessWhileDozing);
@@ -89,6 +94,9 @@
} else if (BrightnessUtils
.isValidBrightnessValue(displayPowerRequest.screenBrightnessOverride)) {
displayBrightnessStrategy = mOverrideBrightnessStrategy;
+ } else if (BrightnessUtils.isValidBrightnessValue(
+ mTemporaryBrightnessStrategy.getTemporaryScreenBrightness())) {
+ displayBrightnessStrategy = mTemporaryBrightnessStrategy;
}
if (!mOldBrightnessStrategyName.equals(displayBrightnessStrategy.getName())) {
@@ -101,6 +109,10 @@
return displayBrightnessStrategy;
}
+ public TemporaryBrightnessStrategy getTemporaryDisplayBrightnessStrategy() {
+ return mTemporaryBrightnessStrategy;
+ }
+
/**
* Returns a boolean flag indicating if the light sensor is to be used to decide the screen
* brightness when dozing
@@ -120,6 +132,8 @@
writer.println(
" mAllowAutoBrightnessWhileDozingConfig= "
+ mAllowAutoBrightnessWhileDozingConfig);
+ IndentingPrintWriter ipw = new IndentingPrintWriter(writer, " ");
+ mTemporaryBrightnessStrategy.dump(ipw);
}
/**
@@ -152,6 +166,10 @@
return new OverrideBrightnessStrategy();
}
+ TemporaryBrightnessStrategy getTemporaryBrightnessStrategy() {
+ return new TemporaryBrightnessStrategy();
+ }
+
InvalidBrightnessStrategy getInvalidBrightnessStrategy() {
return new InvalidBrightnessStrategy();
}
diff --git a/services/core/java/com/android/server/display/brightness/strategy/TemporaryBrightnessStrategy.java b/services/core/java/com/android/server/display/brightness/strategy/TemporaryBrightnessStrategy.java
new file mode 100644
index 0000000..f8063f3
--- /dev/null
+++ b/services/core/java/com/android/server/display/brightness/strategy/TemporaryBrightnessStrategy.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display.brightness.strategy;
+
+import android.hardware.display.DisplayManagerInternal;
+import android.os.PowerManager;
+
+import com.android.server.display.DisplayBrightnessState;
+import com.android.server.display.brightness.BrightnessReason;
+import com.android.server.display.brightness.BrightnessUtils;
+
+import java.io.PrintWriter;
+
+/**
+ * Manages the brightness of the display when the system brightness is temporary
+ */
+public class TemporaryBrightnessStrategy implements DisplayBrightnessStrategy {
+ // The temporary screen brightness. Typically set when a user is interacting with the
+ // brightness slider but hasn't settled on a choice yet. Set to
+ // PowerManager.BRIGHTNESS_INVALID_FLOAT when there's no temporary brightness set.
+ private float mTemporaryScreenBrightness;
+
+ public TemporaryBrightnessStrategy() {
+ mTemporaryScreenBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT;
+ }
+
+ // Use the temporary screen brightness if there isn't an override, either from
+ // WindowManager or based on the display state.
+ @Override
+ public DisplayBrightnessState updateBrightness(
+ DisplayManagerInternal.DisplayPowerRequest displayPowerRequest) {
+ // Todo(brup): Introduce a validator class and add validations before setting the brightness
+ DisplayBrightnessState displayBrightnessState =
+ BrightnessUtils.constructDisplayBrightnessState(BrightnessReason.REASON_TEMPORARY,
+ mTemporaryScreenBrightness,
+ mTemporaryScreenBrightness);
+ mTemporaryScreenBrightness = Float.NaN;
+ return displayBrightnessState;
+ }
+
+ @Override
+ public String getName() {
+ return "TemporaryBrightnessStrategy";
+ }
+
+ public float getTemporaryScreenBrightness() {
+ return mTemporaryScreenBrightness;
+ }
+
+ public void setTemporaryScreenBrightness(float temporaryScreenBrightness) {
+ mTemporaryScreenBrightness = temporaryScreenBrightness;
+ }
+
+ /**
+ * Dumps the state of this class.
+ */
+ public void dump(PrintWriter writer) {
+ writer.println("TemporaryBrightnessStrategy:");
+ writer.println(" mTemporaryScreenBrightness:" + mTemporaryScreenBrightness);
+ }
+}
diff --git a/services/core/java/com/android/server/hdmi/ArcTerminationActionFromAvr.java b/services/core/java/com/android/server/hdmi/ArcTerminationActionFromAvr.java
index 049a339..4855be6 100644
--- a/services/core/java/com/android/server/hdmi/ArcTerminationActionFromAvr.java
+++ b/services/core/java/com/android/server/hdmi/ArcTerminationActionFromAvr.java
@@ -27,7 +27,7 @@
private static final int STATE_ARC_TERMINATED = 2;
// the required maximum response time specified in CEC 9.2
- private static final int TIMEOUT_MS = 1000;
+ public static final int TIMEOUT_MS = 1000;
ArcTerminationActionFromAvr(HdmiCecLocalDevice source) {
super(source);
@@ -85,6 +85,8 @@
}
private void handleTerminateArcTimeout() {
+ // Disable ARC if TV didn't respond with <Report ARC Terminated> in time.
+ audioSystem().setArcStatus(false);
HdmiLogger.debug("handleTerminateArcTimeout");
finish();
}
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecConfig.java b/services/core/java/com/android/server/hdmi/HdmiCecConfig.java
index 827aafa..6925507 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecConfig.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecConfig.java
@@ -318,6 +318,16 @@
R.bool.config_cecRoutingControlDisabled_allowed,
R.bool.config_cecRoutingControlDisabled_default);
+ Setting soundbarMode = registerSetting(
+ HdmiControlManager.CEC_SETTING_NAME_SOUNDBAR_MODE,
+ R.bool.config_cecSoundbarMode_userConfigurable);
+ soundbarMode.registerValue(HdmiControlManager.SOUNDBAR_MODE_ENABLED,
+ R.bool.config_cecSoundbarModeEnabled_allowed,
+ R.bool.config_cecSoundbarModeEnabled_default);
+ soundbarMode.registerValue(HdmiControlManager.SOUNDBAR_MODE_DISABLED,
+ R.bool.config_cecSoundbarModeDisabled_allowed,
+ R.bool.config_cecSoundbarModeDisabled_default);
+
Setting powerControlMode = registerSetting(
HdmiControlManager.CEC_SETTING_NAME_POWER_CONTROL_MODE,
R.bool.config_cecPowerControlMode_userConfigurable);
@@ -714,6 +724,8 @@
return STORAGE_SHARED_PREFS;
case HdmiControlManager.CEC_SETTING_NAME_ROUTING_CONTROL:
return STORAGE_SHARED_PREFS;
+ case HdmiControlManager.CEC_SETTING_NAME_SOUNDBAR_MODE:
+ return STORAGE_SHARED_PREFS;
case HdmiControlManager.CEC_SETTING_NAME_POWER_CONTROL_MODE:
return STORAGE_SHARED_PREFS;
case HdmiControlManager.CEC_SETTING_NAME_VOLUME_CONTROL_MODE:
@@ -789,6 +801,8 @@
return setting.getName();
case HdmiControlManager.CEC_SETTING_NAME_ROUTING_CONTROL:
return setting.getName();
+ case HdmiControlManager.CEC_SETTING_NAME_SOUNDBAR_MODE:
+ return setting.getName();
case HdmiControlManager.CEC_SETTING_NAME_POWER_CONTROL_MODE:
return setting.getName();
case HdmiControlManager.CEC_SETTING_NAME_VOLUME_CONTROL_MODE:
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
index 4542c39..b4d7fb9 100755
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
@@ -1359,7 +1359,8 @@
List<SendKeyAction> action = getActions(SendKeyAction.class);
int logicalAddress = findAudioReceiverAddress();
if (logicalAddress == Constants.ADDR_INVALID
- || logicalAddress == mDeviceInfo.getLogicalAddress()) {
+ || mService.getAllCecLocalDevices().stream().anyMatch(
+ device -> device.getDeviceInfo().getLogicalAddress() == logicalAddress)) {
// Don't send key event to invalid device or itself.
Slog.w(
TAG,
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java
index 728ed44..ccaa9255d 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java
@@ -226,6 +226,8 @@
@Override
@ServiceThreadOnly
protected void disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback) {
+ terminateAudioReturnChannel();
+
super.disableDevice(initiatedByCec, callback);
assertRunOnServiceThread();
mService.unregisterTvInputCallback(mTvInputCallback);
@@ -884,7 +886,7 @@
private void notifyArcStatusToAudioService(boolean enabled) {
// Note that we don't set any name to ARC.
mService.getAudioManager()
- .setWiredDeviceConnectionState(AudioSystem.DEVICE_IN_HDMI, enabled ? 1 : 0, "", "");
+ .setWiredDeviceConnectionState(AudioSystem.DEVICE_IN_HDMI_ARC, enabled ? 1 : 0, "", "");
}
void reportAudioStatus(int source) {
@@ -1088,6 +1090,16 @@
}
}
+ private void terminateAudioReturnChannel() {
+ // remove pending initiation actions
+ removeAction(ArcInitiationActionFromAvr.class);
+ if (!isArcEnabled()
+ || !mService.readBooleanSystemProperty(Constants.PROPERTY_ARC_SUPPORT, true)) {
+ return;
+ }
+ addAndStartAction(new ArcTerminationActionFromAvr(this));
+ }
+
/** Reports if System Audio Mode is supported by the connected TV */
interface TvSystemAudioModeSupportedCallback {
@@ -1312,6 +1324,9 @@
@ServiceThreadOnly
private void launchDeviceDiscovery() {
assertRunOnServiceThread();
+ if (mService.isDeviceDiscoveryHandledByPlayback()) {
+ return;
+ }
if (hasAction(DeviceDiscoveryAction.class)) {
Slog.i(TAG, "Device Discovery Action is in progress. Restarting.");
removeAction(DeviceDiscoveryAction.class);
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index 3b06786..43cd71a 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -19,6 +19,8 @@
import static android.hardware.hdmi.HdmiControlManager.DEVICE_EVENT_ADD_DEVICE;
import static android.hardware.hdmi.HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE;
import static android.hardware.hdmi.HdmiControlManager.HDMI_CEC_CONTROL_ENABLED;
+import static android.hardware.hdmi.HdmiControlManager.SOUNDBAR_MODE_DISABLED;
+import static android.hardware.hdmi.HdmiControlManager.SOUNDBAR_MODE_ENABLED;
import static com.android.server.hdmi.Constants.ADDR_UNREGISTERED;
import static com.android.server.hdmi.Constants.DISABLED;
@@ -188,6 +190,7 @@
static final int INITIATED_BY_SCREEN_ON = 2;
static final int INITIATED_BY_WAKE_UP_MESSAGE = 3;
static final int INITIATED_BY_HOTPLUG = 4;
+ static final int INITIATED_BY_SOUNDBAR_MODE = 5;
// The reason code representing the intent action that drives the standby
// procedure. The procedure starts either by Intent.ACTION_SCREEN_OFF or
@@ -693,6 +696,14 @@
}
}
}, mServiceThreadExecutor);
+ mHdmiCecConfig.registerChangeListener(HdmiControlManager.CEC_SETTING_NAME_SOUNDBAR_MODE,
+ new HdmiCecConfig.SettingChangeListener() {
+ @Override
+ public void onChange(String setting) {
+ setSoundbarMode(mHdmiCecConfig.getIntValue(
+ HdmiControlManager.CEC_SETTING_NAME_SOUNDBAR_MODE));
+ }
+ }, mServiceThreadExecutor);
mHdmiCecConfig.registerChangeListener(
HdmiControlManager.CEC_SETTING_NAME_SYSTEM_AUDIO_CONTROL,
new HdmiCecConfig.SettingChangeListener() {
@@ -770,6 +781,11 @@
}
@VisibleForTesting
+ void setAudioManager(AudioManager audioManager) {
+ mAudioManager = audioManager;
+ }
+
+ @VisibleForTesting
void setCecController(HdmiCecController cecController) {
mCecController = cecController;
}
@@ -847,6 +863,47 @@
}
/**
+ * Triggers the address allocation that states the presence of a local device audio system in
+ * the network.
+ */
+ @VisibleForTesting
+ public void setSoundbarMode(final int settingValue) {
+ HdmiCecLocalDevicePlayback playback = playback();
+ HdmiCecLocalDeviceAudioSystem audioSystem = audioSystem();
+ if (playback == null) {
+ Slog.w(TAG, "Device type not compatible to change soundbar mode.");
+ return;
+ }
+ if (!SystemProperties.getBoolean(Constants.PROPERTY_ARC_SUPPORT, true)) {
+ Slog.w(TAG, "Device type doesn't support ARC.");
+ return;
+ }
+ if (settingValue == SOUNDBAR_MODE_DISABLED && audioSystem != null) {
+ if (audioSystem.isArcEnabled()) {
+ audioSystem.addAndStartAction(new ArcTerminationActionFromAvr(audioSystem));
+ }
+ if (isSystemAudioActivated()) {
+ audioSystem.terminateSystemAudioMode();
+ }
+ }
+ mAddressAllocated = false;
+ initializeCecLocalDevices(INITIATED_BY_SOUNDBAR_MODE);
+ }
+
+ /**
+ * Checks if the Device Discovery is handled by the local device playback.
+ * See {@link HdmiCecLocalDeviceAudioSystem#launchDeviceDiscovery}.
+ */
+ public boolean isDeviceDiscoveryHandledByPlayback() {
+ HdmiCecLocalDevicePlayback playback = playback();
+ if (playback != null && (playback.hasAction(DeviceDiscoveryAction.class)
+ || playback.hasAction(HotplugDetectionAction.class))) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
* Called when the initialization of local devices is complete.
*/
private void onInitializeCecComplete(int initiatedBy) {
@@ -993,12 +1050,30 @@
initializeCecLocalDevices(initiatedBy);
}
+ /**
+ * If the Soundbar mode is turned on, adds the local device type audio system in the list of
+ * local devices types. This method is called when the local devices are initialized such that
+ * the list of local devices is in sync with the Soundbar mode setting.
+ * @return the list of integer device types
+ */
+ @ServiceThreadOnly
+ private List<Integer> getCecLocalDeviceTypes() {
+ ArrayList<Integer> allLocalDeviceTypes = new ArrayList<>(mCecLocalDevices);
+ if (mHdmiCecConfig.getIntValue(HdmiControlManager.CEC_SETTING_NAME_SOUNDBAR_MODE)
+ == SOUNDBAR_MODE_ENABLED
+ && !allLocalDeviceTypes.contains(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM)
+ && SystemProperties.getBoolean(Constants.PROPERTY_ARC_SUPPORT, true)) {
+ allLocalDeviceTypes.add(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+ }
+ return allLocalDeviceTypes;
+ }
+
@ServiceThreadOnly
private void initializeCecLocalDevices(final int initiatedBy) {
assertRunOnServiceThread();
// A container for [Device type, Local device info].
ArrayList<HdmiCecLocalDevice> localDevices = new ArrayList<>();
- for (int type : mCecLocalDevices) {
+ for (int type : getCecLocalDeviceTypes()) {
HdmiCecLocalDevice localDevice = mHdmiCecNetwork.getLocalDevice(type);
if (localDevice == null) {
localDevice = HdmiCecLocalDevice.create(this, type);
@@ -1051,9 +1126,10 @@
// Address allocation completed for all devices. Notify each device.
if (allocatingDevices.size() == ++finished[0]) {
- if (initiatedBy != INITIATED_BY_HOTPLUG) {
- // In case of the hotplug we don't call
- // onInitializeCecComplete()
+ if (initiatedBy != INITIATED_BY_HOTPLUG
+ && initiatedBy != INITIATED_BY_SOUNDBAR_MODE) {
+ // In case of the hotplug or soundbar mode setting toggle
+ // we don't call onInitializeCecComplete()
// since we reallocate the logical address only.
onInitializeCecComplete(initiatedBy);
}
@@ -1413,7 +1489,7 @@
if (connected && !isTvDevice()
&& getPortInfo(portId).getType() == HdmiPortInfo.PORT_OUTPUT) {
ArrayList<HdmiCecLocalDevice> localDevices = new ArrayList<>();
- for (int type : mCecLocalDevices) {
+ for (int type : getCecLocalDeviceTypes()) {
HdmiCecLocalDevice localDevice = mHdmiCecNetwork.getLocalDevice(type);
if (localDevice == null) {
localDevice = HdmiCecLocalDevice.create(this, type);
@@ -3397,7 +3473,8 @@
}
@ServiceThreadOnly
- private void clearCecLocalDevices() {
+ @VisibleForTesting
+ protected void clearCecLocalDevices() {
assertRunOnServiceThread();
if (mCecController == null) {
return;
diff --git a/services/core/java/com/android/server/input/BatteryController.java b/services/core/java/com/android/server/input/BatteryController.java
index c83fa2d..c99a7a0 100644
--- a/services/core/java/com/android/server/input/BatteryController.java
+++ b/services/core/java/com/android/server/input/BatteryController.java
@@ -19,7 +19,13 @@
import android.annotation.BinderThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothManager;
+import android.content.BroadcastReceiver;
import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
import android.hardware.BatteryState;
import android.hardware.input.IInputDeviceBatteryListener;
import android.hardware.input.IInputDeviceBatteryState;
@@ -46,6 +52,7 @@
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
+import java.util.function.Function;
import java.util.function.Predicate;
/**
@@ -74,6 +81,7 @@
private final NativeInputManagerService mNative;
private final Handler mHandler;
private final UEventManager mUEventManager;
+ private final BluetoothBatteryManager mBluetoothBatteryManager;
// Maps a pid to the registered listener record for that process. There can only be one battery
// listener per process.
@@ -88,18 +96,23 @@
private boolean mIsPolling = false;
@GuardedBy("mLock")
private boolean mIsInteractive = true;
+ @Nullable
+ @GuardedBy("mLock")
+ private BluetoothBatteryManager.BluetoothBatteryListener mBluetoothBatteryListener;
BatteryController(Context context, NativeInputManagerService nativeService, Looper looper) {
- this(context, nativeService, looper, new UEventManager() {});
+ this(context, nativeService, looper, new UEventManager() {},
+ new LocalBluetoothBatteryManager(context));
}
@VisibleForTesting
BatteryController(Context context, NativeInputManagerService nativeService, Looper looper,
- UEventManager uEventManager) {
+ UEventManager uEventManager, BluetoothBatteryManager bbm) {
mContext = context;
mNative = nativeService;
mHandler = new Handler(looper);
mUEventManager = uEventManager;
+ mBluetoothBatteryManager = bbm;
}
public void systemRunning() {
@@ -150,6 +163,7 @@
// This is the first listener that is monitoring this device.
monitor = new DeviceMonitor(deviceId);
mDeviceMonitors.put(deviceId, monitor);
+ updateBluetoothMonitoring();
}
if (DEBUG) {
@@ -202,25 +216,39 @@
mHandler.postDelayed(this::handlePollEvent, delayStart ? POLLING_PERIOD_MILLIS : 0);
}
- private String getInputDeviceName(int deviceId) {
+ private <R> R processInputDevice(int deviceId, R defaultValue, Function<InputDevice, R> func) {
final InputDevice device =
Objects.requireNonNull(mContext.getSystemService(InputManager.class))
.getInputDevice(deviceId);
- return device != null ? device.getName() : "<none>";
+ return device == null ? defaultValue : func.apply(device);
+ }
+
+ private String getInputDeviceName(int deviceId) {
+ return processInputDevice(deviceId, "<none>" /*defaultValue*/, InputDevice::getName);
}
private boolean hasBattery(int deviceId) {
- final InputDevice device =
- Objects.requireNonNull(mContext.getSystemService(InputManager.class))
- .getInputDevice(deviceId);
- return device != null && device.hasBattery();
+ return processInputDevice(deviceId, false /*defaultValue*/, InputDevice::hasBattery);
}
private boolean isUsiDevice(int deviceId) {
- final InputDevice device =
- Objects.requireNonNull(mContext.getSystemService(InputManager.class))
- .getInputDevice(deviceId);
- return device != null && device.supportsUsi();
+ return processInputDevice(deviceId, false /*defaultValue*/, InputDevice::supportsUsi);
+ }
+
+ @Nullable
+ private BluetoothDevice getBluetoothDevice(int inputDeviceId) {
+ return getBluetoothDevice(mContext,
+ processInputDevice(inputDeviceId, null /*defaultValue*/,
+ InputDevice::getBluetoothAddress));
+ }
+
+ @Nullable
+ private static BluetoothDevice getBluetoothDevice(Context context, String address) {
+ if (address == null) return null;
+ final BluetoothAdapter adapter =
+ Objects.requireNonNull(context.getSystemService(BluetoothManager.class))
+ .getAdapter();
+ return adapter.getRemoteDevice(address);
}
@GuardedBy("mLock")
@@ -350,6 +378,17 @@
}
}
+ private void handleBluetoothBatteryLevelChange(long eventTime, String address) {
+ synchronized (mLock) {
+ final DeviceMonitor monitor = findIf(mDeviceMonitors, (m) ->
+ (m.mBluetoothDevice != null
+ && address.equals(m.mBluetoothDevice.getAddress())));
+ if (monitor != null) {
+ monitor.onBluetoothBatteryChanged(eventTime);
+ }
+ }
+ }
+
/** Gets the current battery state of an input device. */
public IInputDeviceBatteryState getBatteryState(int deviceId) {
synchronized (mLock) {
@@ -475,17 +514,52 @@
isPresent ? mNative.getBatteryCapacity(deviceId) / 100.f : Float.NaN);
}
+ // Queries the battery state of an input device from Bluetooth.
+ private State queryBatteryStateFromBluetooth(int deviceId, long updateTime,
+ @NonNull BluetoothDevice bluetoothDevice) {
+ final int level = mBluetoothBatteryManager.getBatteryLevel(bluetoothDevice.getAddress());
+ if (level == BluetoothDevice.BATTERY_LEVEL_BLUETOOTH_OFF
+ || level == BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
+ return new State(deviceId);
+ }
+ return new State(deviceId, updateTime, true /*isPresent*/, BatteryState.STATUS_UNKNOWN,
+ level / 100.f);
+ }
+
+ private void updateBluetoothMonitoring() {
+ synchronized (mLock) {
+ if (anyOf(mDeviceMonitors, (m) -> m.mBluetoothDevice != null)) {
+ // At least one input device being monitored is connected over Bluetooth.
+ if (mBluetoothBatteryListener == null) {
+ if (DEBUG) Slog.d(TAG, "Registering bluetooth battery listener");
+ mBluetoothBatteryListener = this::handleBluetoothBatteryLevelChange;
+ mBluetoothBatteryManager.addListener(mBluetoothBatteryListener);
+ }
+ } else if (mBluetoothBatteryListener != null) {
+ // No Bluetooth input devices are monitored, so remove the registered listener.
+ if (DEBUG) Slog.d(TAG, "Unregistering bluetooth battery listener");
+ mBluetoothBatteryManager.removeListener(mBluetoothBatteryListener);
+ mBluetoothBatteryListener = null;
+ }
+ }
+ }
+
// Holds the state of an InputDevice for which battery changes are currently being monitored.
private class DeviceMonitor {
protected final State mState;
// Represents whether the input device has a sysfs battery node.
protected boolean mHasBattery = false;
+ protected final State mBluetoothState;
+ @Nullable
+ private BluetoothDevice mBluetoothDevice;
+
@Nullable
private UEventBatteryListener mUEventBatteryListener;
DeviceMonitor(int deviceId) {
mState = new State(deviceId);
+ mBluetoothState = new State(deviceId);
// Load the initial battery state and start monitoring.
final long eventTime = SystemClock.uptimeMillis();
@@ -506,18 +580,31 @@
}
private void configureDeviceMonitor(long eventTime) {
+ final int deviceId = mState.deviceId;
if (mHasBattery != hasBattery(mState.deviceId)) {
mHasBattery = !mHasBattery;
if (mHasBattery) {
- startMonitoring();
+ startNativeMonitoring();
} else {
- stopMonitoring();
+ stopNativeMonitoring();
}
updateBatteryStateFromNative(eventTime);
}
+
+ final BluetoothDevice bluetoothDevice = getBluetoothDevice(deviceId);
+ if (!Objects.equals(mBluetoothDevice, bluetoothDevice)) {
+ if (DEBUG) {
+ Slog.d(TAG, "Bluetooth device "
+ + ((bluetoothDevice != null) ? "is" : "is not")
+ + " now present for deviceId " + deviceId);
+ }
+ mBluetoothDevice = bluetoothDevice;
+ updateBluetoothMonitoring();
+ updateBatteryStateFromBluetooth(eventTime);
+ }
}
- private void startMonitoring() {
+ private void startNativeMonitoring() {
final String batteryPath = mNative.getBatteryDevicePath(mState.deviceId);
if (batteryPath == null) {
return;
@@ -538,7 +625,7 @@
return path.startsWith("/sys") ? path.substring(4) : path;
}
- private void stopMonitoring() {
+ private void stopNativeMonitoring() {
if (mUEventBatteryListener != null) {
mUEventManager.removeListener(mUEventBatteryListener);
mUEventBatteryListener = null;
@@ -547,7 +634,9 @@
// This must be called when the device is no longer being monitored.
public void onMonitorDestroy() {
- stopMonitoring();
+ stopNativeMonitoring();
+ mBluetoothDevice = null;
+ updateBluetoothMonitoring();
}
protected void updateBatteryStateFromNative(long eventTime) {
@@ -555,6 +644,13 @@
queryBatteryStateFromNative(mState.deviceId, eventTime, mHasBattery));
}
+ protected void updateBatteryStateFromBluetooth(long eventTime) {
+ final State bluetoothState = mBluetoothDevice == null ? new State(mState.deviceId)
+ : queryBatteryStateFromBluetooth(mState.deviceId, eventTime,
+ mBluetoothDevice);
+ mBluetoothState.updateIfChanged(bluetoothState);
+ }
+
public void onPoll(long eventTime) {
processChangesAndNotify(eventTime, this::updateBatteryStateFromNative);
}
@@ -563,6 +659,10 @@
processChangesAndNotify(eventTime, this::updateBatteryStateFromNative);
}
+ public void onBluetoothBatteryChanged(long eventTime) {
+ processChangesAndNotify(eventTime, this::updateBatteryStateFromBluetooth);
+ }
+
public boolean requiresPolling() {
return true;
}
@@ -577,6 +677,10 @@
// Returns the current battery state that can be used to notify listeners BatteryController.
public State getBatteryStateForReporting() {
+ // Give precedence to the Bluetooth battery state if it's present.
+ if (mBluetoothState.isPresent) {
+ return new State(mBluetoothState);
+ }
return new State(mState);
}
@@ -585,7 +689,8 @@
return "DeviceId=" + mState.deviceId
+ ", Name='" + getInputDeviceName(mState.deviceId) + "'"
+ ", NativeBattery=" + mState
- + ", UEventListener=" + (mUEventBatteryListener != null ? "added" : "none");
+ + ", UEventListener=" + (mUEventBatteryListener != null ? "added" : "none")
+ + ", BluetoothBattery=" + mBluetoothState;
}
}
@@ -670,6 +775,10 @@
@Override
public State getBatteryStateForReporting() {
+ // Give precedence to the Bluetooth battery state if it's present.
+ if (mBluetoothState.isPresent) {
+ return new State(mBluetoothState);
+ }
return mValidityTimeoutCallback != null
? new State(mState) : new State(mState.deviceId);
}
@@ -729,6 +838,82 @@
}
}
+ // An interface used to change the API of adding a bluetooth battery listener to a more
+ // test-friendly format.
+ @VisibleForTesting
+ interface BluetoothBatteryManager {
+ @VisibleForTesting
+ interface BluetoothBatteryListener {
+ void onBluetoothBatteryChanged(long eventTime, String address);
+ }
+ void addListener(BluetoothBatteryListener listener);
+ void removeListener(BluetoothBatteryListener listener);
+ int getBatteryLevel(String address);
+ }
+
+ private static class LocalBluetoothBatteryManager implements BluetoothBatteryManager {
+ private final Context mContext;
+ @Nullable
+ @GuardedBy("mBroadcastReceiver")
+ private BluetoothBatteryListener mRegisteredListener;
+ @GuardedBy("mBroadcastReceiver")
+ private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (!BluetoothDevice.ACTION_BATTERY_LEVEL_CHANGED.equals(intent.getAction())) {
+ return;
+ }
+ final BluetoothDevice bluetoothDevice = intent.getParcelableExtra(
+ BluetoothDevice.EXTRA_DEVICE, BluetoothDevice.class);
+ if (bluetoothDevice == null) {
+ return;
+ }
+ // We do not use the EXTRA_LEVEL value. Instead, the battery level will be queried
+ // from BluetoothDevice later so that we use a single source for the battery level.
+ synchronized (mBroadcastReceiver) {
+ if (mRegisteredListener != null) {
+ final long eventTime = SystemClock.uptimeMillis();
+ mRegisteredListener.onBluetoothBatteryChanged(
+ eventTime, bluetoothDevice.getAddress());
+ }
+ }
+ }
+ };
+
+ LocalBluetoothBatteryManager(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ public void addListener(BluetoothBatteryListener listener) {
+ synchronized (mBroadcastReceiver) {
+ if (mRegisteredListener != null) {
+ throw new IllegalStateException(
+ "Only one bluetooth battery listener can be registered at once.");
+ }
+ mRegisteredListener = listener;
+ mContext.registerReceiver(mBroadcastReceiver,
+ new IntentFilter(BluetoothDevice.ACTION_BATTERY_LEVEL_CHANGED));
+ }
+ }
+
+ @Override
+ public void removeListener(BluetoothBatteryListener listener) {
+ synchronized (mBroadcastReceiver) {
+ if (!listener.equals(mRegisteredListener)) {
+ throw new IllegalStateException("Listener is not registered.");
+ }
+ mRegisteredListener = null;
+ mContext.unregisterReceiver(mBroadcastReceiver);
+ }
+ }
+
+ @Override
+ public int getBatteryLevel(String address) {
+ return getBluetoothDevice(mContext, address).getBatteryLevel();
+ }
+ }
+
// Helper class that adds copying and printing functionality to IInputDeviceBatteryState.
private static class State extends IInputDeviceBatteryState {
@@ -792,11 +977,17 @@
// Check if any value in an ArrayMap matches the predicate in an optimized way.
private static <K, V> boolean anyOf(ArrayMap<K, V> arrayMap, Predicate<V> test) {
+ return findIf(arrayMap, test) != null;
+ }
+
+ // Find the first value in an ArrayMap that matches the predicate in an optimized way.
+ private static <K, V> V findIf(ArrayMap<K, V> arrayMap, Predicate<V> test) {
for (int i = 0; i < arrayMap.size(); i++) {
- if (test.test(arrayMap.valueAt(i))) {
- return true;
+ final V value = arrayMap.valueAt(i);
+ if (test.test(value)) {
+ return value;
}
}
- return false;
+ return null;
}
}
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 1274f02..199519c 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -2126,7 +2126,13 @@
public String getInputDeviceBluetoothAddress(int deviceId) {
super.getInputDeviceBluetoothAddress_enforcePermission();
- return mNative.getBluetoothAddress(deviceId);
+ final String address = mNative.getBluetoothAddress(deviceId);
+ if (address == null) return null;
+ if (!BluetoothAdapter.checkBluetoothAddress(address)) {
+ throw new IllegalStateException("The Bluetooth address of input device " + deviceId
+ + " should not be invalid: address=" + address);
+ }
+ return address;
}
@EnforcePermission(Manifest.permission.MONITOR_INPUT)
diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
index 5623596..d6846be 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -667,9 +667,11 @@
"userId: %d", newActiveUserId));
mCurrentActiveUserId = newActiveUserId;
- for (int i = 0; i < mUserRecords.size(); i++) {
- int userId = mUserRecords.keyAt(i);
- UserRecord userRecord = mUserRecords.valueAt(i);
+ // disposeUserIfNeededLocked might modify the collection, hence clone
+ final var userRecords = mUserRecords.clone();
+ for (int i = 0; i < userRecords.size(); i++) {
+ int userId = userRecords.keyAt(i);
+ UserRecord userRecord = userRecords.valueAt(i);
if (isUserActiveLocked(userId)) {
// userId corresponds to the active user, or one of its profiles. We
// ensure the associated structures are initialized.
@@ -1742,6 +1744,9 @@
indexOfRouteProviderInfoByUniqueId(provider.getUniqueId(), mLastProviderInfos);
MediaRoute2ProviderInfo oldInfo =
providerInfoIndex == -1 ? null : mLastProviderInfos.get(providerInfoIndex);
+ MediaRouter2ServiceImpl mediaRouter2Service = mServiceRef.get();
+ EventLogger eventLogger =
+ mediaRouter2Service != null ? mediaRouter2Service.mEventLogger : null;
if (oldInfo == newInfo) {
// Nothing to do.
return;
@@ -1767,6 +1772,7 @@
}
// Add new routes to the maps.
+ ArrayList<MediaRoute2Info> addedRoutes = new ArrayList<>();
boolean hasAddedOrModifiedRoutes = false;
for (MediaRoute2Info newRouteInfo : newRoutes) {
if (!newRouteInfo.isValid()) {
@@ -1781,11 +1787,14 @@
MediaRoute2Info oldRouteInfo =
mLastNotifiedRoutesToPrivilegedRouters.put(
newRouteInfo.getId(), newRouteInfo);
- hasAddedOrModifiedRoutes |=
- oldRouteInfo == null || !oldRouteInfo.equals(newRouteInfo);
+ hasAddedOrModifiedRoutes |= !newRouteInfo.equals(oldRouteInfo);
+ if (oldRouteInfo == null) {
+ addedRoutes.add(newRouteInfo);
+ }
}
// Remove stale routes from the maps.
+ ArrayList<MediaRoute2Info> removedRoutes = new ArrayList<>();
Collection<MediaRoute2Info> oldRoutes =
oldInfo == null ? Collections.emptyList() : oldInfo.getRoutes();
boolean hasRemovedRoutes = false;
@@ -1795,6 +1804,26 @@
hasRemovedRoutes = true;
mLastNotifiedRoutesToPrivilegedRouters.remove(oldRouteId);
mLastNotifiedRoutesToNonPrivilegedRouters.remove(oldRouteId);
+ removedRoutes.add(oldRoute);
+ }
+ }
+
+ if (eventLogger != null) {
+ if (!addedRoutes.isEmpty()) {
+ // If routes were added, newInfo cannot be null.
+ eventLogger.enqueue(
+ toLoggingEvent(
+ /* source= */ "addProviderRoutes",
+ newInfo.getUniqueId(),
+ addedRoutes));
+ }
+ if (!removedRoutes.isEmpty()) {
+ // If routes were removed, oldInfo cannot be null.
+ eventLogger.enqueue(
+ toLoggingEvent(
+ /* source= */ "removeProviderRoutes",
+ oldInfo.getUniqueId(),
+ removedRoutes));
}
}
@@ -1805,6 +1834,16 @@
mSystemProvider.getDefaultRoute());
}
+ private static EventLogger.Event toLoggingEvent(
+ String source, String providerId, ArrayList<MediaRoute2Info> routes) {
+ String routesString =
+ routes.stream()
+ .map(it -> String.format("%s | %s", it.getOriginalId(), it.getName()))
+ .collect(Collectors.joining(/* delimiter= */ ", "));
+ return EventLogger.StringEvent.from(
+ source, "provider: %s, routes: [%s]", providerId, routesString);
+ }
+
/**
* Dispatches the latest route updates in {@link #mLastNotifiedRoutesToPrivilegedRouters}
* and {@link #mLastNotifiedRoutesToNonPrivilegedRouters} to registered {@link
diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java
index add1135..beab5ea 100644
--- a/services/core/java/com/android/server/media/MediaRouterService.java
+++ b/services/core/java/com/android/server/media/MediaRouterService.java
@@ -638,9 +638,11 @@
synchronized (mLock) {
if (mCurrentActiveUserId != newActiveUserId) {
mCurrentActiveUserId = newActiveUserId;
- for (int i = 0; i < mUserRecords.size(); i++) {
- int userId = mUserRecords.keyAt(i);
- UserRecord userRecord = mUserRecords.valueAt(i);
+ // disposeUserIfNeededLocked might modify the collection, hence clone
+ final var userRecords = mUserRecords.clone();
+ for (int i = 0; i < userRecords.size(); i++) {
+ int userId = userRecords.keyAt(i);
+ UserRecord userRecord = userRecords.valueAt(i);
if (isUserActiveLocked(userId)) {
// userId corresponds to the active user, or one of its profiles. We
// ensure the associated structures are initialized.
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index d6b9bd5..90135ad 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -3797,13 +3797,13 @@
}
private void createNotificationChannelsImpl(String pkg, int uid,
- ParceledListSlice channelsList, boolean fromTargetApp) {
- createNotificationChannelsImpl(pkg, uid, channelsList, fromTargetApp,
+ ParceledListSlice channelsList) {
+ createNotificationChannelsImpl(pkg, uid, channelsList,
ActivityTaskManager.INVALID_TASK_ID);
}
private void createNotificationChannelsImpl(String pkg, int uid,
- ParceledListSlice channelsList, boolean fromTargetApp, int startingTaskId) {
+ ParceledListSlice channelsList, int startingTaskId) {
List<NotificationChannel> channels = channelsList.getList();
final int channelsSize = channels.size();
ParceledListSlice<NotificationChannel> oldChannels =
@@ -3815,7 +3815,7 @@
final NotificationChannel channel = channels.get(i);
Objects.requireNonNull(channel, "channel in list is null");
needsPolicyFileChange = mPreferencesHelper.createNotificationChannel(pkg, uid,
- channel, fromTargetApp,
+ channel, true /* fromTargetApp */,
mConditionProviders.isPackageOrComponentAllowed(
pkg, UserHandle.getUserId(uid)));
if (needsPolicyFileChange) {
@@ -3851,7 +3851,6 @@
@Override
public void createNotificationChannels(String pkg, ParceledListSlice channelsList) {
checkCallerIsSystemOrSameApp(pkg);
- boolean fromTargetApp = !isCallerSystemOrPhone(); // if not system, it's from the app
int taskId = ActivityTaskManager.INVALID_TASK_ID;
try {
int uid = mPackageManager.getPackageUid(pkg, 0,
@@ -3860,15 +3859,14 @@
} catch (RemoteException e) {
// Do nothing
}
- createNotificationChannelsImpl(pkg, Binder.getCallingUid(), channelsList, fromTargetApp,
- taskId);
+ createNotificationChannelsImpl(pkg, Binder.getCallingUid(), channelsList, taskId);
}
@Override
public void createNotificationChannelsForPackage(String pkg, int uid,
ParceledListSlice channelsList) {
enforceSystemOrSystemUI("only system can call this");
- createNotificationChannelsImpl(pkg, uid, channelsList, false /* fromTargetApp */);
+ createNotificationChannelsImpl(pkg, uid, channelsList);
}
@Override
@@ -3883,8 +3881,7 @@
CONVERSATION_CHANNEL_ID_FORMAT, parentId, conversationId));
conversationChannel.setConversationId(parentId, conversationId);
createNotificationChannelsImpl(
- pkg, uid, new ParceledListSlice(Arrays.asList(conversationChannel)),
- false /* fromTargetApp */);
+ pkg, uid, new ParceledListSlice(Arrays.asList(conversationChannel)));
mRankingHandler.requestSort();
handleSavePolicyFile();
}
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index 444fef6..1bbcc83 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -918,7 +918,7 @@
throw new IllegalArgumentException("Reserved id");
}
NotificationChannel existing = r.channels.get(channel.getId());
- if (existing != null) {
+ if (existing != null && fromTargetApp) {
// Actually modifying an existing channel - keep most of the existing settings
if (existing.isDeleted()) {
// The existing channel was deleted - undelete it.
@@ -1004,7 +1004,9 @@
}
if (fromTargetApp) {
channel.setLockscreenVisibility(r.visibility);
- channel.setAllowBubbles(NotificationChannel.DEFAULT_ALLOW_BUBBLE);
+ channel.setAllowBubbles(existing != null
+ ? existing.getAllowBubbles()
+ : NotificationChannel.DEFAULT_ALLOW_BUBBLE);
}
clearLockedFieldsLocked(channel);
diff --git a/services/core/java/com/android/server/pm/UserVisibilityMediator.java b/services/core/java/com/android/server/pm/UserVisibilityMediator.java
index 9c4187b..2650b23 100644
--- a/services/core/java/com/android/server/pm/UserVisibilityMediator.java
+++ b/services/core/java/com/android/server/pm/UserVisibilityMediator.java
@@ -413,41 +413,12 @@
if (displayId == Display.INVALID_DISPLAY) {
return false;
}
- if (!mUsersOnSecondaryDisplaysEnabled) {
- return isCurrentUserOrRunningProfileOfCurrentUser(userId);
- }
- // TODO(b/256242848): temporary workaround to let WM use this API without breaking current
- // behavior - return true for current user / profile for any display (other than those
- // explicitly assigned to another users), otherwise they wouldn't be able to launch
- // activities on other non-passenger displays, like cluster).
- // In the long-term, it should rely just on mUsersOnSecondaryDisplays, which
- // would be updated by CarService to allow additional mappings.
- if (isCurrentUserOrRunningProfileOfCurrentUser(userId)) {
- synchronized (mLock) {
- boolean assignedToUser = false;
- boolean assignedToAnotherUser = false;
- for (int i = 0; i < mUsersOnSecondaryDisplays.size(); i++) {
- if (mUsersOnSecondaryDisplays.valueAt(i) == displayId) {
- if (mUsersOnSecondaryDisplays.keyAt(i) == userId) {
- assignedToUser = true;
- break;
- } else {
- assignedToAnotherUser = true;
- // Cannot break because it could be assigned to a profile of the user
- // (and we better not assume that the iteration will check for the
- // parent user before its profiles)
- }
- }
- }
- if (DBG) {
- Slogf.d(TAG, "isUserVisibleOnDisplay(%d, %d): assignedToUser=%b, "
- + "assignedToAnotherUser=%b, mUsersOnSecondaryDisplays=%s",
- userId, displayId, assignedToUser, assignedToAnotherUser,
- mUsersOnSecondaryDisplays);
- }
- return assignedToUser || !assignedToAnotherUser;
- }
+ if (!mUsersOnSecondaryDisplaysEnabled || displayId == Display.DEFAULT_DISPLAY) {
+ // TODO(b/245939659): will need to move the displayId == Display.DEFAULT_DISPLAY outside
+ // once it supports background users on DEFAULT_DISPLAY (for example, passengers in a
+ // no-driver configuration)
+ return isCurrentUserOrRunningProfileOfCurrentUser(userId);
}
synchronized (mLock) {
diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java
index 12d0f3c..fa811ef 100644
--- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java
+++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java
@@ -1050,15 +1050,31 @@
TelephonyTimeZoneAlgorithmStatus telephonyAlgorithmStatus =
createTelephonyAlgorithmStatus(currentConfigurationInternal);
- LocationTimeZoneAlgorithmStatus locationAlgorithmStatus =
- latestLocationAlgorithmEvent == null ? LocationTimeZoneAlgorithmStatus.UNKNOWN
- : latestLocationAlgorithmEvent.getAlgorithmStatus();
+ LocationTimeZoneAlgorithmStatus locationAlgorithmStatus = createLocationAlgorithmStatus(
+ currentConfigurationInternal, latestLocationAlgorithmEvent);
return new TimeZoneDetectorStatus(
detectorStatus, telephonyAlgorithmStatus, locationAlgorithmStatus);
}
@NonNull
+ private static LocationTimeZoneAlgorithmStatus createLocationAlgorithmStatus(
+ ConfigurationInternal currentConfigurationInternal,
+ LocationAlgorithmEvent latestLocationAlgorithmEvent) {
+ LocationTimeZoneAlgorithmStatus locationAlgorithmStatus;
+ if (latestLocationAlgorithmEvent != null) {
+ locationAlgorithmStatus = latestLocationAlgorithmEvent.getAlgorithmStatus();
+ } else if (!currentConfigurationInternal.isGeoDetectionSupported()) {
+ locationAlgorithmStatus = LocationTimeZoneAlgorithmStatus.NOT_SUPPORTED;
+ } else if (currentConfigurationInternal.isGeoDetectionExecutionEnabled()) {
+ locationAlgorithmStatus = LocationTimeZoneAlgorithmStatus.RUNNING_NOT_REPORTED;
+ } else {
+ locationAlgorithmStatus = LocationTimeZoneAlgorithmStatus.NOT_RUNNING;
+ }
+ return locationAlgorithmStatus;
+ }
+
+ @NonNull
private static TelephonyTimeZoneAlgorithmStatus createTelephonyAlgorithmStatus(
@NonNull ConfigurationInternal currentConfigurationInternal) {
int algorithmStatus;
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyState.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyState.java
index aad82cd..5fc3cb0 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyState.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyState.java
@@ -80,7 +80,7 @@
private boolean resolvePolicy() {
V resolvedPolicy = mPolicyDefinition.resolvePolicy(mAdminsPolicy);
- boolean policyChanged = Objects.equals(resolvedPolicy, mCurrentResolvedPolicy);
+ boolean policyChanged = !Objects.equals(resolvedPolicy, mCurrentResolvedPolicy);
mCurrentResolvedPolicy = resolvedPolicy;
return policyChanged;
diff --git a/services/people/java/com/android/server/people/data/DataManager.java b/services/people/java/com/android/server/people/data/DataManager.java
index 693f3a0..1bd5031 100644
--- a/services/people/java/com/android/server/people/data/DataManager.java
+++ b/services/people/java/com/android/server/people/data/DataManager.java
@@ -788,7 +788,7 @@
private void updateDefaultSmsApp(@NonNull UserData userData) {
ComponentName component = SmsApplication.getDefaultSmsApplicationAsUser(
- mContext, /* updateIfNeeded= */ false, userData.getUserId());
+ mContext, /* updateIfNeeded= */ false, UserHandle.of(userData.getUserId()));
String defaultSmsApp = component != null ? component.getPackageName() : null;
userData.setDefaultSmsApp(defaultSmsApp);
}
diff --git a/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java b/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java
index 298cbf3..6af7269 100644
--- a/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java
+++ b/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java
@@ -74,6 +74,7 @@
import android.app.Application;
import android.app.IBackupAgent;
import android.app.backup.BackupAgent;
+import android.app.backup.BackupAnnotations;
import android.app.backup.BackupDataInput;
import android.app.backup.BackupDataOutput;
import android.app.backup.BackupManager;
@@ -183,7 +184,7 @@
private static final String BACKUP_AGENT_SHARED_PREFS_SYNCHRONIZER_CLASS =
"android.app.backup.BackupAgent$SharedPrefsSynchronizer";
private static final int USER_ID = 10;
- private static final int OPERATION_TYPE = BackupManager.OperationType.BACKUP;
+ private static final int BACKUP_DESTINATION = BackupAnnotations.BackupDestination.CLOUD;
@Mock private TransportManager mTransportManager;
@Mock private DataChangedJournal mOldJournal;
@@ -264,7 +265,8 @@
LocalServices.removeServiceForTest(PackageManagerInternal.class);
LocalServices.addService(PackageManagerInternal.class, mPackageManagerInternal);
mBackupEligibilityRules = new BackupEligibilityRules(mPackageManager,
- LocalServices.getService(PackageManagerInternal.class), USER_ID, OPERATION_TYPE);
+ LocalServices.getService(PackageManagerInternal.class), USER_ID,
+ BACKUP_DESTINATION);
}
@After
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
index 55d1160..9234431 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
@@ -439,14 +439,25 @@
@SuppressWarnings("GuardedBy")
@Test
public void testUpdateOomAdj_DoOne_FgService_ShortFgs() {
+ sService.mConstants.TOP_TO_FGS_GRACE_DURATION = 100_000;
+ sService.mConstants.mShortFgsProcStateExtraWaitDuration = 200_000;
+
+ ServiceRecord s = ServiceRecord.newEmptyInstanceForTest(sService);
+ s.startRequested = true;
+ s.isForeground = true;
+ s.foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE;
+ s.setShortFgsInfo(SystemClock.uptimeMillis());
+
// SHORT_SERVICE FGS will get IMP_FG and a slightly different recent-adjustment.
{
ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true));
+ app.mServices.startService(s);
app.mServices.setHasForegroundServices(true,
ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE, /* hasNoneType=*/false);
app.mState.setLastTopTime(SystemClock.uptimeMillis());
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
+
sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
assertProcStates(app, PROCESS_STATE_IMPORTANT_FOREGROUND,
@@ -461,9 +472,11 @@
MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true));
app.mServices.setHasForegroundServices(true,
ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE, /* hasNoneType=*/false);
+ app.mServices.startService(s);
app.mState.setLastTopTime(SystemClock.uptimeMillis()
- sService.mConstants.TOP_TO_FGS_GRACE_DURATION);
sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
+
sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
assertProcStates(app, PROCESS_STATE_IMPORTANT_FOREGROUND,
@@ -471,6 +484,33 @@
// Still should get network access.
assertTrue((app.mState.getSetCapability() & PROCESS_CAPABILITY_NETWORK) != 0);
}
+
+ // SHORT_SERVICE, timed out already.
+ s = ServiceRecord.newEmptyInstanceForTest(sService);
+ s.startRequested = true;
+ s.isForeground = true;
+ s.foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE;
+ s.setShortFgsInfo(SystemClock.uptimeMillis()
+ - sService.mConstants.mShortFgsTimeoutDuration
+ - sService.mConstants.mShortFgsProcStateExtraWaitDuration);
+ {
+ ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
+ MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true));
+ app.mServices.setHasForegroundServices(true,
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE, /* hasNoneType=*/false);
+ app.mServices.startService(s);
+ app.mState.setLastTopTime(SystemClock.uptimeMillis()
+ - sService.mConstants.TOP_TO_FGS_GRACE_DURATION);
+ sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
+
+ sService.mOomAdjuster.updateOomAdjLocked(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+
+ // Procstate should be lower than FGS. (It should be SERVICE)
+ assertEquals(app.mState.getSetProcState(), PROCESS_STATE_SERVICE);
+
+ // Shouldn't have the network capability now.
+ assertTrue((app.mState.getSetCapability() & PROCESS_CAPABILITY_NETWORK) == 0);
+ }
}
@SuppressWarnings("GuardedBy")
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/AsyncUserVisibilityListener.java b/services/tests/mockingservicestests/src/com/android/server/pm/AsyncUserVisibilityListener.java
index afcedd6..a97491d 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/AsyncUserVisibilityListener.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/AsyncUserVisibilityListener.java
@@ -40,7 +40,6 @@
private static final String TAG = AsyncUserVisibilityListener.class.getSimpleName();
private static final long WAIT_TIMEOUT_MS = 2_000;
-
private static final long WAIT_NO_EVENTS_TIMEOUT_MS = 100;
private static int sNextId;
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 c5a8572..6d8910e 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUMDTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUMDTest.java
@@ -15,12 +15,14 @@
*/
package com.android.server.pm;
+import static android.os.UserHandle.USER_NULL;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.INVALID_DISPLAY;
import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_FAILURE;
import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE;
import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE;
+import static com.android.server.pm.UserVisibilityChangedEvent.onInvisible;
import static com.android.server.pm.UserVisibilityChangedEvent.onVisible;
import static com.android.server.pm.UserVisibilityMediator.INITIAL_CURRENT_USER_ID;
@@ -40,6 +42,88 @@
}
@Test
+ public void testStartFgUser_onDefaultDisplay() throws Exception {
+ AsyncUserVisibilityListener listener = addListenerForEvents(
+ onInvisible(INITIAL_CURRENT_USER_ID),
+ onVisible(USER_ID));
+
+ int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, FG,
+ DEFAULT_DISPLAY);
+ assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE);
+
+ expectUserIsVisible(USER_ID);
+ expectUserIsVisibleOnDisplay(USER_ID, DEFAULT_DISPLAY);
+ expectUserIsNotVisibleOnDisplay(USER_ID, INVALID_DISPLAY);
+ expectUserIsNotVisibleOnDisplay(USER_ID, SECONDARY_DISPLAY_ID);
+ expectVisibleUsers(USER_ID);
+
+ expectDisplayAssignedToUser(USER_ID, DEFAULT_DISPLAY);
+ expectUserAssignedToDisplay(DEFAULT_DISPLAY, USER_ID);
+ expectUserAssignedToDisplay(INVALID_DISPLAY, USER_ID);
+ expectUserAssignedToDisplay(SECONDARY_DISPLAY_ID, USER_ID);
+
+ expectDisplayAssignedToUser(USER_NULL, INVALID_DISPLAY);
+
+ listener.verify();
+ }
+
+ @Test
+ public void testSwitchFgUser_onDefaultDisplay() throws Exception {
+ int previousCurrentUserId = OTHER_USER_ID;
+ int currentUserId = USER_ID;
+ AsyncUserVisibilityListener listener = addListenerForEvents(
+ onInvisible(INITIAL_CURRENT_USER_ID),
+ onVisible(previousCurrentUserId),
+ onInvisible(previousCurrentUserId),
+ onVisible(currentUserId));
+ startForegroundUser(previousCurrentUserId);
+
+ int result = mMediator.assignUserToDisplayOnStart(currentUserId, currentUserId, FG,
+ DEFAULT_DISPLAY);
+ assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE);
+
+ expectUserIsVisible(currentUserId);
+ expectUserIsVisibleOnDisplay(currentUserId, DEFAULT_DISPLAY);
+ expectUserIsNotVisibleOnDisplay(currentUserId, INVALID_DISPLAY);
+ expectUserIsNotVisibleOnDisplay(currentUserId, SECONDARY_DISPLAY_ID);
+ expectVisibleUsers(currentUserId);
+
+ expectDisplayAssignedToUser(currentUserId, DEFAULT_DISPLAY);
+ expectUserAssignedToDisplay(DEFAULT_DISPLAY, currentUserId);
+ expectUserAssignedToDisplay(INVALID_DISPLAY, currentUserId);
+ expectUserAssignedToDisplay(SECONDARY_DISPLAY_ID, currentUserId);
+
+ expectUserIsNotVisibleAtAll(previousCurrentUserId);
+ expectNoDisplayAssignedToUser(previousCurrentUserId);
+
+ listener.verify();
+ }
+
+ @Test
+ public void testStartBgProfile_onDefaultDisplay_whenParentIsCurrentUser() throws Exception {
+ AsyncUserVisibilityListener listener = addListenerForEvents(
+ onInvisible(INITIAL_CURRENT_USER_ID),
+ onVisible(PARENT_USER_ID),
+ onVisible(PROFILE_USER_ID));
+ startForegroundUser(PARENT_USER_ID);
+
+ int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID, BG,
+ DEFAULT_DISPLAY);
+ assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE);
+
+ expectUserIsVisible(PROFILE_USER_ID);
+ expectUserIsNotVisibleOnDisplay(PROFILE_USER_ID, INVALID_DISPLAY);
+ expectUserIsNotVisibleOnDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID);
+ expectUserIsVisibleOnDisplay(PROFILE_USER_ID, DEFAULT_DISPLAY);
+ expectVisibleUsers(PARENT_USER_ID, PROFILE_USER_ID);
+
+ expectDisplayAssignedToUser(PROFILE_USER_ID, DEFAULT_DISPLAY);
+ expectUserAssignedToDisplay(DEFAULT_DISPLAY, PARENT_USER_ID);
+
+ listener.verify();
+ }
+
+ @Test
public void testStartFgUser_onInvalidDisplay() throws Exception {
AsyncUserVisibilityListener listener = addListenerForNoEvents();
@@ -89,15 +173,15 @@
startDefaultProfile();
// Make sure they were visible before
- expectUserIsVisibleOnDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID);
- expectUserIsVisibleOnDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID);
+ expectUserIsNotVisibleOnDisplay("before", PARENT_USER_ID, SECONDARY_DISPLAY_ID);
+ expectUserIsNotVisibleOnDisplay("before", PROFILE_USER_ID, SECONDARY_DISPLAY_ID);
int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG,
SECONDARY_DISPLAY_ID);
assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE);
- expectUserIsNotVisibleOnDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID);
- expectUserIsNotVisibleOnDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID);
+ expectUserIsNotVisibleOnDisplay("after", PARENT_USER_ID, SECONDARY_DISPLAY_ID);
+ expectUserIsNotVisibleOnDisplay("after", PROFILE_USER_ID, SECONDARY_DISPLAY_ID);
}
@Test
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 fc0287f..1065392 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorSUSDTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorSUSDTest.java
@@ -15,7 +15,15 @@
*/
package com.android.server.pm;
+import static android.os.UserHandle.USER_NULL;
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.Display.INVALID_DISPLAY;
+
import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_FAILURE;
+import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE;
+import static com.android.server.pm.UserVisibilityChangedEvent.onInvisible;
+import static com.android.server.pm.UserVisibilityChangedEvent.onVisible;
+import static com.android.server.pm.UserVisibilityMediator.INITIAL_CURRENT_USER_ID;
import org.junit.Test;
@@ -33,6 +41,88 @@
}
@Test
+ public void testStartFgUser_onDefaultDisplay() throws Exception {
+ AsyncUserVisibilityListener listener = addListenerForEvents(
+ onInvisible(INITIAL_CURRENT_USER_ID),
+ onVisible(USER_ID));
+
+ int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, FG,
+ DEFAULT_DISPLAY);
+ assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE);
+
+ expectUserIsVisible(USER_ID);
+ expectUserIsNotVisibleOnDisplay(USER_ID, INVALID_DISPLAY);
+ expectUserIsVisibleOnDisplay(USER_ID, DEFAULT_DISPLAY);
+ expectUserIsVisibleOnDisplay(USER_ID, SECONDARY_DISPLAY_ID);
+ expectVisibleUsers(USER_ID);
+
+ expectDisplayAssignedToUser(USER_ID, DEFAULT_DISPLAY);
+ expectUserAssignedToDisplay(DEFAULT_DISPLAY, USER_ID);
+ expectUserAssignedToDisplay(INVALID_DISPLAY, USER_ID);
+ expectUserAssignedToDisplay(SECONDARY_DISPLAY_ID, USER_ID);
+
+ expectDisplayAssignedToUser(USER_NULL, INVALID_DISPLAY);
+
+ listener.verify();
+ }
+
+ @Test
+ public void testSwitchFgUser_onDefaultDisplay() throws Exception {
+ int previousCurrentUserId = OTHER_USER_ID;
+ int currentUserId = USER_ID;
+ AsyncUserVisibilityListener listener = addListenerForEvents(
+ onInvisible(INITIAL_CURRENT_USER_ID),
+ onVisible(previousCurrentUserId),
+ onInvisible(previousCurrentUserId),
+ onVisible(currentUserId));
+ startForegroundUser(previousCurrentUserId);
+
+ int result = mMediator.assignUserToDisplayOnStart(currentUserId, currentUserId, FG,
+ DEFAULT_DISPLAY);
+ assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE);
+
+ expectUserIsVisible(currentUserId);
+ expectUserIsNotVisibleOnDisplay(currentUserId, INVALID_DISPLAY);
+ expectUserIsVisibleOnDisplay(currentUserId, DEFAULT_DISPLAY);
+ expectUserIsVisibleOnDisplay(currentUserId, SECONDARY_DISPLAY_ID);
+ expectVisibleUsers(currentUserId);
+
+ expectDisplayAssignedToUser(currentUserId, DEFAULT_DISPLAY);
+ expectUserAssignedToDisplay(DEFAULT_DISPLAY, currentUserId);
+ expectUserAssignedToDisplay(INVALID_DISPLAY, currentUserId);
+ expectUserAssignedToDisplay(SECONDARY_DISPLAY_ID, currentUserId);
+
+ expectUserIsNotVisibleAtAll(previousCurrentUserId);
+ expectNoDisplayAssignedToUser(previousCurrentUserId);
+
+ listener.verify();
+ }
+
+ @Test
+ public void testStartBgProfile_onDefaultDisplay_whenParentIsCurrentUser() throws Exception {
+ AsyncUserVisibilityListener listener = addListenerForEvents(
+ onInvisible(INITIAL_CURRENT_USER_ID),
+ onVisible(PARENT_USER_ID),
+ onVisible(PROFILE_USER_ID));
+ startForegroundUser(PARENT_USER_ID);
+
+ int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID, BG,
+ DEFAULT_DISPLAY);
+ assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE);
+
+ expectUserIsVisible(PROFILE_USER_ID);
+ expectUserIsNotVisibleOnDisplay(PROFILE_USER_ID, INVALID_DISPLAY);
+ expectUserIsVisibleOnDisplay(PROFILE_USER_ID, DEFAULT_DISPLAY);
+ expectUserIsVisibleOnDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID);
+ expectVisibleUsers(PARENT_USER_ID, PROFILE_USER_ID);
+
+ expectDisplayAssignedToUser(PROFILE_USER_ID, DEFAULT_DISPLAY);
+ expectUserAssignedToDisplay(DEFAULT_DISPLAY, PARENT_USER_ID);
+
+ listener.verify();
+ }
+
+ @Test
public void testStartBgUser_onSecondaryDisplay() throws Exception {
AsyncUserVisibilityListener listener = addListenerForNoEvents();
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 6ceb38a..c203831 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java
@@ -37,6 +37,7 @@
import android.annotation.UserIdInt;
import android.os.Handler;
+import android.text.TextUtils;
import android.util.IntArray;
import android.util.Log;
@@ -135,66 +136,6 @@
}
@Test
- public final void testStartFgUser_onDefaultDisplay() throws Exception {
- AsyncUserVisibilityListener listener = addListenerForEvents(
- onInvisible(INITIAL_CURRENT_USER_ID),
- onVisible(USER_ID));
-
- int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, FG,
- DEFAULT_DISPLAY);
- assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE);
-
- expectUserIsVisible(USER_ID);
- expectUserIsNotVisibleOnDisplay(USER_ID, INVALID_DISPLAY);
- expectUserIsVisibleOnDisplay(USER_ID, DEFAULT_DISPLAY);
- // TODO(b/244644281): once isUserVisible() is fixed (see note there), this assertion will
- // fail on MUMD, so we'll need to refactor / split this test (and possibly others)
- expectUserIsVisibleOnDisplay(USER_ID, SECONDARY_DISPLAY_ID);
- expectVisibleUsers(USER_ID);
-
- expectDisplayAssignedToUser(USER_ID, DEFAULT_DISPLAY);
- expectUserAssignedToDisplay(DEFAULT_DISPLAY, USER_ID);
- expectUserAssignedToDisplay(INVALID_DISPLAY, USER_ID);
- expectUserAssignedToDisplay(SECONDARY_DISPLAY_ID, USER_ID);
-
- expectDisplayAssignedToUser(USER_NULL, INVALID_DISPLAY);
-
- listener.verify();
- }
-
- @Test
- public final void testSwitchFgUser_onDefaultDisplay() throws Exception {
- int previousCurrentUserId = OTHER_USER_ID;
- int currentUserId = USER_ID;
- AsyncUserVisibilityListener listener = addListenerForEvents(
- onInvisible(INITIAL_CURRENT_USER_ID),
- onVisible(previousCurrentUserId),
- onInvisible(previousCurrentUserId),
- onVisible(currentUserId));
- startForegroundUser(previousCurrentUserId);
-
- int result = mMediator.assignUserToDisplayOnStart(currentUserId, currentUserId, FG,
- DEFAULT_DISPLAY);
- assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE);
-
- expectUserIsVisible(currentUserId);
- expectUserIsNotVisibleOnDisplay(currentUserId, INVALID_DISPLAY);
- expectUserIsVisibleOnDisplay(currentUserId, DEFAULT_DISPLAY);
- expectUserIsVisibleOnDisplay(currentUserId, SECONDARY_DISPLAY_ID);
- expectVisibleUsers(currentUserId);
-
- expectDisplayAssignedToUser(currentUserId, DEFAULT_DISPLAY);
- expectUserAssignedToDisplay(DEFAULT_DISPLAY, currentUserId);
- expectUserAssignedToDisplay(INVALID_DISPLAY, currentUserId);
- expectUserAssignedToDisplay(SECONDARY_DISPLAY_ID, currentUserId);
-
- expectUserIsNotVisibleAtAll(previousCurrentUserId);
- expectNoDisplayAssignedToUser(previousCurrentUserId);
-
- listener.verify();
- }
-
- @Test
public final void testStartFgUser_onSecondaryDisplay() throws Exception {
AsyncUserVisibilityListener listener = addListenerForNoEvents();
@@ -245,31 +186,6 @@
}
@Test
- public final void testStartBgProfile_onDefaultDisplay_whenParentIsCurrentUser()
- throws Exception {
- AsyncUserVisibilityListener listener = addListenerForEvents(
- onInvisible(INITIAL_CURRENT_USER_ID),
- onVisible(PARENT_USER_ID),
- onVisible(PROFILE_USER_ID));
- startForegroundUser(PARENT_USER_ID);
-
- int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID, BG,
- DEFAULT_DISPLAY);
- assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE);
-
- expectUserIsVisible(PROFILE_USER_ID);
- expectUserIsNotVisibleOnDisplay(PROFILE_USER_ID, INVALID_DISPLAY);
- expectUserIsVisibleOnDisplay(PROFILE_USER_ID, DEFAULT_DISPLAY);
- expectUserIsVisibleOnDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID);
- expectVisibleUsers(PARENT_USER_ID, PROFILE_USER_ID);
-
- expectDisplayAssignedToUser(PROFILE_USER_ID, DEFAULT_DISPLAY);
- expectUserAssignedToDisplay(DEFAULT_DISPLAY, PARENT_USER_ID);
-
- listener.verify();
- }
-
- @Test
public final void testStopVisibleProfile() throws Exception {
AsyncUserVisibilityListener listener = addListenerForEvents(
onInvisible(INITIAL_CURRENT_USER_ID),
@@ -530,6 +446,14 @@
.isFalse();
}
+ protected void expectUserIsNotVisibleOnDisplay(String when, @UserIdInt int userId,
+ int displayId) {
+ String suffix = TextUtils.isEmpty(when) ? "" : " on " + when;
+ expectWithMessage("mediator.isUserVisible(%s, %s)%s", userId, displayId, suffix)
+ .that(mMediator.isUserVisible(userId, displayId))
+ .isFalse();
+ }
+
protected void expectUserIsNotVisibleAtAll(@UserIdInt int userId) {
expectWithMessage("mediator.isUserVisible(%s)", userId)
.that(mMediator.isUserVisible(userId))
diff --git a/services/tests/servicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java
index 6016558..cd2f205 100644
--- a/services/tests/servicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java
@@ -29,7 +29,7 @@
import static org.mockito.Mockito.when;
import android.app.backup.BackupAgent;
-import android.app.backup.BackupManager.OperationType;
+import android.app.backup.BackupAnnotations.BackupDestination;
import android.app.backup.IBackupManagerMonitor;
import android.app.backup.IBackupObserver;
import android.content.Context;
@@ -148,18 +148,18 @@
}
@Test
- public void testGetOperationTypeFromTransport_returnsBackupByDefault()
+ public void testGetBackupDestinationFromTransport_returnsCloudByDefault()
throws Exception {
when(mTransportConnection.connectOrThrow(any())).thenReturn(mBackupTransport);
when(mBackupTransport.getTransportFlags()).thenReturn(0);
- int operationType = mService.getOperationTypeFromTransport(mTransportConnection);
+ int backupDestination = mService.getBackupDestinationFromTransport(mTransportConnection);
- assertThat(operationType).isEqualTo(OperationType.BACKUP);
+ assertThat(backupDestination).isEqualTo(BackupDestination.CLOUD);
}
@Test
- public void testGetOperationTypeFromTransport_returnsMigrationForMigrationTransport()
+ public void testGetBackupDestinationFromTransport_returnsDeviceTransferForD2dTransport()
throws Exception {
// This is a temporary flag to control the new behaviour until it's ready to be fully
// rolled out.
@@ -169,9 +169,9 @@
when(mBackupTransport.getTransportFlags()).thenReturn(
BackupAgent.FLAG_DEVICE_TO_DEVICE_TRANSFER);
- int operationType = mService.getOperationTypeFromTransport(mTransportConnection);
+ int backupDestination = mService.getBackupDestinationFromTransport(mTransportConnection);
- assertThat(operationType).isEqualTo(OperationType.MIGRATION);
+ assertThat(backupDestination).isEqualTo(BackupDestination.DEVICE_TRANSFER);
}
@Test
diff --git a/services/tests/servicestests/src/com/android/server/backup/utils/BackupEligibilityRulesTest.java b/services/tests/servicestests/src/com/android/server/backup/utils/BackupEligibilityRulesTest.java
index 310c8f4..48b0aad 100644
--- a/services/tests/servicestests/src/com/android/server/backup/utils/BackupEligibilityRulesTest.java
+++ b/services/tests/servicestests/src/com/android/server/backup/utils/BackupEligibilityRulesTest.java
@@ -23,7 +23,7 @@
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.when;
-import android.app.backup.BackupManager.OperationType;
+import android.app.backup.BackupAnnotations.BackupDestination;
import android.compat.testing.PlatformCompatChangeRule;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
@@ -78,7 +78,7 @@
MockitoAnnotations.initMocks(this);
mUserId = UserHandle.USER_SYSTEM;
- mBackupEligibilityRules = getBackupEligibilityRules(OperationType.BACKUP);
+ mBackupEligibilityRules = getBackupEligibilityRules(BackupDestination.CLOUD);
}
@Test
@@ -225,7 +225,7 @@
/* flags */ 0, CUSTOM_BACKUP_AGENT_NAME);
BackupEligibilityRules eligibilityRules = getBackupEligibilityRules(
- OperationType.MIGRATION);
+ BackupDestination.DEVICE_TRANSFER);
boolean isEligible = eligibilityRules.appIsEligibleForBackup(applicationInfo);
assertThat(isEligible).isTrue();
@@ -237,7 +237,7 @@
ApplicationInfo applicationInfo = getApplicationInfo(Process.SYSTEM_UID,
ApplicationInfo.FLAG_SYSTEM, CUSTOM_BACKUP_AGENT_NAME);
BackupEligibilityRules eligibilityRules = getBackupEligibilityRules(
- OperationType.MIGRATION);
+ BackupDestination.DEVICE_TRANSFER);
boolean isEligible = eligibilityRules.appIsEligibleForBackup(applicationInfo);
assertThat(isEligible).isFalse();
@@ -250,7 +250,7 @@
ApplicationInfo applicationInfo = getApplicationInfo(Process.FIRST_APPLICATION_UID,
/* flags */ ApplicationInfo.PRIVATE_FLAG_PRIVILEGED, CUSTOM_BACKUP_AGENT_NAME);
BackupEligibilityRules eligibilityRules = getBackupEligibilityRules(
- OperationType.ADB_BACKUP);
+ BackupDestination.ADB_BACKUP);
when(mPackageManager.getPropertyAsUser(eq(PackageManager.PROPERTY_ALLOW_ADB_BACKUP),
eq(TEST_PACKAGE_NAME), isNull(), eq(mUserId)))
.thenReturn(getAdbBackupProperty(/* allowAdbBackup */ false));
@@ -267,7 +267,7 @@
ApplicationInfo applicationInfo = getApplicationInfo(Process.FIRST_APPLICATION_UID,
/* flags */ ApplicationInfo.PRIVATE_FLAG_PRIVILEGED, CUSTOM_BACKUP_AGENT_NAME);
BackupEligibilityRules eligibilityRules = getBackupEligibilityRules(
- OperationType.ADB_BACKUP);
+ BackupDestination.ADB_BACKUP);
when(mPackageManager.getPropertyAsUser(eq(PackageManager.PROPERTY_ALLOW_ADB_BACKUP),
eq(TEST_PACKAGE_NAME), isNull(), eq(mUserId)))
.thenReturn(getAdbBackupProperty(/* allowAdbBackup */ true));
@@ -284,7 +284,7 @@
ApplicationInfo applicationInfo = getApplicationInfo(Process.FIRST_APPLICATION_UID,
/* flags */ ApplicationInfo.FLAG_DEBUGGABLE, CUSTOM_BACKUP_AGENT_NAME);
BackupEligibilityRules eligibilityRules = getBackupEligibilityRules(
- OperationType.ADB_BACKUP);
+ BackupDestination.ADB_BACKUP);
boolean isEligible = eligibilityRules.appIsEligibleForBackup(applicationInfo);
@@ -298,7 +298,7 @@
ApplicationInfo applicationInfo = getApplicationInfo(Process.FIRST_APPLICATION_UID,
ApplicationInfo.FLAG_ALLOW_BACKUP, CUSTOM_BACKUP_AGENT_NAME);
BackupEligibilityRules eligibilityRules = getBackupEligibilityRules(
- OperationType.ADB_BACKUP);
+ BackupDestination.ADB_BACKUP);
boolean isEligible = eligibilityRules.appIsEligibleForBackup(applicationInfo);
@@ -312,7 +312,7 @@
ApplicationInfo applicationInfo = getApplicationInfo(Process.FIRST_APPLICATION_UID,
/* flags */ 0, CUSTOM_BACKUP_AGENT_NAME);
BackupEligibilityRules eligibilityRules = getBackupEligibilityRules(
- OperationType.ADB_BACKUP);
+ BackupDestination.ADB_BACKUP);
boolean isEligible = eligibilityRules.appIsEligibleForBackup(applicationInfo);
@@ -787,9 +787,10 @@
assertThat(result).isFalse();
}
- private BackupEligibilityRules getBackupEligibilityRules(@OperationType int operationType) {
+ private BackupEligibilityRules getBackupEligibilityRules(
+ @BackupDestination int backupDestination) {
return new BackupEligibilityRules(mPackageManager, mMockPackageManagerInternal, mUserId,
- operationType);
+ backupDestination);
}
private static Signature generateSignature(byte i) {
diff --git a/services/tests/servicestests/src/com/android/server/display/brightness/DisplayBrightnessStrategySelectorTest.java b/services/tests/servicestests/src/com/android/server/display/brightness/DisplayBrightnessStrategySelectorTest.java
index 59c69d1..ccc43f2 100644
--- a/services/tests/servicestests/src/com/android/server/display/brightness/DisplayBrightnessStrategySelectorTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/brightness/DisplayBrightnessStrategySelectorTest.java
@@ -33,6 +33,7 @@
import com.android.server.display.brightness.strategy.InvalidBrightnessStrategy;
import com.android.server.display.brightness.strategy.OverrideBrightnessStrategy;
import com.android.server.display.brightness.strategy.ScreenOffBrightnessStrategy;
+import com.android.server.display.brightness.strategy.TemporaryBrightnessStrategy;
import org.junit.Before;
import org.junit.Test;
@@ -53,6 +54,8 @@
@Mock
private OverrideBrightnessStrategy mOverrideBrightnessStrategy;
@Mock
+ private TemporaryBrightnessStrategy mTemporaryBrightnessStrategy;
+ @Mock
private InvalidBrightnessStrategy mInvalidBrightnessStrategy;
@Mock
private Context mContext;
@@ -65,6 +68,7 @@
public void before() {
MockitoAnnotations.initMocks(this);
when(mContext.getResources()).thenReturn(mResources);
+ when(mInvalidBrightnessStrategy.getName()).thenReturn("InvalidBrightnessStrategy");
DisplayBrightnessStrategySelector.Injector injector =
new DisplayBrightnessStrategySelector.Injector() {
@Override
@@ -83,6 +87,11 @@
}
@Override
+ TemporaryBrightnessStrategy getTemporaryBrightnessStrategy() {
+ return mTemporaryBrightnessStrategy;
+ }
+
+ @Override
InvalidBrightnessStrategy getInvalidBrightnessStrategy() {
return mInvalidBrightnessStrategy;
}
@@ -121,6 +130,16 @@
}
@Test
+ public void selectStrategySelectsTemporaryStrategyWhenValid() {
+ DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = mock(
+ DisplayManagerInternal.DisplayPowerRequest.class);
+ displayPowerRequest.screenBrightnessOverride = Float.NaN;
+ when(mTemporaryBrightnessStrategy.getTemporaryScreenBrightness()).thenReturn(0.3f);
+ assertEquals(mDisplayBrightnessStrategySelector.selectStrategy(displayPowerRequest,
+ Display.STATE_ON), mTemporaryBrightnessStrategy);
+ }
+
+ @Test
public void selectStrategySelectsInvalidStrategyWhenNoStrategyIsValid() {
DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = mock(
DisplayManagerInternal.DisplayPowerRequest.class);
diff --git a/services/tests/servicestests/src/com/android/server/display/brightness/strategy/TemporaryBrightnessStrategyTest.java b/services/tests/servicestests/src/com/android/server/display/brightness/strategy/TemporaryBrightnessStrategyTest.java
new file mode 100644
index 0000000..4a32796
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/display/brightness/strategy/TemporaryBrightnessStrategyTest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display.brightness.strategy;
+
+
+import static org.junit.Assert.assertEquals;
+
+import android.hardware.display.DisplayManagerInternal;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.display.DisplayBrightnessState;
+import com.android.server.display.brightness.BrightnessReason;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+
+public class TemporaryBrightnessStrategyTest {
+ private TemporaryBrightnessStrategy mTemporaryBrightnessStrategy;
+
+ @Before
+ public void before() {
+ mTemporaryBrightnessStrategy = new TemporaryBrightnessStrategy();
+ }
+
+ @Test
+ public void updateBrightnessWorksAsExpectedWhenTemporaryBrightnessIsSet() {
+ DisplayManagerInternal.DisplayPowerRequest
+ displayPowerRequest = new DisplayManagerInternal.DisplayPowerRequest();
+ float temporaryBrightness = 0.2f;
+ mTemporaryBrightnessStrategy.setTemporaryScreenBrightness(temporaryBrightness);
+ BrightnessReason brightnessReason = new BrightnessReason();
+ brightnessReason.setReason(BrightnessReason.REASON_TEMPORARY);
+ DisplayBrightnessState expectedDisplayBrightnessState =
+ new DisplayBrightnessState.Builder()
+ .setBrightness(temporaryBrightness)
+ .setBrightnessReason(brightnessReason)
+ .setSdrBrightness(temporaryBrightness)
+ .build();
+ DisplayBrightnessState updatedDisplayBrightnessState =
+ mTemporaryBrightnessStrategy.updateBrightness(displayPowerRequest);
+ assertEquals(updatedDisplayBrightnessState, expectedDisplayBrightnessState);
+ assertEquals(mTemporaryBrightnessStrategy.getTemporaryScreenBrightness(),
+ Float.NaN, 0.0f);
+ }
+
+}
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java b/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java
index 5b11466..09cd47a 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java
@@ -143,8 +143,12 @@
Constants.ADDR_AUDIO_SYSTEM, Constants.ADDR_TV);
assertThat(mNativeWrapper.getResultMessages()).contains(terminateArc);
+ mTestLooper.dispatchAll();
- assertThat(mHdmiCecLocalDeviceAudioSystem.isArcEnabled()).isTrue();
+ mTestLooper.moveTimeForward(ArcTerminationActionFromAvr.TIMEOUT_MS);
+ mTestLooper.dispatchAll();
+
+ assertThat(mHdmiCecLocalDeviceAudioSystem.isArcEnabled()).isFalse();
}
@Test
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/FakeHdmiCecConfig.java b/services/tests/servicestests/src/com/android/server/hdmi/FakeHdmiCecConfig.java
index 5722ff3..167e0f8 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/FakeHdmiCecConfig.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/FakeHdmiCecConfig.java
@@ -80,6 +80,17 @@
R.bool.config_cecRoutingControlDisabled_default);
doReturn(true).when(resources).getBoolean(
+ R.bool.config_cecSoundbarMode_userConfigurable);
+ doReturn(true).when(resources).getBoolean(
+ R.bool.config_cecSoundbarModeEnabled_allowed);
+ doReturn(false).when(resources).getBoolean(
+ R.bool.config_cecSoundbarModeEnabled_default);
+ doReturn(true).when(resources).getBoolean(
+ R.bool.config_cecSoundbarModeDisabled_allowed);
+ doReturn(true).when(resources).getBoolean(
+ R.bool.config_cecSoundbarModeDisabled_default);
+
+ doReturn(true).when(resources).getBoolean(
R.bool.config_cecPowerControlMode_userConfigurable);
doReturn(true).when(resources).getBoolean(
R.bool.config_cecPowerControlModeTv_allowed);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecConfigTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecConfigTest.java
index 392d7f1..870b681 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecConfigTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecConfigTest.java
@@ -76,6 +76,7 @@
.containsExactly(HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_ENABLED,
HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_VERSION,
HdmiControlManager.CEC_SETTING_NAME_ROUTING_CONTROL,
+ HdmiControlManager.CEC_SETTING_NAME_SOUNDBAR_MODE,
HdmiControlManager.CEC_SETTING_NAME_POWER_CONTROL_MODE,
HdmiControlManager.CEC_SETTING_NAME_POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST,
HdmiControlManager.CEC_SETTING_NAME_SYSTEM_AUDIO_CONTROL,
@@ -116,6 +117,7 @@
.containsExactly(HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_ENABLED,
HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_VERSION,
HdmiControlManager.CEC_SETTING_NAME_ROUTING_CONTROL,
+ HdmiControlManager.CEC_SETTING_NAME_SOUNDBAR_MODE,
HdmiControlManager.CEC_SETTING_NAME_POWER_CONTROL_MODE,
HdmiControlManager.CEC_SETTING_NAME_POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST,
HdmiControlManager.CEC_SETTING_NAME_SYSTEM_AUDIO_CONTROL,
@@ -157,6 +159,7 @@
.containsExactly(HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_VERSION,
HdmiControlManager.CEC_SETTING_NAME_POWER_CONTROL_MODE,
HdmiControlManager.CEC_SETTING_NAME_ROUTING_CONTROL,
+ HdmiControlManager.CEC_SETTING_NAME_SOUNDBAR_MODE,
HdmiControlManager.CEC_SETTING_NAME_POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST,
HdmiControlManager.CEC_SETTING_NAME_SYSTEM_AUDIO_CONTROL,
HdmiControlManager.CEC_SETTING_NAME_SYSTEM_AUDIO_MODE_MUTING,
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
index 08d0e90..7c6c990 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
@@ -354,7 +354,7 @@
HdmiCecMessage expectedMessage =
HdmiCecMessageBuilder.buildSetSystemAudioMode(
ADDR_AUDIO_SYSTEM, ADDR_BROADCAST, false);
- assertThat(mNativeWrapper.getOnlyResultMessage()).isEqualTo(expectedMessage);
+ assertThat(mNativeWrapper.getResultMessages()).contains(expectedMessage);
assertThat(mMusicMute).isTrue();
}
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
index cb2d255..3ed8983 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
@@ -31,6 +31,7 @@
import android.hardware.hdmi.HdmiPortInfo;
import android.hardware.hdmi.IHdmiControlCallback;
import android.hardware.tv.cec.V1_0.SendMessageResult;
+import android.media.AudioManager;
import android.os.Looper;
import android.os.RemoteException;
import android.os.test.TestLooper;
@@ -45,6 +46,8 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
import java.util.Collections;
@@ -85,9 +88,13 @@
private boolean mActiveMediaSessionsPaused;
private FakePowerManagerInternalWrapper mPowerManagerInternal =
new FakePowerManagerInternalWrapper();
+ @Mock
+ protected AudioManager mAudioManager;
@Before
public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
Context context = InstrumentationRegistry.getTargetContext();
mMyLooper = mTestLooper.getLooper();
@@ -103,6 +110,11 @@
}
@Override
+ AudioManager getAudioManager() {
+ return mAudioManager;
+ }
+
+ @Override
void pauseActiveMediaSessions() {
mActiveMediaSessionsPaused = true;
}
@@ -1424,6 +1436,32 @@
}
@Test
+ public void sendVolumeKeyEvent_toLocalDevice_discardMessage() {
+ HdmiCecLocalDeviceAudioSystem audioSystem =
+ new HdmiCecLocalDeviceAudioSystem(mHdmiControlService);
+ audioSystem.init();
+ mLocalDevices.add(audioSystem);
+ mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
+ mTestLooper.dispatchAll();
+
+ mHdmiControlService.setHdmiCecVolumeControlEnabledInternal(
+ HdmiControlManager.VOLUME_CONTROL_ENABLED);
+ mHdmiControlService.setSystemAudioActivated(true);
+
+ mHdmiCecLocalDevicePlayback.sendVolumeKeyEvent(KeyEvent.KEYCODE_VOLUME_UP, true);
+ mHdmiCecLocalDevicePlayback.sendVolumeKeyEvent(KeyEvent.KEYCODE_VOLUME_UP, false);
+
+ HdmiCecMessage keyPressed = HdmiCecMessageBuilder.buildUserControlPressed(
+ mPlaybackLogicalAddress, ADDR_AUDIO_SYSTEM, HdmiCecKeycode.CEC_KEYCODE_VOLUME_UP);
+ HdmiCecMessage keyReleased = HdmiCecMessageBuilder.buildUserControlReleased(
+ mPlaybackLogicalAddress, ADDR_AUDIO_SYSTEM);
+ mTestLooper.dispatchAll();
+
+ assertThat(mNativeWrapper.getResultMessages()).doesNotContain(keyPressed);
+ assertThat(mNativeWrapper.getResultMessages()).doesNotContain(keyReleased);
+ }
+
+ @Test
public void handleSetStreamPath_broadcastsActiveSource() {
HdmiCecMessage setStreamPath = HdmiCecMessageBuilder.buildSetStreamPath(ADDR_TV,
mPlaybackPhysicalAddress);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
index 81d569b..ef2b212 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
@@ -44,6 +44,7 @@
import android.hardware.hdmi.IHdmiCecVolumeControlFeatureListener;
import android.hardware.hdmi.IHdmiControlStatusChangeListener;
import android.hardware.hdmi.IHdmiVendorCommandListener;
+import android.media.AudioManager;
import android.os.Binder;
import android.os.Looper;
import android.os.RemoteException;
@@ -58,7 +59,9 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
+import org.mockito.Mock;
import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
import java.util.Arrays;
@@ -86,8 +89,12 @@
private HdmiPortInfo[] mHdmiPortInfo;
private ArrayList<Integer> mLocalDeviceTypes = new ArrayList<>();
+ @Mock protected AudioManager mAudioManager;
+
@Before
public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
HdmiCecConfig hdmiCecConfig = new FakeHdmiCecConfig(mContextSpy);
@@ -132,6 +139,7 @@
mPowerManager = new FakePowerManagerWrapper(mContextSpy);
mHdmiControlServiceSpy.setPowerManager(mPowerManager);
mHdmiControlServiceSpy.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
+ mHdmiControlServiceSpy.setAudioManager(mAudioManager);
mTestLooper.dispatchAll();
}
@@ -1026,6 +1034,48 @@
.containsExactly(DEVICE_PLAYBACK, DEVICE_AUDIO_SYSTEM);
}
+ @Test
+ public void setSoundbarMode_enabled_addAudioSystemLocalDevice() {
+ // Initialize the local devices excluding the audio system.
+ mHdmiControlServiceSpy.clearCecLocalDevices();
+ mLocalDevices.remove(mAudioSystemDeviceSpy);
+ mHdmiControlServiceSpy.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
+ mTestLooper.dispatchAll();
+ assertThat(mHdmiControlServiceSpy.audioSystem()).isNull();
+
+ // Enable the setting and check if the audio system local device is found in the network.
+ mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue(
+ HdmiControlManager.CEC_SETTING_NAME_SOUNDBAR_MODE,
+ HdmiControlManager.SOUNDBAR_MODE_ENABLED);
+ mTestLooper.dispatchAll();
+ assertThat(mHdmiControlServiceSpy.audioSystem()).isNotNull();
+ }
+
+ @Test
+ public void setSoundbarMode_disabled_removeAudioSystemLocalDevice() {
+ // Initialize the local devices excluding the audio system.
+ mHdmiControlServiceSpy.clearCecLocalDevices();
+ mLocalDevices.remove(mAudioSystemDeviceSpy);
+ mHdmiControlServiceSpy.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
+ mTestLooper.dispatchAll();
+ assertThat(mHdmiControlServiceSpy.audioSystem()).isNull();
+
+ // Enable the setting and check if the audio system local device is found in the network.
+ mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue(
+ HdmiControlManager.CEC_SETTING_NAME_SOUNDBAR_MODE,
+ HdmiControlManager.SOUNDBAR_MODE_ENABLED);
+ mTestLooper.dispatchAll();
+ assertThat(mHdmiControlServiceSpy.audioSystem()).isNotNull();
+
+ // Disable the setting and check if the audio system local device is removed from the
+ // network.
+ mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue(
+ HdmiControlManager.CEC_SETTING_NAME_SOUNDBAR_MODE,
+ HdmiControlManager.SOUNDBAR_MODE_DISABLED);
+ mTestLooper.dispatchAll();
+ assertThat(mHdmiControlServiceSpy.audioSystem()).isNull();
+ }
+
protected static class MockPlaybackDevice extends HdmiCecLocalDevicePlayback {
private boolean mCanGoToStandby;
diff --git a/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt b/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt
index 6590a2b..ecd9d89 100644
--- a/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt
+++ b/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt
@@ -16,6 +16,7 @@
package com.android.server.input
+import android.bluetooth.BluetoothDevice
import android.content.Context
import android.content.ContextWrapper
import android.hardware.BatteryState.STATUS_CHARGING
@@ -33,6 +34,8 @@
import android.platform.test.annotations.Presubmit
import android.view.InputDevice
import androidx.test.InstrumentationRegistry
+import com.android.server.input.BatteryController.BluetoothBatteryManager
+import com.android.server.input.BatteryController.BluetoothBatteryManager.BluetoothBatteryListener
import com.android.server.input.BatteryController.POLLING_PERIOD_MILLIS
import com.android.server.input.BatteryController.UEventManager
import com.android.server.input.BatteryController.UEventManager.UEventBatteryListener
@@ -52,6 +55,7 @@
import org.junit.Rule
import org.junit.Test
import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.notNull
import org.mockito.Mock
import org.mockito.Mockito.anyInt
@@ -172,6 +176,8 @@
const val SECOND_DEVICE_ID = 11
const val USI_DEVICE_ID = 101
const val SECOND_USI_DEVICE_ID = 102
+ const val BT_DEVICE_ID = 100001
+ const val SECOND_BT_DEVICE_ID = 100002
const val TIMESTAMP = 123456789L
}
@@ -184,6 +190,8 @@
private lateinit var iInputManager: IInputManager
@Mock
private lateinit var uEventManager: UEventManager
+ @Mock
+ private lateinit var bluetoothBatteryManager: BluetoothBatteryManager
private lateinit var batteryController: BatteryController
private lateinit var context: Context
@@ -203,11 +211,13 @@
addInputDevice(DEVICE_ID)
addInputDevice(SECOND_DEVICE_ID)
- batteryController = BatteryController(context, native, testLooper.looper, uEventManager)
+ batteryController = BatteryController(context, native, testLooper.looper, uEventManager,
+ bluetoothBatteryManager)
batteryController.systemRunning()
val listenerCaptor = ArgumentCaptor.forClass(IInputDevicesChangedListener::class.java)
verify(iInputManager).registerInputDevicesChangedListener(listenerCaptor.capture())
devicesChangedListener = listenerCaptor.value
+ testLooper.dispatchAll()
}
private fun notifyDeviceChanged(
@@ -230,7 +240,7 @@
private fun addInputDevice(
deviceId: Int,
hasBattery: Boolean = true,
- supportsUsi: Boolean = false
+ supportsUsi: Boolean = false,
) {
deviceGenerationMap[deviceId] = 0
notifyDeviceChanged(deviceId, hasBattery, supportsUsi)
@@ -634,4 +644,125 @@
assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID),
matchesState(USI_DEVICE_ID, status = STATUS_UNKNOWN, capacity = 0f))
}
+
+ @Test
+ fun testRegisterBluetoothListenerForMonitoredBluetoothDevices() {
+ `when`(iInputManager.getInputDeviceBluetoothAddress(BT_DEVICE_ID))
+ .thenReturn("AA:BB:CC:DD:EE:FF")
+ `when`(iInputManager.getInputDeviceBluetoothAddress(SECOND_BT_DEVICE_ID))
+ .thenReturn("11:22:33:44:55:66")
+ addInputDevice(BT_DEVICE_ID)
+ testLooper.dispatchNext()
+ addInputDevice(SECOND_BT_DEVICE_ID)
+ testLooper.dispatchNext()
+
+ // Ensure that a BT battery listener is not added when there are no monitored BT devices.
+ verify(bluetoothBatteryManager, never()).addListener(any())
+
+ val bluetoothListener = ArgumentCaptor.forClass(BluetoothBatteryListener::class.java)
+ val listener = createMockListener()
+
+ // The BT battery listener is added when the first BT input device is monitored.
+ batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID)
+ verify(bluetoothBatteryManager).addListener(bluetoothListener.capture())
+
+ // The BT listener is only added once for all BT devices.
+ batteryController.registerBatteryListener(SECOND_BT_DEVICE_ID, listener, PID)
+ verify(bluetoothBatteryManager, times(1)).addListener(any())
+
+ // The BT listener is only removed when there are no monitored BT devices.
+ batteryController.unregisterBatteryListener(BT_DEVICE_ID, listener, PID)
+ verify(bluetoothBatteryManager, never()).removeListener(any())
+
+ `when`(iInputManager.getInputDeviceBluetoothAddress(SECOND_BT_DEVICE_ID))
+ .thenReturn(null)
+ notifyDeviceChanged(SECOND_BT_DEVICE_ID)
+ testLooper.dispatchNext()
+ verify(bluetoothBatteryManager).removeListener(bluetoothListener.value)
+ }
+
+ @Test
+ fun testNotifiesBluetoothBatteryChanges() {
+ `when`(iInputManager.getInputDeviceBluetoothAddress(BT_DEVICE_ID))
+ .thenReturn("AA:BB:CC:DD:EE:FF")
+ `when`(bluetoothBatteryManager.getBatteryLevel(eq("AA:BB:CC:DD:EE:FF"))).thenReturn(21)
+ addInputDevice(BT_DEVICE_ID)
+ val bluetoothListener = ArgumentCaptor.forClass(BluetoothBatteryListener::class.java)
+ val listener = createMockListener()
+ batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID)
+ verify(bluetoothBatteryManager).addListener(bluetoothListener.capture())
+ listener.verifyNotified(BT_DEVICE_ID, capacity = 0.21f)
+
+ // When the state has not changed, the listener is not notified again.
+ bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF")
+ listener.verifyNotified(BT_DEVICE_ID, mode = times(1), capacity = 0.21f)
+
+ `when`(bluetoothBatteryManager.getBatteryLevel(eq("AA:BB:CC:DD:EE:FF"))).thenReturn(25)
+ bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF")
+ listener.verifyNotified(BT_DEVICE_ID, capacity = 0.25f)
+ }
+
+ @Test
+ fun testBluetoothBatteryIsPrioritized() {
+ `when`(native.getBatteryDevicePath(BT_DEVICE_ID)).thenReturn("/sys/dev/bt_device")
+ `when`(iInputManager.getInputDeviceBluetoothAddress(BT_DEVICE_ID))
+ .thenReturn("AA:BB:CC:DD:EE:FF")
+ `when`(bluetoothBatteryManager.getBatteryLevel(eq("AA:BB:CC:DD:EE:FF"))).thenReturn(21)
+ `when`(native.getBatteryCapacity(BT_DEVICE_ID)).thenReturn(98)
+ addInputDevice(BT_DEVICE_ID)
+ val bluetoothListener = ArgumentCaptor.forClass(BluetoothBatteryListener::class.java)
+ val listener = createMockListener()
+ val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java)
+
+ // When the device is first monitored and both native and BT battery is available,
+ // the latter is used.
+ batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID)
+ verify(bluetoothBatteryManager).addListener(bluetoothListener.capture())
+ verify(uEventManager).addListener(uEventListener.capture(), any())
+ listener.verifyNotified(BT_DEVICE_ID, capacity = 0.21f)
+ assertThat("battery state matches", batteryController.getBatteryState(BT_DEVICE_ID),
+ matchesState(BT_DEVICE_ID, capacity = 0.21f))
+
+ // If only the native battery state changes the listener is not notified.
+ `when`(native.getBatteryCapacity(BT_DEVICE_ID)).thenReturn(97)
+ uEventListener.value!!.onBatteryUEvent(TIMESTAMP)
+ listener.verifyNotified(BT_DEVICE_ID, mode = times(1), capacity = 0.21f)
+ assertThat("battery state matches", batteryController.getBatteryState(BT_DEVICE_ID),
+ matchesState(BT_DEVICE_ID, capacity = 0.21f))
+ }
+
+ @Test
+ fun testFallBackToNativeBatteryStateWhenBluetoothStateInvalid() {
+ `when`(native.getBatteryDevicePath(BT_DEVICE_ID)).thenReturn("/sys/dev/bt_device")
+ `when`(iInputManager.getInputDeviceBluetoothAddress(BT_DEVICE_ID))
+ .thenReturn("AA:BB:CC:DD:EE:FF")
+ `when`(bluetoothBatteryManager.getBatteryLevel(eq("AA:BB:CC:DD:EE:FF"))).thenReturn(21)
+ `when`(native.getBatteryCapacity(BT_DEVICE_ID)).thenReturn(98)
+ addInputDevice(BT_DEVICE_ID)
+ val bluetoothListener = ArgumentCaptor.forClass(BluetoothBatteryListener::class.java)
+ val listener = createMockListener()
+ val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java)
+
+ batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID)
+ verify(bluetoothBatteryManager).addListener(bluetoothListener.capture())
+ verify(uEventManager).addListener(uEventListener.capture(), any())
+ listener.verifyNotified(BT_DEVICE_ID, capacity = 0.21f)
+
+ // Fall back to the native state when BT is off.
+ `when`(bluetoothBatteryManager.getBatteryLevel(eq("AA:BB:CC:DD:EE:FF")))
+ .thenReturn(BluetoothDevice.BATTERY_LEVEL_BLUETOOTH_OFF)
+ bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF")
+ listener.verifyNotified(BT_DEVICE_ID, capacity = 0.98f)
+
+ `when`(bluetoothBatteryManager.getBatteryLevel(eq("AA:BB:CC:DD:EE:FF"))).thenReturn(22)
+ bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF")
+ verify(bluetoothBatteryManager).addListener(bluetoothListener.capture())
+ listener.verifyNotified(BT_DEVICE_ID, capacity = 0.22f)
+
+ // Fall back to the native state when BT battery is unknown.
+ `when`(bluetoothBatteryManager.getBatteryLevel(eq("AA:BB:CC:DD:EE:FF")))
+ .thenReturn(BluetoothDevice.BATTERY_LEVEL_UNKNOWN)
+ bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF")
+ listener.verifyNotified(BT_DEVICE_ID, mode = times(2), capacity = 0.98f)
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java
index b991c5a..74efdb5 100644
--- a/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java
@@ -880,7 +880,7 @@
TimeZoneDetectorStatus expectedInitialDetectorStatus = new TimeZoneDetectorStatus(
DETECTOR_STATUS_RUNNING,
TELEPHONY_ALGORITHM_RUNNING_STATUS,
- LocationTimeZoneAlgorithmStatus.UNKNOWN);
+ LocationTimeZoneAlgorithmStatus.RUNNING_NOT_REPORTED);
script.verifyCachedDetectorStatus(expectedInitialDetectorStatus);
LocationTimeZoneAlgorithmStatus algorithmStatus1 = new LocationTimeZoneAlgorithmStatus(
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index afec085..d54d1fe 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -1101,10 +1101,6 @@
new NotificationChannel("id", "name", IMPORTANCE_HIGH);
mBinderService.updateNotificationChannelForPackage(PKG, mUid, updatedChannel);
- // pretend only this following part is called by the app (system permissions are required to
- // update the notification channel on behalf of the user above)
- mService.isSystemUid = false;
-
// Recreating with a lower importance leaves channel unchanged.
final NotificationChannel dupeChannel =
new NotificationChannel("id", "name", NotificationManager.IMPORTANCE_LOW);
@@ -1130,46 +1126,6 @@
}
@Test
- public void testCreateNotificationChannels_fromAppCannotSetFields() throws Exception {
- // Confirm that when createNotificationChannels is called from the relevant app and not
- // system, then it cannot set fields that can't be set by apps
- mService.isSystemUid = false;
-
- final NotificationChannel channel =
- new NotificationChannel("id", "name", IMPORTANCE_DEFAULT);
- channel.setBypassDnd(true);
- channel.setAllowBubbles(true);
-
- mBinderService.createNotificationChannels(PKG,
- new ParceledListSlice(Arrays.asList(channel)));
-
- final NotificationChannel createdChannel =
- mBinderService.getNotificationChannel(PKG, mContext.getUserId(), PKG, "id");
- assertFalse(createdChannel.canBypassDnd());
- assertFalse(createdChannel.canBubble());
- }
-
- @Test
- public void testCreateNotificationChannels_fromSystemCanSetFields() throws Exception {
- // Confirm that when createNotificationChannels is called from system,
- // then it can set fields that can't be set by apps
- mService.isSystemUid = true;
-
- final NotificationChannel channel =
- new NotificationChannel("id", "name", IMPORTANCE_DEFAULT);
- channel.setBypassDnd(true);
- channel.setAllowBubbles(true);
-
- mBinderService.createNotificationChannels(PKG,
- new ParceledListSlice(Arrays.asList(channel)));
-
- final NotificationChannel createdChannel =
- mBinderService.getNotificationChannel(PKG, mContext.getUserId(), PKG, "id");
- assertTrue(createdChannel.canBypassDnd());
- assertTrue(createdChannel.canBubble());
- }
-
- @Test
public void testBlockedNotifications_suspended() throws Exception {
when(mPackageManager.isPackageSuspendedForUser(anyString(), anyInt())).thenReturn(true);
@@ -3132,8 +3088,6 @@
@Test
public void testDeleteChannelGroupChecksForFgses() throws Exception {
- // the setup for this test requires it to seem like it's coming from the app
- mService.isSystemUid = false;
when(mCompanionMgr.getAssociations(PKG, UserHandle.getUserId(mUid)))
.thenReturn(singletonList(mock(AssociationInfo.class)));
CountDownLatch latch = new CountDownLatch(2);
@@ -3146,7 +3100,7 @@
ParceledListSlice<NotificationChannel> pls =
new ParceledListSlice(ImmutableList.of(notificationChannel));
try {
- mBinderService.createNotificationChannels(PKG, pls);
+ mBinderService.createNotificationChannelsForPackage(PKG, mUid, pls);
} catch (RemoteException e) {
throw new RuntimeException(e);
}
@@ -3165,10 +3119,8 @@
ParceledListSlice<NotificationChannel> pls =
new ParceledListSlice(ImmutableList.of(notificationChannel));
try {
- // Because existing channels won't have their groups overwritten when the call
- // is from the app, this call won't take the channel out of the group
- mBinderService.createNotificationChannels(PKG, pls);
- mBinderService.deleteNotificationChannelGroup(PKG, "group");
+ mBinderService.createNotificationChannelsForPackage(PKG, mUid, pls);
+ mBinderService.deleteNotificationChannelGroup(PKG, "group");
} catch (RemoteException e) {
throw new RuntimeException(e);
}
@@ -8729,7 +8681,7 @@
assertEquals("friend", friendChannel.getConversationId());
assertEquals(null, original.getConversationId());
assertEquals(original.canShowBadge(), friendChannel.canShowBadge());
- assertEquals(original.canBubble(), friendChannel.canBubble()); // called by system
+ assertFalse(friendChannel.canBubble()); // can't be modified by app
assertFalse(original.getId().equals(friendChannel.getId()));
assertNotNull(friendChannel.getId());
}
diff --git a/telephony/common/com/android/internal/telephony/SmsApplication.java b/telephony/common/com/android/internal/telephony/SmsApplication.java
index f848c40..a9cdf7e 100644
--- a/telephony/common/com/android/internal/telephony/SmsApplication.java
+++ b/telephony/common/com/android/internal/telephony/SmsApplication.java
@@ -210,6 +210,15 @@
}
/**
+ * Returns the userHandle of the current process, if called from a system app,
+ * otherwise it returns the caller's userHandle
+ * @return userHandle of the caller.
+ */
+ private static UserHandle getIncomingUserHandle() {
+ return UserHandle.of(getIncomingUserId());
+ }
+
+ /**
* Returns the list of available SMS apps defined as apps that are registered for both the
* SMS_RECEIVED_ACTION (SMS) and WAP_PUSH_RECEIVED_ACTION (MMS) broadcasts (and their broadcast
* receivers are enabled)
@@ -951,24 +960,28 @@
*/
@UnsupportedAppUsage
public static ComponentName getDefaultSmsApplication(Context context, boolean updateIfNeeded) {
- return getDefaultSmsApplicationAsUser(context, updateIfNeeded, getIncomingUserId());
+ return getDefaultSmsApplicationAsUser(context, updateIfNeeded, getIncomingUserHandle());
}
/**
* Gets the default SMS application on a given user
* @param context context from the calling app
* @param updateIfNeeded update the default app if there is no valid default app configured.
- * @param userId target user ID.
+ * @param userHandle target user handle
+ * if {@code null} is passed in then calling package uid is used to find out target user handle.
* @return component name of the app and class to deliver SMS messages to
*/
- @VisibleForTesting
public static ComponentName getDefaultSmsApplicationAsUser(Context context,
- boolean updateIfNeeded, int userId) {
+ boolean updateIfNeeded, @Nullable UserHandle userHandle) {
+ if (userHandle == null) {
+ userHandle = getIncomingUserHandle();
+ }
+
final long token = Binder.clearCallingIdentity();
try {
ComponentName component = null;
SmsApplicationData smsApplicationData = getApplication(context, updateIfNeeded,
- userId);
+ userHandle.getIdentifier());
if (smsApplicationData != null) {
component = new ComponentName(smsApplicationData.mPackageName,
smsApplicationData.mSmsReceiverClass);
@@ -987,23 +1000,28 @@
*/
@UnsupportedAppUsage
public static ComponentName getDefaultMmsApplication(Context context, boolean updateIfNeeded) {
- return getDefaultMmsApplicationAsUser(context, updateIfNeeded, getIncomingUserId());
+ return getDefaultMmsApplicationAsUser(context, updateIfNeeded, getIncomingUserHandle());
}
/**
* Gets the default MMS application on a given user
* @param context context from the calling app
* @param updateIfNeeded update the default app if there is no valid default app configured.
- * @param userId target user ID.
+ * @param userHandle target user handle
+ * if {@code null} is passed in then calling package uid is used to find out target user handle.
* @return component name of the app and class to deliver MMS messages to.
*/
public static ComponentName getDefaultMmsApplicationAsUser(Context context,
- boolean updateIfNeeded, int userId) {
+ boolean updateIfNeeded, @Nullable UserHandle userHandle) {
+ if (userHandle == null) {
+ userHandle = getIncomingUserHandle();
+ }
+
final long token = Binder.clearCallingIdentity();
try {
ComponentName component = null;
SmsApplicationData smsApplicationData = getApplication(context, updateIfNeeded,
- userId);
+ userHandle.getIdentifier());
if (smsApplicationData != null) {
component = new ComponentName(smsApplicationData.mPackageName,
smsApplicationData.mMmsReceiverClass);
@@ -1024,23 +1042,28 @@
public static ComponentName getDefaultRespondViaMessageApplication(Context context,
boolean updateIfNeeded) {
return getDefaultRespondViaMessageApplicationAsUser(context, updateIfNeeded,
- getIncomingUserId());
+ getIncomingUserHandle());
}
/**
* Gets the default Respond Via Message application on a given user
* @param context context from the calling app
* @param updateIfNeeded update the default app if there is no valid default app configured
- * @param userId target user ID.
+ * @param userHandle target user handle
+ * if {@code null} is passed in then calling package uid is used to find out target user handle.
* @return component name of the app and class to direct Respond Via Message intent to
*/
public static ComponentName getDefaultRespondViaMessageApplicationAsUser(Context context,
- boolean updateIfNeeded, int userId) {
+ boolean updateIfNeeded, @Nullable UserHandle userHandle) {
+ if (userHandle == null) {
+ userHandle = getIncomingUserHandle();
+ }
+
final long token = Binder.clearCallingIdentity();
try {
ComponentName component = null;
SmsApplicationData smsApplicationData = getApplication(context, updateIfNeeded,
- userId);
+ userHandle.getIdentifier());
if (smsApplicationData != null) {
component = new ComponentName(smsApplicationData.mPackageName,
smsApplicationData.mRespondViaMessageClass);
@@ -1062,6 +1085,7 @@
public static ComponentName getDefaultSendToApplication(Context context,
boolean updateIfNeeded) {
int userId = getIncomingUserId();
+
final long token = Binder.clearCallingIdentity();
try {
ComponentName component = null;
@@ -1087,7 +1111,7 @@
public static ComponentName getDefaultExternalTelephonyProviderChangedApplication(
Context context, boolean updateIfNeeded) {
return getDefaultExternalTelephonyProviderChangedApplicationAsUser(context, updateIfNeeded,
- getIncomingUserId());
+ getIncomingUserHandle());
}
/**
@@ -1095,16 +1119,21 @@
* MmsProvider on a given user.
* @param context context from the calling app
* @param updateIfNeeded update the default app if there is no valid default app configured
- * @param userId target user ID.
+ * @param userHandle target user handle
+ * if {@code null} is passed in then calling package uid is used to find out target user handle.
* @return component name of the app and class to deliver change intents to.
*/
public static ComponentName getDefaultExternalTelephonyProviderChangedApplicationAsUser(
- Context context, boolean updateIfNeeded, int userId) {
+ Context context, boolean updateIfNeeded, @Nullable UserHandle userHandle) {
+ if (userHandle == null) {
+ userHandle = getIncomingUserHandle();
+ }
+
final long token = Binder.clearCallingIdentity();
try {
ComponentName component = null;
SmsApplicationData smsApplicationData = getApplication(context, updateIfNeeded,
- userId);
+ userHandle.getIdentifier());
if (smsApplicationData != null
&& smsApplicationData.mProviderChangedReceiverClass != null) {
component = new ComponentName(smsApplicationData.mPackageName,
@@ -1124,23 +1153,28 @@
*/
public static ComponentName getDefaultSimFullApplication(
Context context, boolean updateIfNeeded) {
- return getDefaultSimFullApplicationAsUser(context, updateIfNeeded, getIncomingUserId());
+ return getDefaultSimFullApplicationAsUser(context, updateIfNeeded, getIncomingUserHandle());
}
/**
* Gets the default application that handles sim full event on a given user.
* @param context context from the calling app
* @param updateIfNeeded update the default app if there is no valid default app configured
- * @param userId target user ID.
+ * @param userHandle target user handle
+ * if {@code null} is passed in then calling package uid is used to find out target user handle.
* @return component name of the app and class to deliver change intents to
*/
public static ComponentName getDefaultSimFullApplicationAsUser(Context context,
- boolean updateIfNeeded, int userId) {
+ boolean updateIfNeeded, @Nullable UserHandle userHandle) {
+ if (userHandle == null) {
+ userHandle = getIncomingUserHandle();
+ }
+
final long token = Binder.clearCallingIdentity();
try {
ComponentName component = null;
SmsApplicationData smsApplicationData = getApplication(context, updateIfNeeded,
- userId);
+ userHandle.getIdentifier());
if (smsApplicationData != null
&& smsApplicationData.mSimFullReceiverClass != null) {
component = new ComponentName(smsApplicationData.mPackageName,
@@ -1153,19 +1187,35 @@
}
/**
- * Returns whether need to wrgetIncomingUserIdite the SMS message to SMS database for this
- * package.
+ * Returns whether it is required to write the SMS message to SMS database for this package.
+ *
+ * @param packageName the name of the package to be checked
+ * @param context context from the calling app
+ * @return true if it is required to write SMS message to SMS database for this package.
+ *
* <p>
* Caller must pass in the correct user context if calling from a singleton service.
*/
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public static boolean shouldWriteMessageForPackage(String packageName, Context context) {
- return !shouldWriteMessageForPackageAsUser(packageName, context, getIncomingUserId());
+ return !shouldWriteMessageForPackageAsUser(packageName, context, getIncomingUserHandle());
}
+ /**
+ * Returns whether it is required to write the SMS message to SMS database for this package.
+ *
+ * @param packageName the name of the package to be checked
+ * @param context context from the calling app
+ * @param userHandle target user handle
+ * if {@code null} is passed in then calling package uid is used to find out target user handle.
+ * @return true if it is required to write SMS message to SMS database for this package.
+ *
+ * <p>
+ * Caller must pass in the correct user context if calling from a singleton service.
+ */
public static boolean shouldWriteMessageForPackageAsUser(String packageName, Context context,
- int userId) {
- return !isDefaultSmsApplicationAsUser(context, packageName, userId);
+ @Nullable UserHandle userHandle) {
+ return !isDefaultSmsApplicationAsUser(context, packageName, userHandle);
}
/**
@@ -1177,7 +1227,7 @@
*/
@UnsupportedAppUsage
public static boolean isDefaultSmsApplication(Context context, String packageName) {
- return isDefaultSmsApplicationAsUser(context, packageName, getIncomingUserId());
+ return isDefaultSmsApplicationAsUser(context, packageName, getIncomingUserHandle());
}
/**
@@ -1185,16 +1235,22 @@
*
* @param context context from the calling app
* @param packageName the name of the package to be checked
- * @param userId target user ID.
+ * @param userHandle target user handle
+ * if {@code null} is passed in then calling package uid is used to find out target user handle.
* @return true if the package is default sms app or bluetooth
*/
public static boolean isDefaultSmsApplicationAsUser(Context context, String packageName,
- int userId) {
+ @Nullable UserHandle userHandle) {
if (packageName == null) {
return false;
}
+
+ if (userHandle == null) {
+ userHandle = getIncomingUserHandle();
+ }
+
ComponentName component = getDefaultSmsApplicationAsUser(context, false,
- userId);
+ userHandle);
if (component == null) {
return false;
}
@@ -1222,7 +1278,7 @@
*/
@UnsupportedAppUsage
public static boolean isDefaultMmsApplication(Context context, String packageName) {
- return isDefaultMmsApplicationAsUser(context, packageName, getIncomingUserId());
+ return isDefaultMmsApplicationAsUser(context, packageName, getIncomingUserHandle());
}
/**
@@ -1230,17 +1286,22 @@
*
* @param context context from the calling app
* @param packageName the name of the package to be checked
- * @param userId target user ID.
+ * @param userHandle target user handle
+ * if {@code null} is passed in then calling package uid is used to find out target user handle.
* @return true if the package is default mms app or bluetooth
*/
public static boolean isDefaultMmsApplicationAsUser(Context context, String packageName,
- int userId) {
+ @Nullable UserHandle userHandle) {
if (packageName == null) {
return false;
}
+ if (userHandle == null) {
+ userHandle = getIncomingUserHandle();
+ }
+
ComponentName component = getDefaultMmsApplicationAsUser(context, false,
- userId);
+ userHandle);
if (component == null) {
return false;
}
diff --git a/telephony/common/com/android/internal/telephony/util/TelephonyUtils.java b/telephony/common/com/android/internal/telephony/util/TelephonyUtils.java
index 76d2b7d..3dc7111 100644
--- a/telephony/common/com/android/internal/telephony/util/TelephonyUtils.java
+++ b/telephony/common/com/android/internal/telephony/util/TelephonyUtils.java
@@ -27,6 +27,8 @@
import android.os.Bundle;
import android.os.PersistableBundle;
import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import java.io.PrintWriter;
@@ -212,4 +214,30 @@
return "UNKNOWN(" + mobileDataPolicy + ")";
}
}
-}
+
+ /**
+ * Utility method to get user handle associated with this subscription.
+ *
+ * This method should be used internally as it returns null instead of throwing
+ * IllegalArgumentException or IllegalStateException.
+ *
+ * @param context Context object
+ * @param subId the subId of the subscription.
+ * @return userHandle associated with this subscription
+ * or {@code null} if:
+ * 1. subscription is not associated with any user
+ * 2. subId is invalid.
+ * 3. subscription service is not available.
+ *
+ * @throws SecurityException if the caller doesn't have permissions required.
+ */
+ @Nullable
+ public static UserHandle getSubscriptionUserHandle(Context context, int subId) {
+ UserHandle userHandle = null;
+ SubscriptionManager subManager = context.getSystemService(SubscriptionManager.class);
+ if ((subManager != null) && (SubscriptionManager.isValidSubscriptionId(subId))) {
+ userHandle = subManager.getSubscriptionUserHandle(subId);
+ }
+ return userHandle;
+ }
+}
\ No newline at end of file
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index 4ce2ca1..5c1d497 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -1663,17 +1663,33 @@
}
/**
- * @return List of all SubscriptionInfo records in database,
- * include those that were inserted before, maybe empty but not null.
+ * Get all subscription info records from SIMs that are inserted now or were inserted before.
+ *
+ * <p>
+ * If the caller does not have {@link Manifest.permission#READ_PHONE_NUMBERS} permission,
+ * {@link SubscriptionInfo#getNumber()} will return empty string.
+ * If the caller does not have {@link Manifest.permission#USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER},
+ * {@link SubscriptionInfo#getIccId()} and {@link SubscriptionInfo#getCardString()} will return
+ * empty string, and {@link SubscriptionInfo#getGroupUuid()} will return {@code null}.
+ *
+ * <p>
+ * The carrier app will always have full {@link SubscriptionInfo} for the subscriptions
+ * that it has carrier privilege.
+ *
+ * @return List of all {@link SubscriptionInfo} records from SIMs that are inserted or
+ * inserted before. Sorted by {@link SubscriptionInfo#getSimSlotIndex()}, then
+ * {@link SubscriptionInfo#getSubscriptionId()}.
+ *
* @hide
*/
+ @RequiresPermission(anyOf = {
+ Manifest.permission.READ_PHONE_STATE,
+ Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
+ "carrier privileges",
+ })
@NonNull
- @UnsupportedAppUsage
public List<SubscriptionInfo> getAllSubscriptionInfoList() {
- if (VDBG) logd("[getAllSubscriptionInfoList]+");
-
List<SubscriptionInfo> result = null;
-
try {
ISub iSub = TelephonyManager.getSubscriptionService();
if (iSub != null) {
@@ -3424,7 +3440,6 @@
/**
* Get subscriptionInfo list of subscriptions that are in the same group of given subId.
- * See {@link #createSubscriptionGroup(List)} for more details.
*
* Caller must have {@link android.Manifest.permission#READ_PHONE_STATE}
* or carrier privilege permission on the subscription.
@@ -4125,6 +4140,26 @@
}
/**
+ * Convert phone number source to string.
+ *
+ * @param source The phone name source.
+ *
+ * @return The phone name source in string format.
+ *
+ * @hide
+ */
+ @NonNull
+ public static String phoneNumberSourceToString(@PhoneNumberSource int source) {
+ switch (source) {
+ case SubscriptionManager.PHONE_NUMBER_SOURCE_UICC: return "UICC";
+ case SubscriptionManager.PHONE_NUMBER_SOURCE_CARRIER: return "CARRIER";
+ case SubscriptionManager.PHONE_NUMBER_SOURCE_IMS: return "IMS";
+ default:
+ return "UNKNOWN(" + source + ")";
+ }
+ }
+
+ /**
* Convert display name source to string.
*
* @param source The display name source.
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index d3ddb1b..3024b89 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -71,6 +71,7 @@
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.SystemProperties;
+import android.os.UserHandle;
import android.os.WorkSource;
import android.provider.Settings.SettingNotFoundException;
import android.service.carrier.CarrierIdentifier;
@@ -11934,8 +11935,9 @@
}
/**
- * Gets the default Respond Via Message application, updating the cache if there is no
- * respond-via-message application currently configured.
+ * Get the component name of the default app to direct respond-via-message intent for the
+ * user associated with this subscription, update the cache if there is no respond-via-message
+ * application currently configured for this user.
* @return component name of the app and class to direct Respond Via Message intent to, or
* {@code null} if the functionality is not supported.
* @hide
@@ -11944,11 +11946,20 @@
@RequiresPermission(Manifest.permission.INTERACT_ACROSS_USERS)
@RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public @Nullable ComponentName getAndUpdateDefaultRespondViaMessageApplication() {
- return SmsApplication.getDefaultRespondViaMessageApplication(mContext, true);
+ try {
+ ITelephony telephony = getITelephony();
+ if (telephony != null) {
+ return telephony.getDefaultRespondViaMessageApplication(getSubId(), true);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error in getAndUpdateDefaultRespondViaMessageApplication: " + e);
+ }
+ return null;
}
/**
- * Gets the default Respond Via Message application.
+ * Get the component name of the default app to direct respond-via-message intent for the
+ * user associated with this subscription.
* @return component name of the app and class to direct Respond Via Message intent to, or
* {@code null} if the functionality is not supported.
* @hide
@@ -11957,7 +11968,15 @@
@RequiresPermission(Manifest.permission.INTERACT_ACROSS_USERS)
@RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public @Nullable ComponentName getDefaultRespondViaMessageApplication() {
- return SmsApplication.getDefaultRespondViaMessageApplication(mContext, false);
+ try {
+ ITelephony telephony = getITelephony();
+ if (telephony != null) {
+ return telephony.getDefaultRespondViaMessageApplication(getSubId(), false);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error in getDefaultRespondViaMessageApplication: " + e);
+ }
+ return null;
}
/**
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index abf4cde..616ea50 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -17,6 +17,7 @@
package com.android.internal.telephony;
import android.app.PendingIntent;
+import android.content.ComponentName;
import android.content.Intent;
import android.content.IntentSender;
import android.os.Bundle;
@@ -2618,4 +2619,14 @@
* @hide
*/
boolean isRemovableEsimDefaultEuicc(String callingPackage);
+
+ /**
+ * Get the component name of the default app to direct respond-via-message intent for the
+ * user associated with this subscription, update the cache if there is no respond-via-message
+ * application currently configured for this user.
+ * @return component name of the app and class to direct Respond Via Message intent to, or
+ * {@code null} if the functionality is not supported.
+ * @hide
+ */
+ ComponentName getDefaultRespondViaMessageApplication(int subId, boolean updateIfNeeded);
}
diff --git a/tests/TelephonyCommonTests/Android.bp b/tests/TelephonyCommonTests/Android.bp
index a9fbfd9..81ec265 100644
--- a/tests/TelephonyCommonTests/Android.bp
+++ b/tests/TelephonyCommonTests/Android.bp
@@ -47,7 +47,7 @@
// Uncomment this and comment out the jarjar rule if you want to attach a debugger and step
// through the renamed classes.
- // platform_apis: true,
+ platform_apis: true,
libs: [
"android.test.runner",
diff --git a/tests/TelephonyCommonTests/src/com/android/internal/telephony/tests/SmsApplicationTest.java b/tests/TelephonyCommonTests/src/com/android/internal/telephony/tests/SmsApplicationTest.java
index 7a2af72..adefac6 100644
--- a/tests/TelephonyCommonTests/src/com/android/internal/telephony/tests/SmsApplicationTest.java
+++ b/tests/TelephonyCommonTests/src/com/android/internal/telephony/tests/SmsApplicationTest.java
@@ -44,6 +44,7 @@
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
import android.net.Uri;
import android.os.Handler;
import android.os.UserHandle;
@@ -75,9 +76,12 @@
public class SmsApplicationTest {
private static final ComponentName TEST_COMPONENT_NAME =
ComponentName.unflattenFromString("com.android.test/.TestSmsApp");
+ public static final String BLUETOOTH_PACKAGE_NAME = "com.android.bluetooth.services";
private static final String MMS_RECEIVER_NAME = "TestMmsReceiver";
private static final String RESPOND_VIA_SMS_NAME = "TestRespondViaSmsHandler";
private static final String SEND_TO_NAME = "TestSendTo";
+ private static final String EXTERNAL_PROVIDER_CHANGE_NAME = "TestExternalProviderChangeHandler";
+ private static final String SIM_FULL_NAME = "TestSimFullHandler";
private static final int SMS_APP_UID = 10001;
private static final int FAKE_PHONE_UID = 10002;
@@ -102,6 +106,7 @@
}).collect(Collectors.toSet());
@Mock private Context mContext;
+ @Mock private Resources mResources;
@Mock private TelephonyManager mTelephonyManager;
@Mock private RoleManager mRoleManager;
@Mock private PackageManager mPackageManager;
@@ -118,6 +123,9 @@
when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager);
when(mContext.getSystemService(AppOpsManager.class)).thenReturn(mAppOpsManager);
when(mContext.createContextAsUser(isNotNull(), anyInt())).thenReturn(mContext);
+ when(mContext.getResources()).thenReturn(mResources);
+ when(mResources.getString(eq(com.android.internal.R.string.config_systemBluetoothStack)))
+ .thenReturn(BLUETOOTH_PACKAGE_NAME);
doAnswer(invocation -> getResolveInfosForIntent(invocation.getArgument(0)))
.when(mPackageManager)
@@ -146,24 +154,46 @@
}
}
+
@Test
- public void testGetDefaultSmsApplication() {
+ public void testGetDefaultSmsApplicationAsUser() {
assertEquals(TEST_COMPONENT_NAME,
- SmsApplication.getDefaultSmsApplicationAsUser(mContext, false, 0));
+ SmsApplication.getDefaultSmsApplicationAsUser(mContext, false,
+ UserHandle.SYSTEM));
+ }
+
+
+ @Test
+ public void testGetDefaultMmsApplicationAsUser() {
+ ComponentName componentName = SmsApplication.getDefaultMmsApplicationAsUser(mContext,
+ false, UserHandle.SYSTEM);
+ assertEquals(TEST_COMPONENT_NAME.getPackageName(), componentName.getPackageName());
+ assertEquals(MMS_RECEIVER_NAME, componentName.getClassName());
}
@Test
- public void testGetDefaultMmsApplication() {
- assertEquals(TEST_COMPONENT_NAME,
- SmsApplication.getDefaultMmsApplicationAsUser(mContext, false,
- UserHandle.USER_SYSTEM));
+ public void testGetDefaultExternalTelephonyProviderChangedApplicationAsUser() {
+ ComponentName componentName = SmsApplication
+ .getDefaultExternalTelephonyProviderChangedApplicationAsUser(mContext,
+ false, UserHandle.SYSTEM);
+ assertEquals(TEST_COMPONENT_NAME.getPackageName(), componentName.getPackageName());
+ assertEquals(EXTERNAL_PROVIDER_CHANGE_NAME, componentName.getClassName());
}
@Test
- public void testGetDefaultExternalTelephonyProviderChangedApplication() {
- assertEquals(TEST_COMPONENT_NAME,
- SmsApplication.getDefaultExternalTelephonyProviderChangedApplicationAsUser(mContext,
- false, UserHandle.USER_SYSTEM));
+ public void testGetDefaultRespondViaMessageApplicationAsUserAsUser() {
+ ComponentName componentName = SmsApplication.getDefaultRespondViaMessageApplicationAsUser(
+ mContext, false, UserHandle.SYSTEM);
+ assertEquals(TEST_COMPONENT_NAME.getPackageName(), componentName.getPackageName());
+ assertEquals(RESPOND_VIA_SMS_NAME, componentName.getClassName());
+ }
+
+ @Test
+ public void testGetDefaultSimFullApplicationAsUser() {
+ ComponentName componentName = SmsApplication.getDefaultSimFullApplicationAsUser(mContext,
+ false, UserHandle.SYSTEM);
+ assertEquals(TEST_COMPONENT_NAME.getPackageName(), componentName.getPackageName());
+ assertEquals(SIM_FULL_NAME, componentName.getClassName());
}
@Test
@@ -174,7 +204,8 @@
setupPackageInfosForCoreApps();
assertEquals(TEST_COMPONENT_NAME,
- SmsApplication.getDefaultSmsApplicationAsUser(mContext, true, 0));
+ SmsApplication.getDefaultSmsApplicationAsUser(mContext, true,
+ UserHandle.SYSTEM));
verify(mAppOpsManager, atLeastOnce()).setUidMode(AppOpsManager.OPSTR_READ_SMS, SMS_APP_UID,
AppOpsManager.MODE_ALLOWED);
}
@@ -251,6 +282,10 @@
return Collections.singletonList(makeRespondViaMessageResolveInfo());
case Intent.ACTION_SENDTO:
return Collections.singletonList(makeSendToResolveInfo());
+ case Telephony.Sms.Intents.ACTION_EXTERNAL_PROVIDER_CHANGE:
+ return Collections.singletonList(makeExternalProviderChangeResolveInfo());
+ case Telephony.Sms.Intents.SIM_FULL_ACTION:
+ return Collections.singletonList(makeSimFullResolveInfo());
}
return Collections.emptyList();
}
@@ -308,4 +343,26 @@
info.activityInfo = activityInfo;
return info;
}
+
+ private ResolveInfo makeExternalProviderChangeResolveInfo() {
+ ResolveInfo info = new ResolveInfo();
+ ActivityInfo activityInfo = new ActivityInfo();
+
+ activityInfo.packageName = TEST_COMPONENT_NAME.getPackageName();
+ activityInfo.name = EXTERNAL_PROVIDER_CHANGE_NAME;
+
+ info.activityInfo = activityInfo;
+ return info;
+ }
+
+ private ResolveInfo makeSimFullResolveInfo() {
+ ResolveInfo info = new ResolveInfo();
+ ActivityInfo activityInfo = new ActivityInfo();
+
+ activityInfo.packageName = TEST_COMPONENT_NAME.getPackageName();
+ activityInfo.name = SIM_FULL_NAME;
+
+ info.activityInfo = activityInfo;
+ return info;
+ }
}
diff --git a/tests/TelephonyCommonTests/src/com/android/internal/telephony/tests/TelephonyUtilsTest.java b/tests/TelephonyCommonTests/src/com/android/internal/telephony/tests/TelephonyUtilsTest.java
new file mode 100644
index 0000000..a62103e
--- /dev/null
+++ b/tests/TelephonyCommonTests/src/com/android/internal/telephony/tests/TelephonyUtilsTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.telephony.tests;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.telephony.SubscriptionManager;
+
+import com.android.internal.telephony.util.TelephonyUtils;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+public class TelephonyUtilsTest {
+ @Rule
+ public final MockitoRule mockito = MockitoJUnit.rule();
+
+ // Mocked classes
+ @Mock
+ private Context mContext;
+ @Mock
+ private SubscriptionManager mSubscriptionManager;
+
+ @Before
+ public void setup() {
+ doReturn(mSubscriptionManager).when(mContext)
+ .getSystemService(eq(SubscriptionManager.class));
+ }
+
+
+ @Test
+ public void getSubscriptionUserHandle_subId_invalid() {
+ int invalidSubId = -10;
+ doReturn(false).when(mSubscriptionManager).isActiveSubscriptionId(eq(invalidSubId));
+
+ TelephonyUtils.getSubscriptionUserHandle(mContext, invalidSubId);
+
+ // getSubscriptionUserHandle should not be called if subID is inactive.
+ verify(mSubscriptionManager, never()).getSubscriptionUserHandle(eq(invalidSubId));
+ }
+
+ @Test
+ public void getSubscriptionUserHandle_subId_valid() {
+ int activeSubId = 1;
+ doReturn(true).when(mSubscriptionManager).isActiveSubscriptionId(eq(activeSubId));
+
+ TelephonyUtils.getSubscriptionUserHandle(mContext, activeSubId);
+
+ // getSubscriptionUserHandle should be called if subID is active.
+ verify(mSubscriptionManager, times(1)).getSubscriptionUserHandle(eq(activeSubId));
+ }
+}
+
+
diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt b/tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt
index 741655b..413e197 100644
--- a/tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt
+++ b/tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt
@@ -21,7 +21,7 @@
import com.android.tools.lint.detector.api.CURRENT_API
import com.google.android.lint.aidl.EnforcePermissionDetector
import com.google.android.lint.aidl.EnforcePermissionHelperDetector
-import com.google.android.lint.aidl.ManualPermissionCheckDetector
+import com.google.android.lint.aidl.SimpleManualPermissionEnforcementDetector
import com.google.android.lint.parcel.SaferParcelChecker
import com.google.auto.service.AutoService
@@ -40,7 +40,7 @@
EnforcePermissionDetector.ISSUE_MISSING_ENFORCE_PERMISSION,
EnforcePermissionDetector.ISSUE_MISMATCHING_ENFORCE_PERMISSION,
EnforcePermissionHelperDetector.ISSUE_ENFORCE_PERMISSION_HELPER,
- ManualPermissionCheckDetector.ISSUE_USE_ENFORCE_PERMISSION_ANNOTATION,
+ SimpleManualPermissionEnforcementDetector.ISSUE_USE_ENFORCE_PERMISSION_ANNOTATION,
SaferParcelChecker.ISSUE_UNSAFE_API_USAGE,
PackageVisibilityDetector.ISSUE_PACKAGE_NAME_NO_PACKAGE_VISIBILITY_FILTERS,
RegisterReceiverFlagDetector.ISSUE_RECEIVER_EXPORTED_FLAG,
diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/AidlImplementationDetector.kt b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/AidlImplementationDetector.kt
new file mode 100644
index 0000000..227cdcd
--- /dev/null
+++ b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/AidlImplementationDetector.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.lint.aidl
+
+import com.android.tools.lint.client.api.UElementHandler
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import org.jetbrains.uast.UBlockExpression
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UMethod
+
+/**
+ * Abstract class for detectors that look for methods implementing
+ * generated AIDL interface stubs
+ */
+abstract class AidlImplementationDetector : Detector(), SourceCodeScanner {
+ override fun getApplicableUastTypes(): List<Class<out UElement?>> =
+ listOf(UMethod::class.java)
+
+ override fun createUastHandler(context: JavaContext): UElementHandler = AidlStubHandler(context)
+
+ private inner class AidlStubHandler(val context: JavaContext) : UElementHandler() {
+ override fun visitMethod(node: UMethod) {
+ val interfaceName = getContainingAidlInterface(node)
+ .takeUnless(EXCLUDED_CPP_INTERFACES::contains) ?: return
+ val body = (node.uastBody as? UBlockExpression) ?: return
+ visitAidlMethod(context, node, interfaceName, body)
+ }
+ }
+
+ abstract fun visitAidlMethod(
+ context: JavaContext,
+ node: UMethod,
+ interfaceName: String,
+ body: UBlockExpression,
+ )
+}
diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/ManualPermissionCheckDetector.kt b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/ManualPermissionCheckDetector.kt
deleted file mode 100644
index 2c53f39..0000000
--- a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/ManualPermissionCheckDetector.kt
+++ /dev/null
@@ -1,158 +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.google.android.lint.aidl
-
-import com.android.tools.lint.client.api.UElementHandler
-import com.android.tools.lint.detector.api.Category
-import com.android.tools.lint.detector.api.Detector
-import com.android.tools.lint.detector.api.Implementation
-import com.android.tools.lint.detector.api.Issue
-import com.android.tools.lint.detector.api.JavaContext
-import com.android.tools.lint.detector.api.Scope
-import com.android.tools.lint.detector.api.Severity
-import com.android.tools.lint.detector.api.SourceCodeScanner
-import org.jetbrains.uast.UBlockExpression
-import org.jetbrains.uast.UCallExpression
-import org.jetbrains.uast.UElement
-import org.jetbrains.uast.UIfExpression
-import org.jetbrains.uast.UMethod
-import org.jetbrains.uast.UQualifiedReferenceExpression
-
-/**
- * Looks for methods implementing generated AIDL interface stubs
- * that can have simple permission checks migrated to
- * @EnforcePermission annotations
- *
- * TODO: b/242564870 (enable parse and autoFix of .aidl files)
- */
-@Suppress("UnstableApiUsage")
-class ManualPermissionCheckDetector : Detector(), SourceCodeScanner {
- override fun getApplicableUastTypes(): List<Class<out UElement?>> =
- listOf(UMethod::class.java)
-
- override fun createUastHandler(context: JavaContext): UElementHandler = AidlStubHandler(context)
-
- private inner class AidlStubHandler(val context: JavaContext) : UElementHandler() {
- override fun visitMethod(node: UMethod) {
- val interfaceName = getContainingAidlInterface(node)
- .takeUnless(EXCLUDED_CPP_INTERFACES::contains) ?: return
- val body = (node.uastBody as? UBlockExpression) ?: return
- val fix = accumulateSimplePermissionCheckFixes(body) ?: return
-
- val javaRemoveFixes = fix.locations.map {
- fix()
- .replace()
- .reformat(true)
- .range(it)
- .with("")
- .autoFix()
- .build()
- }
-
- val javaAnnotateFix = fix()
- .annotate(fix.annotation)
- .range(context.getLocation(node))
- .autoFix()
- .build()
-
- val message =
- "$interfaceName permission check can be converted to @EnforcePermission annotation"
-
- context.report(
- ISSUE_USE_ENFORCE_PERMISSION_ANNOTATION,
- fix.locations.last(),
- message,
- fix().composite(*javaRemoveFixes.toTypedArray(), javaAnnotateFix)
- )
- }
-
- /**
- * Walk the expressions in the method, looking for simple permission checks.
- *
- * If a single permission check is found at the beginning of the method,
- * this should be migrated to @EnforcePermission(value).
- *
- * If multiple consecutive permission checks are found,
- * these should be migrated to @EnforcePermission(allOf={value1, value2, ...})
- *
- * As soon as something other than a permission check is encountered, stop looking,
- * as some other business logic is happening that prevents an automated fix.
- */
- private fun accumulateSimplePermissionCheckFixes(methodBody: UBlockExpression):
- EnforcePermissionFix? {
- val singleFixes = mutableListOf<EnforcePermissionFix>()
- for (expression in methodBody.expressions) {
- singleFixes.add(getPermissionCheckFix(expression) ?: break)
- }
- return when (singleFixes.size) {
- 0 -> null
- 1 -> singleFixes[0]
- else -> EnforcePermissionFix.compose(singleFixes)
- }
- }
-
- /**
- * If an expression boils down to a permission check, return
- * the helper for creating a lint auto fix to @EnforcePermission
- */
- private fun getPermissionCheckFix(startingExpression: UElement?):
- EnforcePermissionFix? {
- return when (startingExpression) {
- is UQualifiedReferenceExpression -> getPermissionCheckFix(
- startingExpression.selector
- )
-
- is UIfExpression -> getPermissionCheckFix(startingExpression.condition)
-
- is UCallExpression -> return EnforcePermissionFix
- .fromCallExpression(context, startingExpression)
-
- else -> null
- }
- }
- }
-
- companion object {
-
- private val EXPLANATION = """
- Whenever possible, method implementations of AIDL interfaces should use the @EnforcePermission
- annotation to declare the permissions to be enforced. The verification code is then
- generated by the AIDL compiler, which also takes care of annotating the generated java
- code.
-
- This reduces the risk of bugs around these permission checks (that often become vulnerabilities).
- It also enables easier auditing and review.
-
- Please migrate to an @EnforcePermission annotation. (See: go/aidl-enforce-howto)
- """.trimIndent()
-
- @JvmField
- val ISSUE_USE_ENFORCE_PERMISSION_ANNOTATION = Issue.create(
- id = "UseEnforcePermissionAnnotation",
- briefDescription = "Manual permission check can be @EnforcePermission annotation",
- explanation = EXPLANATION,
- category = Category.SECURITY,
- priority = 5,
- severity = Severity.WARNING,
- implementation = Implementation(
- ManualPermissionCheckDetector::class.java,
- Scope.JAVA_FILE_SCOPE
- ),
- enabledByDefault = false, // TODO: enable once b/241171714 is resolved
- )
- }
-}
diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetector.kt b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetector.kt
new file mode 100644
index 0000000..4c0cbe7
--- /dev/null
+++ b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetector.kt
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.lint.aidl
+
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import org.jetbrains.uast.UBlockExpression
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UIfExpression
+import org.jetbrains.uast.UMethod
+import org.jetbrains.uast.UQualifiedReferenceExpression
+
+/**
+ * Looks for methods implementing generated AIDL interface stubs
+ * that can have simple permission checks migrated to
+ * @EnforcePermission annotations
+ *
+ * TODO: b/242564870 (enable parse and autoFix of .aidl files)
+ */
+@Suppress("UnstableApiUsage")
+class SimpleManualPermissionEnforcementDetector : AidlImplementationDetector() {
+ override fun visitAidlMethod(
+ context: JavaContext,
+ node: UMethod,
+ interfaceName: String,
+ body: UBlockExpression
+ ) {
+ val fix = accumulateSimplePermissionCheckFixes(body, context) ?: return
+
+ val javaRemoveFixes = fix.locations.map {
+ fix()
+ .replace()
+ .reformat(true)
+ .range(it)
+ .with("")
+ .autoFix()
+ .build()
+ }
+
+ val javaAnnotateFix = fix()
+ .annotate(fix.annotation)
+ .range(context.getLocation(node))
+ .autoFix()
+ .build()
+
+ context.report(
+ ISSUE_USE_ENFORCE_PERMISSION_ANNOTATION,
+ fix.locations.last(),
+ "$interfaceName permission check can be converted to @EnforcePermission annotation",
+ fix().composite(*javaRemoveFixes.toTypedArray(), javaAnnotateFix)
+ )
+ }
+
+ /**
+ * Walk the expressions in the method, looking for simple permission checks.
+ *
+ * If a single permission check is found at the beginning of the method,
+ * this should be migrated to @EnforcePermission(value).
+ *
+ * If multiple consecutive permission checks are found,
+ * these should be migrated to @EnforcePermission(allOf={value1, value2, ...})
+ *
+ * As soon as something other than a permission check is encountered, stop looking,
+ * as some other business logic is happening that prevents an automated fix.
+ */
+ private fun accumulateSimplePermissionCheckFixes(
+ methodBody: UBlockExpression,
+ context: JavaContext
+ ):
+ EnforcePermissionFix? {
+ val singleFixes = mutableListOf<EnforcePermissionFix>()
+ for (expression in methodBody.expressions) {
+ singleFixes.add(getPermissionCheckFix(expression, context) ?: break)
+ }
+ return when (singleFixes.size) {
+ 0 -> null
+ 1 -> singleFixes[0]
+ else -> EnforcePermissionFix.compose(singleFixes)
+ }
+ }
+
+ /**
+ * If an expression boils down to a permission check, return
+ * the helper for creating a lint auto fix to @EnforcePermission
+ */
+ private fun getPermissionCheckFix(startingExpression: UElement?, context: JavaContext):
+ EnforcePermissionFix? {
+ return when (startingExpression) {
+ is UQualifiedReferenceExpression -> getPermissionCheckFix(
+ startingExpression.selector, context
+ )
+
+ is UIfExpression -> getPermissionCheckFix(startingExpression.condition, context)
+
+ is UCallExpression -> return EnforcePermissionFix
+ .fromCallExpression(context, startingExpression)
+
+ else -> null
+ }
+ }
+
+ companion object {
+
+ private val EXPLANATION = """
+ Whenever possible, method implementations of AIDL interfaces should use the @EnforcePermission
+ annotation to declare the permissions to be enforced. The verification code is then
+ generated by the AIDL compiler, which also takes care of annotating the generated java
+ code.
+
+ This reduces the risk of bugs around these permission checks (that often become vulnerabilities).
+ It also enables easier auditing and review.
+
+ Please migrate to an @EnforcePermission annotation. (See: go/aidl-enforce-howto)
+ """.trimIndent()
+
+ @JvmField
+ val ISSUE_USE_ENFORCE_PERMISSION_ANNOTATION = Issue.create(
+ id = "SimpleManualPermissionEnforcement",
+ briefDescription = "Manual permission check can be @EnforcePermission annotation",
+ explanation = EXPLANATION,
+ category = Category.SECURITY,
+ priority = 5,
+ severity = Severity.WARNING,
+ implementation = Implementation(
+ SimpleManualPermissionEnforcementDetector::class.java,
+ Scope.JAVA_FILE_SCOPE
+ ),
+ enabledByDefault = false, // TODO: enable once b/241171714 is resolved
+ )
+ }
+}
diff --git a/tools/lint/checks/src/test/java/com/google/android/lint/aidl/ManualPermissionCheckDetectorTest.kt b/tools/lint/checks/src/test/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetectorTest.kt
similarity index 94%
rename from tools/lint/checks/src/test/java/com/google/android/lint/aidl/ManualPermissionCheckDetectorTest.kt
rename to tools/lint/checks/src/test/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetectorTest.kt
index d4a3497..150fc26 100644
--- a/tools/lint/checks/src/test/java/com/google/android/lint/aidl/ManualPermissionCheckDetectorTest.kt
+++ b/tools/lint/checks/src/test/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetectorTest.kt
@@ -23,10 +23,10 @@
import com.android.tools.lint.detector.api.Issue
@Suppress("UnstableApiUsage")
-class ManualPermissionCheckDetectorTest : LintDetectorTest() {
- override fun getDetector(): Detector = ManualPermissionCheckDetector()
+class SimpleManualPermissionEnforcementDetectorTest : LintDetectorTest() {
+ override fun getDetector(): Detector = SimpleManualPermissionEnforcementDetector()
override fun getIssues(): List<Issue> = listOf(
- ManualPermissionCheckDetector
+ SimpleManualPermissionEnforcementDetector
.ISSUE_USE_ENFORCE_PERMISSION_ANNOTATION
)
@@ -52,7 +52,7 @@
.run()
.expect(
"""
- src/Foo.java:7: Warning: ITest permission check can be converted to @EnforcePermission annotation [UseEnforcePermissionAnnotation]
+ src/Foo.java:7: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement]
mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
0 errors, 1 warnings
@@ -92,7 +92,7 @@
.run()
.expect(
"""
- src/Foo.java:8: Warning: ITest permission check can be converted to @EnforcePermission annotation [UseEnforcePermissionAnnotation]
+ src/Foo.java:8: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement]
mContext.enforceCallingOrSelfPermission(
^
0 errors, 1 warnings
@@ -132,7 +132,7 @@
.run()
.expect(
"""
- src/Foo.java:8: Warning: ITest permission check can be converted to @EnforcePermission annotation [UseEnforcePermissionAnnotation]
+ src/Foo.java:8: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement]
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.READ_CONTACTS, "foo");
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
0 errors, 1 warnings
@@ -174,7 +174,7 @@
.run()
.expect(
"""
- src/Foo.java:10: Warning: ITest permission check can be converted to @EnforcePermission annotation [UseEnforcePermissionAnnotation]
+ src/Foo.java:10: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement]
mContext.enforceCallingOrSelfPermission(
^
0 errors, 1 warnings
@@ -243,7 +243,7 @@
.run()
.expect(
"""
- src/Foo.java:14: Warning: ITest permission check can be converted to @EnforcePermission annotation [UseEnforcePermissionAnnotation]
+ src/Foo.java:14: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement]
helper();
~~~~~~~~~
0 errors, 1 warnings
@@ -289,7 +289,7 @@
.run()
.expect(
"""
- src/Foo.java:16: Warning: ITest permission check can be converted to @EnforcePermission annotation [UseEnforcePermissionAnnotation]
+ src/Foo.java:16: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement]
mContext.enforceCallingOrSelfPermission("FOO", "foo");
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
0 errors, 1 warnings
@@ -340,7 +340,7 @@
.run()
.expect(
"""
- src/Foo.java:19: Warning: ITest permission check can be converted to @EnforcePermission annotation [UseEnforcePermissionAnnotation]
+ src/Foo.java:19: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement]
helperHelper();
~~~~~~~~~~~~~~~
0 errors, 1 warnings